// script.js
(() => {
  'use strict';

  // -----------------------------
  // Organic app logic
  // -----------------------------
  document.addEventListener('DOMContentLoaded', () => {
    // Shared values
    const {
      PX_PER_INCH,
      SIZE_PRESETS,
      TEXTURE_ZOOM_DEFAULT,
      TEXTURE_FOCUS_DEFAULT,
      SWATCH_TEXTURE_ZOOM,
      PNG_EXPORT_SCALE,
      clamp,
      clamp01,
      normalizeHex,
      hexToRgb,
      shineStyle,
      luminance,
      FLAT_COLORS,
      NAME_BY_HEX,
      HEX_TO_FIRST_IDX,
      allowedSet,
      getImage: sharedGetImage,
      imageUrlToDataUrl,
      download: sharedDownload,
      XLINK_NS,
      blobToDataUrl,
      imageToDataUrl,
      DATA_URL_CACHE,
    } = window.shared || {};
    if (!window.shared) return;
    const getImageHrefShared = (el) => el.getAttribute('href') || el.getAttributeNS(window.shared.XLINK_NS, 'href');
    const getImage = (path) => sharedGetImage(path, () => draw());

    // ====== Shine ellipse tuning ======
    const SHINE_OFFSET = 0.30, SHINE_RX = 0.40, SHINE_RY = 0.24, SHINE_ROT = -25, SHINE_ALPHA = 0.20; // ROT is now in degrees
    let view = { s: 1, tx: 0, ty: 0 };
    const FIT_PADDING_PX = 30;

    const VIEW_MIN_SCALE = 0.12;
    const VIEW_MAX_SCALE = 1.05;
    const MAX_BALLOONS = 800;

    // ====== Garland path defaults ======
    const GARLAND_POINT_STEP = 8;
    const GARLAND_BASE_DIAM = 18;
    const GARLAND_FILLER_DIAMS = [11, 9];
    const GARLAND_ACCENT_DIAM = 5;
    const GARLAND_SPACING_RATIO = 0.85; // spacing along path vs base diameter
    const GARLAND_WOBBLE_RATIO = 0.35;
    const GARLAND_SIZE_JITTER = 0.14;

    // Make sure shared palette is populated (fallback in case shared init missed)
    if (Array.isArray(window.PALETTE) && FLAT_COLORS.length === 0) {
      window.PALETTE.forEach(group => {
        (group.colors || []).forEach(c => {
          if (!c?.hex) return;
          const item = { ...c, family: group.family };
          item.imageZoom = Number.isFinite(c.imageZoom) ? Math.max(1, c.imageZoom) : TEXTURE_ZOOM_DEFAULT;
          item.imageFocus = {
            x: clamp01(c.imageFocusX ?? c.imageFocus?.x ?? TEXTURE_FOCUS_DEFAULT.x),
            y: clamp01(c.imageFocusY ?? c.imageFocus?.y ?? TEXTURE_FOCUS_DEFAULT.y)
          };
          item._idx = FLAT_COLORS.length;
          FLAT_COLORS.push(item);
          const key = (c.hex || '').toLowerCase();
          if (!NAME_BY_HEX.has(key)) NAME_BY_HEX.set(key, c.name);
          if (!HEX_TO_FIRST_IDX.has(key)) HEX_TO_FIRST_IDX.set(key, item._idx);
          allowedSet.add(key);
        });
      });
    }

    // Ensure palette exists if shared initialization was skipped
    if (Array.isArray(window.PALETTE) && FLAT_COLORS.length === 0) {
      window.PALETTE.forEach(group => {
        (group.colors || []).forEach(c => {
          if (!c?.hex) return;
          const item = { ...c, family: group.family };
          item.imageZoom = Number.isFinite(c.imageZoom) ? Math.max(1, c.imageZoom) : TEXTURE_ZOOM_DEFAULT;
          item.imageFocus = {
            x: clamp01(c.imageFocusX ?? c.imageFocus?.x ?? TEXTURE_FOCUS_DEFAULT.x),
            y: clamp01(c.imageFocusY ?? c.imageFocus?.y ?? TEXTURE_FOCUS_DEFAULT.y)
          };
          item._idx = FLAT_COLORS.length;
          FLAT_COLORS.push(item);
          const key = (c.hex || '').toLowerCase();
          if (!NAME_BY_HEX.has(key)) NAME_BY_HEX.set(key, c.name);
          if (!HEX_TO_FIRST_IDX.has(key)) HEX_TO_FIRST_IDX.set(key, item._idx);
          allowedSet.add(key);
        });
      });
    }

    const makeSeededRng = seed => {
      let s = seed || 1;
      return () => {
        s ^= s << 13;
        s ^= s >>> 17;
        s ^= s << 5;
        return (s >>> 0) / 4294967296;
      };
    };

    const QUERY_KEY = 'd';

    // ====== DOM ======
    const canvas = document.getElementById('balloon-canvas');
    const ctx = canvas?.getContext('2d');
    const orgSheet = document.getElementById('controls-panel');
    const claSheet = document.getElementById('classic-controls-panel');
    const wallSheet = document.getElementById('wall-controls-panel');
    // tool buttons
    const toolDrawBtn = document.getElementById('tool-draw');
    const toolGarlandBtn = document.getElementById('tool-garland');
    const toolEraseBtn = document.getElementById('tool-erase');
    const toolSelectBtn = document.getElementById('tool-select');
    const toolUndoBtn = document.getElementById('tool-undo');
    const toolRedoBtn = document.getElementById('tool-redo');

    // panels/controls
    const eraserControls = document.getElementById('eraser-controls');
    const selectControls = document.getElementById('select-controls');
    const eraserSizeInput = document.getElementById('eraser-size');
    const eraserSizeLabel = document.getElementById('eraser-size-label');
    const deleteSelectedBtn = document.getElementById('delete-selected');
    const duplicateSelectedBtn = document.getElementById('duplicate-selected');
    const selectedSizeInput = document.getElementById('selected-size');
    const selectedSizeLabel = document.getElementById('selected-size-label');
    const nudgeSelectedBtns = Array.from(document.querySelectorAll('.nudge-selected'));
    const bringForwardBtn = document.getElementById('bring-forward');
    const sendBackwardBtn = document.getElementById('send-backward');
    const applyColorBtn = document.getElementById('apply-selected-color');
    const fitViewBtn = document.getElementById('fit-view-btn');
    const garlandDensityInput = document.getElementById('garland-density');
    const garlandDensityLabel = document.getElementById('garland-density-label');
    const garlandMainChips = document.getElementById('garland-main-chips');
    const garlandAddColorBtn = document.getElementById('garland-add-color');
    const garlandAccentChip = document.getElementById('garland-accent-chip');
    const garlandAccentClearBtn = document.getElementById('garland-accent-clear');
    const garlandControls = document.getElementById('garland-controls');

    const sizePresetGroup = document.getElementById('size-preset-group');
    const toggleShineBtn = null;
    const toggleShineCheckbox = document.getElementById('toggle-shine-checkbox');
    const toggleBorderCheckbox = document.getElementById('toggle-border-checkbox');

    const paletteBox = document.getElementById('color-palette');
    const usedPaletteBox = document.getElementById('used-palette');
    const sortUsedToggle = document.getElementById('sort-used-toggle');

    // replace colors panel
    const replaceFromSel = document.getElementById('replace-from');
    const replaceToSel = document.getElementById('replace-to');
    const replaceBtn = document.getElementById('replace-btn');
    const replaceMsg = document.getElementById('replace-msg');
    const replaceFromChip = document.getElementById('replace-from-chip');
    const replaceToChip = document.getElementById('replace-to-chip');
    const replaceCountLabel = document.getElementById('replace-count');

    // IO
    const clearCanvasBtn = document.getElementById('clear-canvas-btn');
    const saveJsonBtn = document.getElementById('save-json-btn');
    const loadJsonInput = document.getElementById('load-json-input');

    const generateLinkBtn = document.getElementById('generate-link-btn');
    const shareLinkOutput = document.getElementById('share-link-output');
    const copyMessage = document.getElementById('copy-message');
    const clearCanvasBtnTop = document.getElementById('clear-canvas-btn-top');
    // Debug overlay to diagnose mobile input issues
    const debugOverlay = document.createElement('div');
    debugOverlay.id = 'organic-debug-overlay';
    debugOverlay.style.cssText = 'position:fixed;bottom:8px;right:8px;z-index:9999;background:rgba(0,0,0,0.7);color:#fff;padding:6px 8px;border-radius:8px;font-size:10px;font-family:monospace;pointer-events:none;opacity:0.9;line-height:1.3;display:none;';
    debugOverlay.textContent = 'organic debug';
    document.body.appendChild(debugOverlay);

    // messages
    const messageModal = document.getElementById('message-modal');
    const modalText = document.getElementById('modal-text');
    const modalCloseBtn = document.getElementById('modal-close-btn');

    // layout
    const controlsPanel = document.getElementById('controls-panel');
    const canvasPanel = document.getElementById('canvas-panel');
    const expandBtn = null;
    const fullscreenBtn = null;

    if (!canvas || !ctx) return; // nothing to do if organic UI isn't on page

    // ====== State ======
    let balloons = [];
    let selectedColorIdx = 0;
    let currentDiameterInches = 11;
    let currentRadius = inchesToRadiusPx(currentDiameterInches);
    let isShineEnabled = true; // will be initialized from localStorage
    let isBorderEnabled = true;

    let dpr = 1;
    let mode = 'draw';
    let eraserRadius = parseInt(eraserSizeInput?.value || '40', 10);
    let mouseInside = false;
    let mousePos = { x: 0, y: 0 };
    let selectedIds = new Set();
    let usedSortDesc = true;
    let garlandPath = [];
    let garlandDensity = parseFloat(garlandDensityInput?.value || '1') || 1;
    let garlandMainIdx = [0];
    let garlandAccentIdx = -1;
    let lastCommitMode = '';
    let lastAddStatus = '';
    let evtStats = { down: 0, up: 0, cancel: 0, touchEnd: 0, addBalloon: 0, addGarland: 0, lastType: '' };

    // History for Undo/Redo
    const historyStack = [];
    let historyPointer = -1;
    function resetHistory() {
      historyStack.length = 0;
      historyPointer = -1;
      pushHistory();
    }

    function updateHistoryUi() {
      const canUndo = historyPointer > 0;
      const canRedo = historyPointer < historyStack.length - 1;
      if (toolUndoBtn) {
        toolUndoBtn.disabled = !canUndo;
        toolUndoBtn.title = canUndo ? 'Undo' : 'Nothing to undo';
      }
      if (toolRedoBtn) {
        toolRedoBtn.disabled = !canRedo;
        toolRedoBtn.title = canRedo ? 'Redo' : 'Nothing to redo';
      }
    }

    function pushHistory() {
      // Remove any future history if we are in the middle of the stack
      if (historyPointer < historyStack.length - 1) {
        historyStack.splice(historyPointer + 1);
      }
      // Deep clone balloons array
      const snapshot = JSON.parse(JSON.stringify(balloons));
      historyStack.push(snapshot);
      historyPointer++;
      // Limit stack size
      if (historyStack.length > 50) {
        historyStack.shift();
        historyPointer--;
      }
      updateHistoryUi();
    }

    function undo() {
      if (historyPointer > 0) {
        historyPointer--;
        balloons = JSON.parse(JSON.stringify(historyStack[historyPointer]));
        selectedIds.clear(); // clear selection on undo to avoid issues
        updateSelectButtons();
        draw();
        renderUsedPalette();
        persist();
      }
      updateHistoryUi();
    }

    function redo() {
      if (historyPointer < historyStack.length - 1) {
        historyPointer++;
        balloons = JSON.parse(JSON.stringify(historyStack[historyPointer]));
        selectedIds.clear();
        updateSelectButtons();
        draw();
        renderUsedPalette();
        persist();
      }
      updateHistoryUi();
    }

    // Bind Undo/Redo Buttons
    toolUndoBtn?.addEventListener('click', undo);
    toolRedoBtn?.addEventListener('click', redo);

    // Eyedropper Tool
    const toolEyedropperBtn = document.getElementById('tool-eyedropper');
    toolEyedropperBtn?.addEventListener('click', () => {
        // Toggle eyedropper mode
        if (mode === 'eyedropper') {
            setMode('draw'); // toggle off
        } else {
            setMode('eyedropper');
        }
    });


    function clampViewScale() {
      view.s = clamp(view.s, VIEW_MIN_SCALE, VIEW_MAX_SCALE);
    }
    function inchesToRadiusPx(diam) { return (diam * PX_PER_INCH) / 2; }
    function radiusPxToInches(r) { return (r * 2) / PX_PER_INCH; }
    function fmtInches(val) {
      const v = Math.round(val * 10) / 10;
      return `${String(v).replace(/\.0$/, '')}"`;
    }
    const makeId = (() => {
      let n = 0;
      return () => {
        try { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID(); } catch {}
        return `b-${Date.now().toString(36)}-${(n++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
      };
    })();

    function radiusToSizeIndex(r) {
      let best = 0, bestDiff = Infinity;
      for (let i = 0; i < SIZE_PRESETS.length; i++) {
        const diff = Math.abs(inchesToRadiusPx(SIZE_PRESETS[i]) - r);
        if (diff < bestDiff) { best = i; bestDiff = diff; }
      }
      return best;
    }
    function showModal(msg, opts = {}) {
      if (window.Swal) {
        Swal.fire({
          title: opts.title || 'Notice',
          text: msg,
          icon: opts.icon || 'info',
          confirmButtonText: opts.confirmText || 'OK'
        });
        return;
      }
      if (!messageModal || !modalText) { window.alert?.(msg); return; }
      modalText.textContent = msg;
      messageModal.classList.remove('hidden');
    }
    function hideModal() {
      if (window.Swal) { Swal.close?.(); return; }
      if (!messageModal) return;
      messageModal.classList.add('hidden');
    }
    function showCopyMessage() { if (!copyMessage) return; copyMessage.classList.add('show'); setTimeout(() => copyMessage.classList.remove('show'), 2000); }
    function getMousePos(e) {
      const r = canvas.getBoundingClientRect();
      return {
        x: (e.clientX - r.left) / view.s - view.tx,
        y: (e.clientY - r.top) / view.s - view.ty
      };
    }

    // ====== Global shine sync (shared with Classic)
    window.syncAppShine = function(isEnabled) {
      isShineEnabled = isEnabled;

      // mirror both UIs
      const organicBtn = document.getElementById('toggle-shine-btn');
      const classicCb  = document.getElementById('classic-shine-enabled');
      if (organicBtn) organicBtn.textContent = isEnabled ? 'Turn Off Shine' : 'Turn On Shine';
      if (classicCb)  classicCb.checked = isEnabled;

      try { localStorage.setItem('app:shineEnabled:v1', JSON.stringify(isEnabled)); } catch {}

      // push into Classic engine if available
      if (window.ClassicDesigner?.api?.setShineEnabled) {
        window.ClassicDesigner.api.setShineEnabled(isEnabled);
      }

      // redraw both tabs (cheap + robust)
      try { draw?.(); } catch {}
      try { window.ClassicDesigner?.redraw?.(); } catch {}
    };

    function setMode(next) {
      if (mode === 'garland' && next !== 'garland') {
        garlandPath = [];
      }
      mode = next;
      toolDrawBtn?.setAttribute('aria-pressed', String(mode === 'draw'));
      toolGarlandBtn?.setAttribute('aria-pressed', String(mode === 'garland'));
      toolEraseBtn?.setAttribute('aria-pressed', String(mode === 'erase'));
      toolSelectBtn?.setAttribute('aria-pressed', String(mode === 'select'));
      toolEyedropperBtn?.setAttribute('aria-pressed', String(mode === 'eyedropper'));
      const mobileErase = document.getElementById('mobile-act-erase');
      const mobilePick = document.getElementById('mobile-act-eyedrop');
      const setActive = (el, on) => {
        if (!el) return;
        el.setAttribute('aria-pressed', String(on));
        el.classList.toggle('active', !!on);
      };
      setActive(mobileErase, mode === 'erase');
      setActive(mobilePick, mode === 'eyedropper');
      
      eraserControls?.classList.toggle('hidden', mode !== 'erase');
      selectControls?.classList.toggle('hidden', mode !== 'select');
      garlandControls?.classList.toggle('hidden', mode !== 'garland');
      
      if (mode === 'erase') canvas.style.cursor = 'none';
      else if (mode === 'select') canvas.style.cursor = 'default'; // will be move over items
      else if (mode === 'garland') canvas.style.cursor = 'crosshair';
      else if (mode === 'eyedropper') canvas.style.cursor = 'cell';
      else canvas.style.cursor = 'crosshair';
      
      draw();
      persist();
    }

    function selectionArray() { return Array.from(selectedIds); }
    function selectionBalloons() {
      const set = new Set(selectedIds);
      return balloons.filter(b => set.has(b.id));
    }
    function setSelection(ids, { additive = false } = {}) {
      if (!additive) selectedIds.clear();
      ids.forEach(id => selectedIds.add(id));
      updateSelectButtons();
      draw();
    }
    function primarySelection() {
      const first = selectedIds.values().next();
      return first.done ? null : first.value;
    }
    function clearSelection() { selectedIds.clear(); updateSelectButtons(); draw(); }
    function updateSelectButtons() {
      const has = selectedIds.size > 0;
      if (deleteSelectedBtn) deleteSelectedBtn.disabled = !has;
      if (duplicateSelectedBtn) duplicateSelectedBtn.disabled = !has;
      if (selectedSizeInput) {
        selectedSizeInput.disabled = !has;
        selectedSizeInput.min = '5';
        selectedSizeInput.max = '32';
        selectedSizeInput.step = '0.5';
      }
      if (bringForwardBtn) bringForwardBtn.disabled = !has;
      if (sendBackwardBtn) sendBackwardBtn.disabled = !has;
      if (applyColorBtn) applyColorBtn.disabled = !has;
      if (selectedSizeInput && selectedSizeLabel) {
        if (has) {
          const first = balloons.find(bb => selectedIds.has(bb.id));
          if (first) {
            const diam = radiusPxToInches(first.radius);
            selectedSizeInput.value = String(Math.min(32, Math.max(5, diam)));
            selectedSizeLabel.textContent = fmtInches(diam);
          }
        } else {
          selectedSizeLabel.textContent = '0"';
        }
      }
    }

    // ====== Pointer Events ======
    let pointerDown = false;
    let isDragging = false;
    let dragStartPos = { x: 0, y: 0 };
    let initialBalloonPos = { x: 0, y: 0 };
    let eraseChanged = false;
    let dragMoved = false;
    let resizeChanged = false;
    let resizeSaveTimer = null;
    let erasingActive = false;
    let drawPending = false;
    let dragOffsets = [];
    let marqueeActive = false;
    let marqueeStart = { x: 0, y: 0 };
    let marqueeEnd = { x: 0, y: 0 };
    let pointerEventsSeen = false;
    let touchFallbackHandled = false;

    function requestDraw() {
      if (drawPending) return;
      drawPending = true;
      requestAnimationFrame(() => {
        drawPending = false;
        draw();
      });
    }

    const pointerTypeOf = (evt, fromTouch) => fromTouch ? 'touch' : (evt.pointerType || '');

    function handlePrimaryDown(evt, { fromTouch = false } = {}) {
      // If the canvas never got sized (some mobile browsers skip ResizeObserver early), size it now.
      if (canvas.width === 0 || canvas.height === 0) resizeCanvas();
      mouseInside = true;
      mousePos = getMousePos(evt);
      evtStats.down += 1;
      evtStats.lastType = pointerTypeOf(evt, fromTouch);

      if (evt.altKey || mode === 'eyedropper') {
        pickColorAt(mousePos.x, mousePos.y);
        if (mode === 'eyedropper') setMode('draw');
        return;
      }

      if (mode === 'erase') {
        pointerDown = true;
        erasingActive = true;
        eraseChanged = eraseAt(mousePos.x, mousePos.y);
        return;
      }

      if (mode === 'garland') {
        pointerDown = true;
        garlandPath = [{ ...mousePos }];
        requestDraw();
        return;
      }

      if (mode === 'select') {
        pointerDown = true;
        const clickedIdx = findBalloonIndexAt(mousePos.x, mousePos.y);
        if (clickedIdx !== -1) {
          const b = balloons[clickedIdx];
          if (evt.shiftKey) {
            if (selectedIds.has(b.id)) selectedIds.delete(b.id);
            else selectedIds.add(b.id);
          } else if (!selectedIds.has(b.id)) {
            selectedIds.clear();
            selectedIds.add(b.id);
          }
          updateSelectButtons();
          draw();
          isDragging = true;
          dragStartPos = { ...mousePos };
          dragOffsets = selectionBalloons().map(bb => ({ id: bb.id, dx: bb.x - mousePos.x, dy: bb.y - mousePos.y }));
          dragMoved = false;
        } else {
          if (!evt.shiftKey) selectedIds.clear();
          updateSelectButtons();
          marqueeActive = true;
          marqueeStart = { ...mousePos };
          marqueeEnd = { ...mousePos };
          requestDraw();
        }
        return;
      }

      addBalloon(mousePos.x, mousePos.y);
      pointerDown = true;
    }

    function handlePrimaryMove(evt, { fromTouch = false } = {}) {
      mouseInside = true;
      mousePos = getMousePos(evt);

      if (mode === 'select') {
        if (isDragging && selectedIds.size) {
          const dx = mousePos.x - dragStartPos.x;
          const dy = mousePos.y - dragStartPos.y;
          dragOffsets.forEach(off => {
            const b = balloons.find(bb => bb.id === off.id);
            if (b) {
              b.x = mousePos.x + off.dx;
              b.y = mousePos.y + off.dy;
            }
          });
          requestDraw();
          dragMoved = true;
        } else if (marqueeActive) {
          marqueeEnd = { ...mousePos };
          requestDraw();
        } else {
          const hoverIdx = findBalloonIndexAt(mousePos.x, mousePos.y);
          canvas.style.cursor = (hoverIdx !== -1) ? 'move' : 'default';
        }
      }

      if (mode === 'garland') {
        if (pointerDown) {
          const last = garlandPath[garlandPath.length - 1];
          if (!last || Math.hypot(mousePos.x - last.x, mousePos.y - last.y) >= GARLAND_POINT_STEP) {
            garlandPath.push({ ...mousePos });
            requestDraw();
          }
        }
        return;
      }

      if (mode === 'erase') {
        if (pointerDown) {
          eraseChanged = eraseAt(mousePos.x, mousePos.y) || eraseChanged;
          if (eraseChanged) requestDraw();
        } else {
          requestDraw();
        }
      }
    }

    function handlePrimaryUp(evt, { fromTouch = false } = {}) {
      pointerDown = false;
      isDragging = false;
      evtStats.up += 1;
      evtStats.lastType = pointerTypeOf(evt, fromTouch);
      if (fromTouch) evtStats.touchEnd += 1;

      if (mode === 'garland') {
        if (garlandPath.length > 1) addGarlandFromPath(garlandPath);
        garlandPath = [];
        requestDraw();
        return;
      }
      if (mode === 'select' && dragMoved) {
        refreshAll();
        pushHistory();
      }
      if (mode === 'select' && marqueeActive) {
        const minX = Math.min(marqueeStart.x, marqueeEnd.x);
        const maxX = Math.max(marqueeStart.x, marqueeEnd.x);
        const minY = Math.min(marqueeStart.y, marqueeEnd.y);
        const maxY = Math.max(marqueeStart.y, marqueeEnd.y);
        const ids = balloons.filter(b => b.x >= minX && b.x <= maxX && b.y >= minY && b.y <= maxY).map(b => b.id);
        if (!evt.shiftKey) selectedIds.clear();
        ids.forEach(id => selectedIds.add(id));
        marqueeActive = false;
        updateSelectButtons();
        requestDraw();
      }
      if (mode === 'erase' && eraseChanged) {
        refreshAll();
        pushHistory();
      }
      erasingActive = false;
      dragMoved = false;
      eraseChanged = false;
      marqueeActive = false;
    }

    function handlePrimaryCancel(evt, { fromTouch = false } = {}) {
      pointerDown = false;
      evtStats.cancel += 1;
      evtStats.lastType = pointerTypeOf(evt, fromTouch);
      if (mode === 'garland') {
        garlandPath = [];
        requestDraw();
      }
    }

    // Avoid touch scrolling stealing pointer events.
    canvas.style.touchAction = 'none';

    canvas.addEventListener('pointerdown', e => {
      // If a touch fallback already handled this gesture, ignore the duplicate pointer event.
      if (touchFallbackHandled && e.pointerType === 'touch') return;
      pointerEventsSeen = true;
      touchFallbackHandled = false;
      e.preventDefault();
      canvas.setPointerCapture?.(e.pointerId);
      handlePrimaryDown(e, { fromTouch: e.pointerType === 'touch' });
    }, { passive: false });

    canvas.addEventListener('pointermove', e => {
      if (touchFallbackHandled && e.pointerType === 'touch') return;
      handlePrimaryMove(e, { fromTouch: e.pointerType === 'touch' });
    }, { passive: true });

    canvas.addEventListener('pointerenter', () => {
      mouseInside = true;
      if (mode === 'erase') requestDraw();
    });

    canvas.addEventListener('pointerup', e => {
      if (touchFallbackHandled && e.pointerType === 'touch') {
        canvas.releasePointerCapture?.(e.pointerId);
        return;
      }
      handlePrimaryUp(e, { fromTouch: e.pointerType === 'touch' });
      canvas.releasePointerCapture?.(e.pointerId);
    }, { passive: true });

    canvas.addEventListener('pointerleave', () => {
      mouseInside = false;
      marqueeActive = false;
      if (mode === 'garland') {
        pointerDown = false;
        garlandPath = [];
        requestDraw();
      }
      if (mode === 'erase') requestDraw();
    }, { passive: true });

    canvas.addEventListener('pointercancel', e => {
      if (touchFallbackHandled && e.pointerType === 'touch') return;
      handlePrimaryCancel(e, { fromTouch: e.pointerType === 'touch' });
    }, { passive: true });

    // Touch fallback for browsers where pointer events are not delivered.
    const touchToPointerLike = (e) => {
      const t = e.changedTouches && e.changedTouches[0];
      if (!t) return null;
      return {
        clientX: t.clientX,
        clientY: t.clientY,
        pointerType: 'touch',
        altKey: e.altKey,
        shiftKey: e.shiftKey
      };
    };

    const shouldHandleTouchFallback = () => !pointerEventsSeen;

    canvas.addEventListener('touchstart', e => {
      if (!shouldHandleTouchFallback()) return;
      const fake = touchToPointerLike(e);
      if (!fake) return;
      touchFallbackHandled = true;
      e.preventDefault();
      handlePrimaryDown(fake, { fromTouch: true });
    }, { passive: false });

    canvas.addEventListener('touchmove', e => {
      if (!touchFallbackHandled || !shouldHandleTouchFallback()) return;
      const fake = touchToPointerLike(e);
      if (!fake) return;
      e.preventDefault();
      handlePrimaryMove(fake, { fromTouch: true });
    }, { passive: false });

    canvas.addEventListener('touchend', e => {
      if (!touchFallbackHandled || !shouldHandleTouchFallback()) return;
      const fake = touchToPointerLike(e) || { pointerType: 'touch', shiftKey: false, altKey: false, clientX: 0, clientY: 0 };
      e.preventDefault();
      handlePrimaryUp(fake, { fromTouch: true });
      touchFallbackHandled = false;
    }, { passive: false });

    canvas.addEventListener('touchcancel', e => {
      if (!touchFallbackHandled || !shouldHandleTouchFallback()) return;
      const fake = touchToPointerLike(e) || { pointerType: 'touch' };
      handlePrimaryCancel(fake, { fromTouch: true });
      touchFallbackHandled = false;
    }, { passive: true });

    // No global pointer/touch commits; rely on canvas handlers (as in the working older version).

    // ====== Canvas & Drawing ======
    function resizeCanvas() {
      const rect = canvas.getBoundingClientRect();
      dpr = Math.max(1, window.devicePixelRatio || 1);
      canvas.width = Math.round(rect.width * dpr);
      canvas.height = Math.round(rect.height * dpr);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      fitView();
      draw();
    }
    function clearCanvasArea() {
      ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
    }

    function draw() {
      clearCanvasArea();
      ctx.save();
      ctx.scale(view.s, view.s);
      ctx.translate(view.tx, view.ty);

      balloons.forEach(b => {
        if (b.image) {
          const img = getImage(b.image);
          if (img && img.complete && img.naturalWidth > 0) {
            const meta = FLAT_COLORS[b.colorIdx] || {};
            const zoom = Math.max(1, meta.imageZoom ?? TEXTURE_ZOOM_DEFAULT);
            const fx = clamp01(meta.imageFocus?.x ?? TEXTURE_FOCUS_DEFAULT.x);
            const fy = clamp01(meta.imageFocus?.y ?? TEXTURE_FOCUS_DEFAULT.y);

            const srcW = img.naturalWidth / zoom;
            const srcH = img.naturalHeight / zoom;
            const srcX = clamp(fx * img.naturalWidth  - srcW/2, 0, img.naturalWidth  - srcW);
          const srcY = clamp(fy * img.naturalHeight - srcH/2, 0, img.naturalHeight - srcH);

          ctx.save();
          ctx.beginPath();
          ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
          ctx.clip();
          const lum = luminance(meta.hex || b.color);
          if (lum > 0.6) {
            const strength = clamp01((lum - 0.6) / 0.4); // more shadow for lighter colors
            ctx.shadowColor = `rgba(0,0,0,${0.05 + 0.07 * strength})`;
            ctx.shadowBlur = 4 + 4 * strength;
            ctx.shadowOffsetY = 1 + 2 * strength;
          }
          ctx.drawImage(img, srcX, srcY, srcW, srcH, b.x - b.radius, b.y - b.radius, b.radius * 2, b.radius * 2);
          if (isBorderEnabled) {
            ctx.strokeStyle = '#111827';
            ctx.lineWidth = Math.max(0.35, 0.5 / view.s);
            ctx.stroke();
          }
          ctx.restore();
        } else {
          // fallback solid
          ctx.beginPath();
          ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
          ctx.fillStyle = b.color;
          ctx.shadowColor = 'rgba(0,0,0,0.2)';
          ctx.shadowBlur = 10;
          ctx.fill();
          if (isBorderEnabled) {
            ctx.strokeStyle = '#111827';
            ctx.lineWidth = Math.max(0.35, 0.5 / view.s);
            ctx.stroke();
          }
          ctx.shadowBlur = 0;
        }
      } else {
        // solid fill
        ctx.beginPath();
        ctx.arc(b.x, b.y, b.radius, 0, Math.PI * 2);
        ctx.fillStyle = b.color;
        ctx.shadowColor = 'rgba(0,0,0,0.2)';
        ctx.shadowBlur = 10;
        ctx.fill();
        if (isBorderEnabled) {
          ctx.strokeStyle = '#111827';
          ctx.lineWidth = Math.max(0.35, 0.5 / view.s);
          ctx.stroke();
        }
        ctx.shadowBlur = 0;
      }

        if (isShineEnabled) {
          const { fill: shineFill, stroke: shineStroke } = shineStyle(b.color);
          const sx = b.x - b.radius * SHINE_OFFSET;
          const sy = b.y - b.radius * SHINE_OFFSET;
          const rx = b.radius * SHINE_RX;
          const ry = b.radius * SHINE_RY;
          const rotRad = SHINE_ROT * Math.PI / 180;
          ctx.save();
          ctx.shadowColor = 'rgba(0,0,0,0.1)';
          ctx.shadowBlur = 3; // SHINE_BLUR
          ctx.beginPath();
          if (ctx.ellipse) {
            ctx.ellipse(sx, sy, rx, ry, rotRad, 0, Math.PI * 2);
          } else {
            ctx.translate(sx, sy);
            ctx.rotate(rotRad);
            ctx.scale(rx / ry, 1);
            ctx.arc(0, 0, ry, 0, Math.PI * 2);
          }
          ctx.fillStyle = shineFill;
      if (shineStroke) {
        ctx.strokeStyle = shineStroke;
        ctx.lineWidth = 1.5;
        ctx.stroke();
      }
      ctx.fill();
      ctx.restore();
    }
      });

      // garland path preview
      if (mode === 'garland' && garlandPath.length) {
        ctx.save();
        ctx.lineWidth = 1.5 / view.s;
        ctx.strokeStyle = 'rgba(59,130,246,0.7)';
        ctx.setLineDash([8 / view.s, 6 / view.s]);
        ctx.beginPath();
        garlandPath.forEach((p, idx) => {
          if (idx === 0) ctx.moveTo(p.x, p.y);
          else ctx.lineTo(p.x, p.y);
        });
        ctx.stroke();
        ctx.setLineDash([]);
        const previewNodes = computeGarlandNodes(garlandPath).sort((a, b) => b.radius - a.radius);
        ctx.strokeStyle = 'rgba(59,130,246,0.28)';
        previewNodes.forEach(n => {
          ctx.beginPath();
          ctx.arc(n.x, n.y, n.radius, 0, Math.PI * 2);
          ctx.stroke();
        });
        ctx.restore();
      }

      // selection ring(s)
      if (selectedIds.size) {
        ctx.save();
        selectedIds.forEach(id => {
          const b = balloons.find(bb => bb.id === id);
          if (!b) return;
          ctx.beginPath();
          ctx.arc(b.x, b.y, b.radius + 3, 0, Math.PI * 2);
          ctx.lineWidth = 4 / view.s;
          ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
          ctx.stroke();
          ctx.lineWidth = 2 / view.s;
          ctx.strokeStyle = '#3b82f6';
          ctx.stroke();
        });
        ctx.restore();
      }

      // marquee preview
      if (mode === 'select' && marqueeActive) {
        ctx.save();
        ctx.setLineDash([6 / view.s, 4 / view.s]);
        ctx.lineWidth = 1.5 / view.s;
        ctx.strokeStyle = 'rgba(59,130,246,0.8)';
        ctx.fillStyle = 'rgba(59,130,246,0.12)';
        const x = Math.min(marqueeStart.x, marqueeEnd.x);
        const y = Math.min(marqueeStart.y, marqueeEnd.y);
        const w = Math.abs(marqueeStart.x - marqueeEnd.x);
        const h = Math.abs(marqueeStart.y - marqueeEnd.y);
        ctx.strokeRect(x, y, w, h);
        ctx.fillRect(x, y, w, h);
        ctx.restore();
      }

      // eraser preview
      if (mode === 'erase' && mouseInside) {
        ctx.save();
        ctx.beginPath();
        ctx.arc(mousePos.x, mousePos.y, eraserRadius, 0, Math.PI * 2);
        ctx.lineWidth = 1.5 / view.s;
        ctx.strokeStyle = 'rgba(31,41,55,0.8)';
        ctx.setLineDash([4 / view.s, 4 / view.s]);
        ctx.stroke();
        ctx.restore();
      }
      
      // eyedropper preview
      if (mode === 'eyedropper' && mouseInside) {
          ctx.save();
          ctx.beginPath();
          ctx.arc(mousePos.x, mousePos.y, 10 / view.s, 0, Math.PI * 2);
          ctx.lineWidth = 2 / view.s;
          ctx.strokeStyle = '#fff';
          ctx.stroke();
          ctx.lineWidth = 1 / view.s;
          ctx.strokeStyle = '#000';
          ctx.stroke();
          ctx.restore();
      }

      ctx.restore();

      // Debug overlay
      const dbg = [
        `mode:${mode}`,
        `balloons:${balloons.length}`,
        `garlandLen:${garlandPath.length}`,
        `pointerDown:${pointerDown}`,
        `lastCommit:${lastCommitMode || '-'}`,
        `lastAdd:${lastAddStatus || '-'}`,
        `dpr:${dpr.toFixed(2)} s:${view.s.toFixed(2)} canvas:${canvas.width}x${canvas.height}`,
        `down:${evtStats.down} up:${evtStats.up} cancel:${evtStats.cancel} touchEnd:${evtStats.touchEnd} add:${evtStats.addBalloon} type:${evtStats.lastType}`
      ];
      if (debugOverlay) debugOverlay.textContent = dbg.join(' | ');
    }

    new ResizeObserver(() => resizeCanvas()).observe(canvas.parentElement);
    canvas.style.touchAction = 'none';

    // ====== State Persistence ======
    const APP_STATE_KEY = 'obd:state:v3';

    function saveAppState() {
      // Note: isShineEnabled is managed globally.
      const state = {
        balloons,
        selectedColorIdx,
        currentDiameterInches,
        eraserRadius,
        view,
        usedSortDesc,
        garlandDensity,
        garlandMainIdx,
        garlandAccentIdx,
        isBorderEnabled
      };
      try { localStorage.setItem(APP_STATE_KEY, JSON.stringify(state)); } catch {}
    }
    const persist = (() => { let t; return () => { clearTimeout(t); t = setTimeout(saveAppState, 120); }; })();

    function loadAppState() {
      try {
        const s = JSON.parse(localStorage.getItem(APP_STATE_KEY) || '{}');
        if (Array.isArray(s.balloons)) balloons = s.balloons;
        if (typeof s.selectedColorIdx === 'number') selectedColorIdx = s.selectedColorIdx;
        if (typeof s.currentDiameterInches === 'number') {
          currentDiameterInches = s.currentDiameterInches;
          currentRadius = inchesToRadiusPx(currentDiameterInches);
        }
        if (typeof s.eraserRadius === 'number') {
          eraserRadius = s.eraserRadius;
          if (eraserSizeInput) eraserSizeInput.value = eraserRadius;
          if (eraserSizeLabel) eraserSizeLabel.textContent = eraserRadius;
        }
        if (s.view && typeof s.view.s === 'number') { view = s.view; clampViewScale(); }
        if (typeof s.usedSortDesc === 'boolean') {
          usedSortDesc = s.usedSortDesc;
          if (sortUsedToggle) sortUsedToggle.textContent = usedSortDesc ? 'Sort: Most → Least' : 'Sort: Least → Most';
        }
        if (typeof s.garlandDensity === 'number') {
          garlandDensity = clamp(s.garlandDensity, 0.6, 1.6);
          if (garlandDensityInput) garlandDensityInput.value = garlandDensity;
          if (garlandDensityLabel) garlandDensityLabel.textContent = garlandDensity.toFixed(1);
        }
        if (Array.isArray(s.garlandMainIdx)) {
          garlandMainIdx = s.garlandMainIdx.slice(0, 10).map(v => Number.isInteger(v) ? v : -1).filter((v, i) => i < 10);
          if (!garlandMainIdx.length) garlandMainIdx = [selectedColorIdx];
        }
        if (typeof s.garlandAccentIdx === 'number') garlandAccentIdx = s.garlandAccentIdx;
        if (typeof s.isBorderEnabled === 'boolean') isBorderEnabled = s.isBorderEnabled;
        if (toggleBorderCheckbox) toggleBorderCheckbox.checked = isBorderEnabled;
        updateCurrentColorChip();
      } catch {}
    }

    loadAppState();
    resetHistory(); // establish initial history state for undo/redo controls

    // ====== Garland color UI (dynamic chips) ======
    const styleChip = (el, meta) => {
      if (!el || !meta) return;
      if (meta.image) {
        el.style.backgroundImage = `url("${meta.image}")`;
        el.style.backgroundColor = meta.hex || '#fff';
        el.style.backgroundSize = `${100 * SWATCH_TEXTURE_ZOOM}%`;
        el.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
      } else {
        el.style.backgroundImage = 'none';
        el.style.backgroundColor = meta.hex || '#f1f5f9';
      }
    };

    const garlandMaxColors = 10;
    function renderGarlandMainChips() {
      if (!garlandMainChips) return;
      garlandMainChips.innerHTML = '';
      const items = garlandMainIdx.length ? garlandMainIdx : [selectedColorIdx];
      items.forEach((idx, i) => {
        const wrap = document.createElement('div');
        wrap.className = 'flex items-center gap-1';
        const chip = document.createElement('button');
        chip.type = 'button';
        chip.className = 'replace-chip garland-chip';
        const meta = FLAT_COLORS[idx] || FLAT_COLORS[selectedColorIdx] || FLAT_COLORS[0];
        styleChip(chip, meta);
        chip.title = meta?.name || meta?.hex || 'Color';
        chip.addEventListener('click', () => {
          if (!window.openColorPicker) return;
          window.openColorPicker({
            title: 'Path color',
            subtitle: 'Pick a main color',
            items: (FLAT_COLORS || []).map((c, ci) => ({ label: c.name || c.hex, metaText: c.family || '', idx: ci })),
            onSelect: (item) => {
              garlandMainIdx[i] = item.idx;
              renderGarlandMainChips();
              if (mode === 'garland') requestDraw();
              persist();
            }
          });
        });
        const removeBtn = document.createElement('button');
        removeBtn.type = 'button';
        removeBtn.className = 'btn-yellow text-xs px-2 py-1';
        removeBtn.textContent = '×';
        removeBtn.title = 'Remove color';
        removeBtn.addEventListener('click', (e) => {
          e.stopPropagation();
          garlandMainIdx.splice(i, 1);
          if (!garlandMainIdx.length) garlandMainIdx.push(selectedColorIdx);
          renderGarlandMainChips();
          if (mode === 'garland') requestDraw();
          persist();
        });
        wrap.appendChild(chip);
        wrap.appendChild(removeBtn);
        garlandMainChips.appendChild(wrap);
      });
    }

    garlandAddColorBtn?.addEventListener('click', () => {
      if (garlandMainIdx.length >= garlandMaxColors) { showModal(`Max ${garlandMaxColors} colors.`); return; }
      if (!window.openColorPicker) return;
      window.openColorPicker({
        title: 'Add path color',
        subtitle: 'Choose a main color',
        items: (FLAT_COLORS || []).map((c, ci) => ({ label: c.name || c.hex, metaText: c.family || '', idx: ci })),
        onSelect: (item) => {
          garlandMainIdx.push(item.idx);
          renderGarlandMainChips();
          if (mode === 'garland') requestDraw();
          persist();
        }
      });
    });

    const updateAccentChip = () => {
      if (!garlandAccentChip) return;
      const meta = garlandAccentIdx >= 0 ? FLAT_COLORS[garlandAccentIdx] : null;
      styleChip(garlandAccentChip, meta || { hex: '#f8fafc' });
    };
    garlandAccentChip?.addEventListener('click', () => {
      if (!window.openColorPicker) return;
      window.openColorPicker({
        title: 'Accent color',
        subtitle: 'Choose a 5" accent color',
        items: (FLAT_COLORS || []).map((c, ci) => ({ label: c.name || c.hex, metaText: c.family || '', idx: ci })),
        onSelect: (item) => {
          garlandAccentIdx = item.idx;
          updateAccentChip();
          if (mode === 'garland') requestDraw();
          persist();
        }
      });
    });
    garlandAccentClearBtn?.addEventListener('click', () => {
      garlandAccentIdx = -1;
      updateAccentChip();
      if (mode === 'garland') requestDraw();
      persist();
    });

    // ====== UI Rendering (Palettes) ======
    function renderAllowedPalette() {
      if (!paletteBox) return;
      paletteBox.innerHTML = '';
      (window.PALETTE || []).forEach(group => {
        const title = document.createElement('div');
        title.className = 'family-title';
        title.textContent = group.family;
        paletteBox.appendChild(title);

        const row = document.createElement('div');
        row.className = 'swatch-row';
        (group.colors || []).forEach(c => {
          const idx =
            FLAT_COLORS.find(fc => fc.name === c.name && fc.hex === c.hex && fc.family === group.family)?._idx
            ?? HEX_TO_FIRST_IDX.get(normalizeHex(c.hex));
          const sw = document.createElement('button');
          sw.type = 'button';
          sw.className = 'swatch';
          sw.setAttribute('aria-label', c.name);

          if (c.image) {
            const meta = FLAT_COLORS[idx] || {};
            sw.style.backgroundImage = `url("${c.image}")`;
            sw.style.backgroundSize = `${100 * SWATCH_TEXTURE_ZOOM}%`;
            sw.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
          } else {
            sw.style.backgroundColor = c.hex;
          }

          if (idx === selectedColorIdx) sw.classList.add('active');
          sw.title = c.name;

          sw.addEventListener('click', () => {
            selectedColorIdx = idx ?? 0;
            renderAllowedPalette();
            updateCurrentColorChip();
            persist();
          });
          row.appendChild(sw);
        });
        paletteBox.appendChild(row);
      });
    }

    function getUsedColors() {
      const map = new Map();
      balloons.forEach(b => {
        const key = normalizeHex(b.color);
        if (!allowedSet.has(key)) return;
        if (!map.has(key)) {
          const meta = FLAT_COLORS[HEX_TO_FIRST_IDX.get(key)] || {};
          map.set(key, { hex: key, count: 0, image: meta.image, name: meta.name });
        }
        map.get(key).count++;
      });
      const arr = [...map.values()];
      arr.sort((a, b) => (usedSortDesc ? (b.count - a.count) : (a.count - b.count)));
      return arr;
    }

    function updateCurrentColorChip() {
      const meta = FLAT_COLORS[selectedColorIdx] || FLAT_COLORS[0];
      const updateChip = (chipId, labelId, { showLabel = true } = {}) => {
        const chip = document.getElementById(chipId);
        const label = labelId ? document.getElementById(labelId) : null;
        if (!chip || !meta) return;
        if (meta.image) {
          const fx = clamp01(meta.imageFocus?.x ?? TEXTURE_FOCUS_DEFAULT.x);
          const fy = clamp01(meta.imageFocus?.y ?? TEXTURE_FOCUS_DEFAULT.y);
          chip.style.backgroundImage = `url("${meta.image}")`;
          chip.style.backgroundSize = `${100 * SWATCH_TEXTURE_ZOOM}%`;
          chip.style.backgroundPosition = `${fx * 100}% ${fy * 100}%`;
          chip.style.backgroundColor = '#fff';
        } else {
          chip.style.backgroundImage = 'none';
          chip.style.backgroundColor = meta.hex || '#fff';
        }
        if (label) {
          label.textContent = showLabel ? (meta.name || meta.hex || 'Current') : '';
          label.title = meta.name || meta.hex || 'Current';
        }
        chip.title = meta.name || meta.hex || 'Current';
      };
      updateChip('current-color-chip', 'current-color-label', { showLabel: true });
      updateChip('current-color-chip-global', 'current-color-label-global', { showLabel: false });
      updateChip('mobile-active-color-chip', null, { showLabel: false });
    }
    
    window.organic = {
      getColor: () => selectedColorIdx,
      updateCurrentColorChip,
      setColor: (idx) => {
        if (!Number.isInteger(idx)) return;
        selectedColorIdx = idx;
        renderAllowedPalette?.();
        renderUsedPalette?.();
        updateCurrentColorChip?.();
        persist?.();
      },
      buildOrganicSvgPayload,
    };

    function renderUsedPalette() {
      if (!usedPaletteBox) return;
      usedPaletteBox.innerHTML = '';
      const used = getUsedColors();
      if (used.length === 0) {
        usedPaletteBox.innerHTML = '<div class="hint">No colors yet.</div>';
        if (replaceFromSel) replaceFromSel.innerHTML = '';
        return;
      }
      const row = document.createElement('div');
      row.className = 'swatch-row';
      used.forEach(item => {
        const sw = document.createElement('button');
        sw.type = 'button';
        sw.className = 'swatch';
        const name = item.name || NAME_BY_HEX.get(item.hex) || item.hex;
        sw.setAttribute('aria-label', `${name} - Count: ${item.count}`);
        
        if (item.image) {
          const meta = FLAT_COLORS[HEX_TO_FIRST_IDX.get(item.hex)] || {};
          sw.style.backgroundImage = `url("${item.image}")`;
          sw.style.backgroundSize = `${100 * SWATCH_TEXTURE_ZOOM}%`;
          sw.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
        } else {
          sw.style.backgroundColor = item.hex;
        }
        if (normalizeHex(FLAT_COLORS[selectedColorIdx]?.hex) === item.hex) sw.classList.add('active');
        sw.title = `${name} — ${item.count}`;
        sw.addEventListener('click', () => {
          selectedColorIdx = HEX_TO_FIRST_IDX.get(item.hex) ?? 0;
          renderAllowedPalette();
          renderUsedPalette();
        });

        const badge = document.createElement('div');
        badge.className = 'badge';
        badge.textContent = String(item.count);
        sw.appendChild(badge);
        row.appendChild(sw);
      });
      usedPaletteBox.appendChild(row);

      // fill "replace from"
      if (replaceFromSel) {
        replaceFromSel.innerHTML = '';
        used.forEach(item => {
          const opt = document.createElement('option');
          const name = item.name || NAME_BY_HEX.get(item.hex) || item.hex;
          opt.value = item.hex;
          opt.textContent = `${name} (${item.count})`;
          replaceFromSel.appendChild(opt);
        });
        updateReplaceChips();
      }
    }

    // ====== Balloon Ops & Data/Export ======
    function buildBalloon(meta, x, y, radius) {
      return {
        x, y,
        radius,
        color: meta.hex,
        image: meta.image || null,
        colorIdx: meta._idx,
        id: makeId()
      };
    }

    function addBalloon(x, y) {
      const meta = FLAT_COLORS[selectedColorIdx] || FLAT_COLORS[0];
      if (balloons.length >= MAX_BALLOONS) {
        lastAddStatus = 'limit';
        showModal(`Balloon limit reached (${MAX_BALLOONS}). Delete some to add more.`);
        return;
      }
      balloons.push(buildBalloon(meta, x, y, currentRadius));
      lastAddStatus = 'balloon';
      evtStats.addBalloon += 1;
      ensureVisibleAfterAdd(balloons[balloons.length - 1]);
      refreshAll();
      pushHistory();
    }

    function garlandSeed(path) {
      let h = 2166136261 >>> 0;
      path.forEach(p => {
        h ^= Math.round(p.x * 10) + 0x9e3779b9;
        h = Math.imul(h, 16777619);
        h ^= Math.round(p.y * 10);
        h = Math.imul(h, 16777619);
      });
      return h >>> 0 || 1;
    }

    function computeGarlandNodes(path) {
      if (!Array.isArray(path) || path.length < 2) return [];
      const baseRadius = inchesToRadiusPx(GARLAND_BASE_DIAM);
      const fillerRadii = GARLAND_FILLER_DIAMS.map(inchesToRadiusPx);
      const accentRadius = inchesToRadiusPx(GARLAND_ACCENT_DIAM);
      const spacing = Math.max(10, baseRadius * (GARLAND_SPACING_RATIO / garlandDensity));
      const nodes = [];
      let carry = 0;
      const rng = makeSeededRng(garlandSeed(path));

      for (let i = 0; i < path.length - 1; i++) {
        const a = path[i];
        const b = path[i + 1];
        const dx = b.x - a.x;
        const dy = b.y - a.y;
        const segLen = Math.hypot(dx, dy);
        if (segLen < 1) continue;

        let dist = carry;
        const nx = segLen > 0 ? (dy / segLen) : 0;
        const ny = segLen > 0 ? (-dx / segLen) : 0;

        while (dist <= segLen) {
          const t = dist / segLen;
          const px = a.x + dx * t;
          const py = a.y + dy * t;

          const side = rng() > 0.5 ? 1 : -1;
          const wobble = (rng() * 2 - 1) * baseRadius * GARLAND_WOBBLE_RATIO;
          const sizeJitter = 1 + (rng() * 2 - 1) * GARLAND_SIZE_JITTER;
          const r = clamp(baseRadius * sizeJitter, baseRadius * 0.75, baseRadius * 1.35);
          const baseX = px + nx * wobble;
          const baseY = py + ny * wobble;
          nodes.push({ x: baseX, y: baseY, radius: r, type: 'base' });

          // filler balloons hugging the base to thicken the line
          const fillerR = fillerRadii[Math.floor(rng() * fillerRadii.length)] || baseRadius * 0.7;
          const offset1 = r * 0.7;
          nodes.push({
            x: baseX + nx * offset1 * side,
            y: baseY + ny * offset1 * side,
            radius: fillerR * (0.9 + rng() * 0.2),
            type: 'filler'
          });

          const tangentSide = side * (rng() > 0.5 ? 1 : -1);
          const offset2 = r * 0.5;
          nodes.push({
            x: baseX + (-ny) * offset2 * tangentSide,
            y: baseY + (nx) * offset2 * tangentSide,
            radius: fillerR * (0.8 + rng() * 0.25),
            type: 'filler'
          });

          // Tight cluster of three 5" balloons, slightly more open
          {
            const clusterCenterX = baseX + nx * r * 0.25 * side;
            const clusterCenterY = baseY + ny * r * 0.25 * side;
            const baseAng = rng() * Math.PI * 2;
            const mags = [0.7, 0.95, 0.8].map(m => m * accentRadius);
            const angs = [baseAng, baseAng + (2 * Math.PI / 3), baseAng + (4 * Math.PI / 3)];
            for (let c = 0; c < 3; c++) {
              const mag = mags[c] * (0.98 + rng() * 0.08);
              const jitterAng = angs[c] + (rng() * 0.25 - 0.125);
              nodes.push({
                x: clusterCenterX + Math.cos(jitterAng) * mag,
                y: clusterCenterY + Math.sin(jitterAng) * mag,
                radius: accentRadius * (0.88 + rng() * 0.15),
                type: 'accent'
              });
            }
          }

          dist += spacing;
        }
        carry = dist - segLen;
      }

      return nodes;
    }

    function addGarlandFromPath(path) {
      const meta = FLAT_COLORS[selectedColorIdx] || FLAT_COLORS[0];
      if (!meta) return;
      const nodes = computeGarlandNodes(path).sort((a, b) => b.radius - a.radius); // draw larger first so small accents sit on top
      if (!nodes.length) { lastAddStatus = 'garland:none'; return; }
      const available = Math.max(0, MAX_BALLOONS - balloons.length);
      const limitedNodes = available ? nodes.slice(0, available) : [];
      if (!limitedNodes.length) { lastAddStatus = 'limit'; showModal(`Balloon limit reached (${MAX_BALLOONS}). Delete some to add more.`); return; }
      const newIds = [];
      const rng = makeSeededRng(garlandSeed(path) + 101);
      const metaFromIdx = idx => {
        const m = FLAT_COLORS[idx];
        return m ? m : meta;
      };
      const pickMainMeta = () => {
        const choices = garlandMainIdx.filter(v => Number.isFinite(v) && v >= 0 && FLAT_COLORS[v]);
        if (!choices.length) return meta;
        const pick = choices.length === 1 ? choices[0] : choices[Math.floor(rng() * choices.length)];
        return metaFromIdx(pick);
      };
      const accentMeta = (garlandAccentIdx >= 0 && FLAT_COLORS[garlandAccentIdx])
        ? FLAT_COLORS[garlandAccentIdx]
        : metaFromIdx(garlandMainIdx.find(v => v >= 0));

      limitedNodes.forEach(n => {
        const m = n.type === 'accent' ? accentMeta : pickMainMeta();
        const b = buildBalloon(m, n.x, n.y, n.radius);
        balloons.push(b);
        newIds.push(b.id);
      });
      lastAddStatus = `garland:${newIds.length}`;
      if (newIds.length) {
        selectedIds.clear();
        updateSelectButtons();
      }
      refreshAll();
      pushHistory();
    }

    function findBalloonIndexAt(x, y) {
      for (let i = balloons.length - 1; i >= 0; i--) {
        const b = balloons[i];
        if (Math.hypot(x - b.x, y - b.y) <= b.radius) return i;
      }
      return -1;
    }

    function selectAt(x, y) {
      const i = findBalloonIndexAt(x, y);
      selectedIds.clear();
      if (i !== -1) selectedIds.add(balloons[i].id);
      updateSelectButtons();
      draw();
    }

    function moveSelected(dx, dy) {
      const sel = selectionBalloons();
      if (!sel.length) return;
      sel.forEach(b => { b.x += dx; b.y += dy; });
      refreshAll();
      pushHistory();
    }

    function resizeSelected(newDiamInches) {
      const sel = selectionBalloons();
      if (!sel.length) return;
      const diam = clamp(newDiamInches, 5, 32);
      const newRadius = inchesToRadiusPx(diam);
      sel.forEach(b => { b.radius = newRadius; });
      refreshAll();
      updateSelectButtons();
      resizeChanged = true;
      clearTimeout(resizeSaveTimer);
      resizeSaveTimer = setTimeout(() => {
        if (resizeChanged) {
          pushHistory();
          resizeChanged = false;
        }
      }, 200);
    }

    function bringSelectedForward() {
      const sel = selectionArray();
      if (!sel.length) return;
      const set = new Set(sel);
      const kept = balloons.filter(b => !set.has(b.id));
      const moving = balloons.filter(b => set.has(b.id));
      balloons = kept.concat(moving);
      refreshAll({ autoFit: true });
      pushHistory();
    }

    function sendSelectedBackward() {
      const sel = selectionArray();
      if (!sel.length) return;
      const set = new Set(sel);
      const moving = balloons.filter(b => set.has(b.id));
      const kept = balloons.filter(b => !set.has(b.id));
      balloons = moving.concat(kept);
      refreshAll({ autoFit: true });
      pushHistory();
    }

    function applyColorToSelected() {
      const meta = FLAT_COLORS[selectedColorIdx] || FLAT_COLORS[0];
      if (!meta) return;
      let changed = false;
      selectionBalloons().forEach(b => {
        b.color = meta.hex;
        b.image = meta.image || null;
        b.colorIdx = meta._idx;
        changed = true;
      });
      if (!changed) return;
      refreshAll();
      pushHistory();
    }

    function deleteSelected() {
      if (!selectedIds.size) return;
      balloons = balloons.filter(b => !selectedIds.has(b.id));
      selectedIds.clear();
      updateSelectButtons();
      refreshAll({ autoFit: true });
      pushHistory();
    }

    function duplicateSelected() {
      const sel = selectionBalloons();
      if (!sel.length) return;
      const copies = sel.map(b => ({ ...b, x: b.x + 10, y: b.y + 10, id: makeId() }));
      copies.forEach(c => balloons.push(c));
      selectedIds = new Set(copies.map(c => c.id));
      refreshAll({ autoFit: true });
      updateSelectButtons();
      pushHistory();
    }

    function eraseAt(x, y) {
      const before = balloons.length;
      balloons = balloons.filter(b => Math.hypot(x - b.x, y - b.y) > eraserRadius);
      const removed = balloons.length !== before;
      if (selectedIds.size) {
        const set = new Set(balloons.map(b => b.id));
        let changed = false;
        selectedIds.forEach(id => { if (!set.has(id)) { selectedIds.delete(id); changed = true; } });
        if (changed) updateSelectButtons();
      }
      if (removed && !erasingActive) requestDraw();
      return removed;
    }

    function pickColorAt(x, y) {
      const i = findBalloonIndexAt(x, y);
      if (i !== -1) {
        selectedColorIdx = HEX_TO_FIRST_IDX.get(normalizeHex(balloons[i].color)) ?? 0;
        renderAllowedPalette();
        renderUsedPalette();
        updateCurrentColorChip();
      }
    }

    function promptForFilename(suggested) {
      const m = suggested.match(/\.([a-z0-9]+)$/i);
      const ext = m ? m[1].toLowerCase() : '';
      const defaultBase = suggested.replace(/\.[^.]+$/, '');
      const lsKey = ext ? `lastFilenameBase.${ext}` : `lastFilenameBase`;
      const last = localStorage.getItem(lsKey) || defaultBase;

      const input = window.prompt(ext ? `File name (.${ext} will be added)` : 'File name', last);
      if (input === null) return null;

      let base = (input.trim() || defaultBase)
        .replace(/[<>:"/\\|?*\x00-\x1F]/g, '')
        .replace(/\.+$/, '')
        .replace(/\.[^.]+$/, '');
      try { localStorage.setItem(lsKey, base); } catch {}

      return ext ? `${base}.${ext}` : base;
    }

    function download(href, suggestedFilename) {
      const finalName = promptForFilename(suggestedFilename);
      if (!finalName) return;
      sharedDownload?.(href, finalName);
    }

    function saveJson() {
      download('data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify({ balloons })), 'balloon_design.json');
    }

    function loadJson(e) {
      const file = e.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = ev => {
        try {
          const data = JSON.parse(ev.target.result);
          const loaded = Array.isArray(data.balloons)
            ? data.balloons.map(b => {
                const idx = b.colorIdx ?? (HEX_TO_FIRST_IDX.get(normalizeHex(b.color)) ?? 0);
                const meta = FLAT_COLORS[idx] || {};
                return {
                  x: b.x, y: b.y, radius: b.radius,
                  color: meta.hex || b.color,
                  image: meta.image || null,
                  colorIdx: idx,
                  id: makeId()
                };
              })
            : [];
          balloons = loaded.slice(0, MAX_BALLOONS);
          selectedIds.clear();
          updateSelectButtons();
          refreshAll({ refit: true });
          resetHistory();
          persist();
          if (loaded.length > MAX_BALLOONS) {
            showModal(`Design loaded (trimmed to ${MAX_BALLOONS} balloons).`);
          }
        } catch {
          showModal('Error parsing JSON file.');
        }
      };
      reader.readAsText(file);
    }

    // ====== Export helpers ======
    let lastActiveTab = '#tab-organic';

    const getImageHref = getImageHrefShared;
    function setImageHref(el, val) {
      el.setAttribute('href', val);
      el.setAttributeNS(XLINK_NS, 'xlink:href', val);
    }

    async function embedImagesInSvg(svgEl) {
      const images = Array.from(svgEl.querySelectorAll('image'));
      const hrefs = [...new Set(images.map(getImageHref).filter(h => h && !h.startsWith('data:')))];
      const urlMap = new Map();
      await Promise.all(hrefs.map(async (href) => {
        urlMap.set(href, await imageUrlToDataUrl(href));
      }));
      images.forEach(img => {
        const orig = getImageHref(img);
        const val = urlMap.get(orig);
        if (val) setImageHref(img, val);
      });
      return svgEl;
    }

    async function buildOrganicSvgPayload() {
      if (balloons.length === 0) throw new Error('Canvas is empty. Add some balloons first.');

      const uniqueImageUrls = [...new Set(balloons.map(b => b.image).filter(Boolean))];
      const dataUrlMap = new Map();
      await Promise.all(uniqueImageUrls.map(async (url) => dataUrlMap.set(url, await imageUrlToDataUrl(url))));

      const bounds = balloonsBounds();
      const pad = 120; // extra room to avoid clipping drop-shadows and outlines
      const width = bounds.w + pad * 2;
      const height = bounds.h + pad * 2;
      const vb = [bounds.minX - pad, bounds.minY - pad, width, height].join(' ');

      let defs = '';
      let elements = '';
      const patterns = new Map();
      const shadowFilters = new Map();

      const ensureShadowFilter = (dx, dy, blurPx, alpha) => {
        const key = `${dx}|${dy}|${blurPx}|${alpha}`;
        if (!shadowFilters.has(key)) {
          const id = `shadow-${shadowFilters.size}`;
          const stdDev = Math.max(0.01, blurPx * 0.5);
          const clampedAlpha = clamp01(alpha);
          const flood = `<feFlood flood-color="#000000" flood-opacity="${clampedAlpha}" />`;
          const blur = `<feGaussianBlur in="SourceAlpha" stdDeviation="${stdDev}" result="blur" />`;
          const offset = `<feOffset dx="${dx}" dy="${dy}" in="blur" result="shadow" />`;
          const composite = `<feComposite in="shadow" in2="SourceAlpha" operator="in" result="shadow" />`;
          const merge = `<feMerge><feMergeNode in="shadow" /><feMergeNode in="SourceGraphic" /></feMerge>`;
          defs += `<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">${flood}${blur}${offset}${composite}${merge}</filter>`;
          shadowFilters.set(key, id);
        }
        return shadowFilters.get(key);
      };
      const shineShadowId = ensureShadowFilter(0, 0, 3, 0.1);

      balloons.forEach(b => {
        const meta = FLAT_COLORS[b.colorIdx] || {};
        let fill = b.color;
        if (b.image) {
          const patternKey = `${b.colorIdx}|${b.image}`;
          const imageHref = dataUrlMap.get(b.image);
          if (!patterns.has(patternKey) && imageHref) {
            const patternId = `p${patterns.size}`;
            patterns.set(patternKey, patternId);
            const meta = FLAT_COLORS[b.colorIdx] || {};
            const zoom = Math.max(1, meta.imageZoom ?? TEXTURE_ZOOM_DEFAULT);
            const fx = clamp01(meta.imageFocus?.x ?? TEXTURE_FOCUS_DEFAULT.x);
            const fy = clamp01(meta.imageFocus?.y ?? TEXTURE_FOCUS_DEFAULT.y);
            const imgW = zoom, imgH = zoom;
            const imgX = 0.5 - (fx * zoom);
            const imgY = 0.5 - (fy * zoom);
            defs += `<pattern id="${patternId}" patternContentUnits="objectBoundingBox" width="1" height="1">
              <image href="${imageHref}" x="${imgX}" y="${imgY}" width="${imgW}" height="${imgH}" preserveAspectRatio="xMidYMid slice" />
            </pattern>`;
          }
          if (patterns.has(patternKey)) fill = `url(#${patterns.get(patternKey)})`;
        }
        let filterAttr = '';
        if (b.image) {
          const lum = luminance(meta.hex || b.color);
          if (lum > 0.6) {
            const strength = clamp01((lum - 0.6) / 0.4);
            const blur = 4 + 4 * strength;
            const offsetY = 1 + 2 * strength;
            const alpha = 0.05 + 0.07 * strength;
            const id = ensureShadowFilter(0, offsetY, blur, alpha);
            filterAttr = ` filter="url(#${id})"`;
          }
        } else {
          const id = ensureShadowFilter(0, 0, 10, 0.2);
          filterAttr = ` filter="url(#${id})"`;
        }
        const strokeAttr = isBorderEnabled ? ` stroke="#111827" stroke-width="0.5"` : ` stroke="none" stroke-width="0"`;
        elements += `<circle cx="${b.x}" cy="${b.y}" r="${b.radius}" fill="${fill}"${strokeAttr}${filterAttr} />`;

        if (isShineEnabled) {
          const sx = b.x - b.radius * SHINE_OFFSET;
          const sy = b.y - b.radius * SHINE_OFFSET;
          const rx = b.radius * SHINE_RX;
          const ry = b.radius * SHINE_RY;
          const { fill: shineFill, stroke: shineStroke } = shineStyle(b.color);
          const stroke = shineStroke ? ` stroke="${shineStroke}" stroke-width="1"` : '';
          const shineFilter = shineShadowId ? ` filter="url(#${shineShadowId})"` : '';
          elements += `<ellipse cx="${sx}" cy="${sy}" rx="${rx}" ry="${ry}" fill="${shineFill}"${stroke}${shineFilter} transform="rotate(${SHINE_ROT} ${sx} ${sy})" />`;
        }
      });

      const svgString = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="${XLINK_NS}" viewBox="${vb}" width="${width}" height="${height}">
        <defs>${defs}</defs>
        ${elements}
      </svg>`;

      return { svgString, width, height };
    }

    function designToCompact(list) {
      return { v: 2, b: list.map(b => [ Math.round(b.x), Math.round(b.y), radiusToSizeIndex(b.radius), b.colorIdx ?? 0 ]) };
    }

    function compactToDesign(obj) {
      if (!obj || !Array.isArray(obj.b)) return [];
      return obj.b.map(row => {
        const [x, y, sizeIdx, colorIdx] = row;
        const diam = SIZE_PRESETS[sizeIdx] ?? SIZE_PRESETS[0];
        const radius = inchesToRadiusPx(diam);
        const meta = FLAT_COLORS[colorIdx] || FLAT_COLORS[0];
        return { x, y, radius, color: meta.hex, image: meta.image || null, colorIdx: meta._idx, id: makeId() };
      });
    }

    function generateShareLink() {
      const base = `${window.location.origin}${window.location.pathname}`;
      const link = `${base}?${QUERY_KEY}=${LZString.compressToEncodedURIComponent(JSON.stringify(designToCompact(balloons)))}`;
      if (shareLinkOutput) shareLinkOutput.value = link;
      navigator.clipboard?.writeText(link).then(showCopyMessage);
    }

    function loadFromUrl() {
      const params = new URLSearchParams(window.location.search);
      const encoded = params.get(QUERY_KEY) || params.get('design');
      if (!encoded) return;
      try {
        let jsonStr = LZString.decompressFromEncodedURIComponent(encoded) || atob(encoded);
        const data = JSON.parse(jsonStr);
        const loaded = Array.isArray(data.balloons)
          ? data.balloons.map(b => {
              const idx = b.colorIdx ?? (HEX_TO_FIRST_IDX.get(normalizeHex(b.color)) ?? 0);
              const meta = FLAT_COLORS[idx] || {};
              return { x: b.x, y: b.y, radius: b.radius, color: meta.hex, image: meta.image, colorIdx: idx, id: makeId() };
            })
          : compactToDesign(data);
        balloons = loaded.slice(0, MAX_BALLOONS);
        refreshAll({ refit: true });
        resetHistory();
        persist();
        updateCurrentColorChip();
        if (loaded.length > MAX_BALLOONS) {
          showModal(`Design loaded (trimmed to ${MAX_BALLOONS} balloons).`);
        } else {
          showModal('Design loaded from link!');
        }
      } catch {
        showModal('Could not load design from URL.');
      }
    }

    // ====== Fit/Camera helpers ======
    function balloonsBounds() {
      if (balloons.length === 0) return { minX: 0, minY: 0, maxX: 500, maxY: 500, w: 500, h: 500 };
      let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
      for (const b of balloons) {
        minX = Math.min(minX, b.x - b.radius);
        minY = Math.min(minY, b.y - b.radius);
        maxX = Math.max(maxX, b.x + b.radius);
        maxY = Math.max(maxY, b.y + b.radius);
      }
      return { minX, minY, maxX, maxY, w: maxX - minX, h: maxY - minY };
    }

    function fitView() {
      const box = balloonsBounds();
      const cw = canvas.width  / dpr; // CSS px
      const ch = canvas.height / dpr;
      if (balloons.length === 0) { view.s = 1; clampViewScale(); view.tx = 0; view.ty = 0; return; }

      const pad = FIT_PADDING_PX;
      const w = Math.max(1, box.w);
      const h = Math.max(1, box.h);

      const sFit = Math.min((cw - 2*pad) / w, (ch - 2*pad) / h);
      view.s = Math.min(VIEW_MAX_SCALE, isFinite(sFit) && sFit > 0 ? sFit : 1);
      clampViewScale();

      const worldW = cw / view.s;
      const worldH = ch / view.s;
      view.tx = (worldW - w) * 0.5 - box.minX;
      view.ty = (worldH - h) * 0.5 - box.minY;
    }

    function balloonScreenBounds(b) {
      const left   = (b.x - b.radius + view.tx) * view.s;
      const right  = (b.x + b.radius + view.tx) * view.s;
      const top    = (b.y - b.radius + view.ty) * view.s;
      const bottom = (b.y + b.radius + view.ty) * view.s;
      return { left, right, top, bottom };
    }

    function ensureVisibleAfterAdd(b) {
      const pad = FIT_PADDING_PX;
      const cw = canvas.width  / dpr;
      const ch = canvas.height / dpr;

      // zoom out only if needed to keep the new balloon visible
      const needSx = (cw - 2*pad) / (2*b.radius);
      const needSy = (ch - 2*pad) / (2*b.radius);
      const sNeeded = Math.min(needSx, needSy);
      if (isFinite(sNeeded) && sNeeded > 0 && sNeeded < view.s) {
        view.s = Math.max(VIEW_MIN_SCALE, sNeeded);
      }
      clampViewScale();

      const r = balloonScreenBounds(b);
      let dx = 0, dy = 0;
      if (r.left < pad)           dx = (pad - r.left) / view.s;
      else if (r.right > cw-pad)  dx = ((cw - pad) - r.right) / view.s;

      if (r.top < pad)            dy = (pad - r.top) / view.s;
      else if (r.bottom > ch-pad) dy = ((ch - pad) - r.bottom) / view.s;

      view.tx += dx;
      view.ty += dy;
      return (dx !== 0 || dy !== 0);
    }

    // ====== Refresh & Events ======
    function refreshAll({ refit = false, autoFit = false } = {}) {
      if (refit) fitView();
      else if (autoFit) fitView();
      draw();
      renderUsedPalette();
      persist();
      if(window.updateExportButtonVisibility) window.updateExportButtonVisibility();
    }

    // --- UI bindings ---
    modalCloseBtn?.addEventListener('click', hideModal);

    toolDrawBtn?.addEventListener('click', () => setMode('draw'));
    toolGarlandBtn?.addEventListener('click', () => setMode('garland'));
    toolEraseBtn?.addEventListener('click', () => setMode('erase'));
    toolSelectBtn?.addEventListener('click', () => setMode('select'));

    eraserSizeInput?.addEventListener('input', e => {
      eraserRadius = parseInt(e.target.value, 10);
      if (eraserSizeLabel) eraserSizeLabel.textContent = eraserRadius;
      if (mode === 'erase') draw();
      persist();
    });
    toggleBorderCheckbox?.addEventListener('change', e => {
      isBorderEnabled = !!e.target.checked;
      draw();
      persist();
    });
    garlandDensityInput?.addEventListener('input', e => {
      garlandDensity = clamp(parseFloat(e.target.value) || 1, 0.6, 1.6);
      if (garlandDensityLabel) garlandDensityLabel.textContent = garlandDensity.toFixed(1);
      if (mode === 'garland') requestDraw();
      persist();
    });
    const refreshGarlandColors = () => {
      renderGarlandMainChips();
      updateAccentChip();
      if (mode === 'garland') requestDraw();
      persist();
    };
    refreshGarlandColors();

    deleteSelectedBtn?.addEventListener('click', deleteSelected);
    duplicateSelectedBtn?.addEventListener('click', duplicateSelected);
    nudgeSelectedBtns.forEach(btn => btn.addEventListener('click', () => {
      const dx = Number(btn.dataset.dx || 0);
      const dy = Number(btn.dataset.dy || 0);
      moveSelected(dx, dy);
    }));

    selectedSizeInput?.addEventListener('input', e => {
      resizeSelected(parseFloat(e.target.value) || 0);
    });
    selectedSizeInput?.addEventListener('pointerdown', () => {
      resizeChanged = false;
      clearTimeout(resizeSaveTimer);
    });
    selectedSizeInput?.addEventListener('pointerup', () => {
      clearTimeout(resizeSaveTimer);
      if (resizeChanged) {
        pushHistory();
        resizeChanged = false;
      }
    });

    bringForwardBtn?.addEventListener('click', bringSelectedForward);
    sendBackwardBtn?.addEventListener('click', sendSelectedBackward);
    applyColorBtn?.addEventListener('click', applyColorToSelected);
    fitViewBtn?.addEventListener('click', () => refreshAll({ refit: true }));

    document.addEventListener('keydown', e => {
      if (document.activeElement && document.activeElement.tagName === 'INPUT') return;
      if (e.key === 'e' || e.key === 'E') setMode('erase');
      else if (e.key === 'v' || e.key === 'V') setMode('draw');
      else if (e.key === 's' || e.key === 'S') setMode('select');
      else if (e.key === 'g' || e.key === 'G') setMode('garland');
      else if (e.key === 'Escape') {
        if (selectedIds.size) {
            clearSelection();
        } else if (mode !== 'draw') {
            setMode('draw');
        }
      } else if (e.key === 'Delete' || e.key === 'Backspace') {
        if (selectedIds.size) { e.preventDefault(); deleteSelected(); }
      } else if ((e.ctrlKey || e.metaKey) && (e.key === 'z' || e.key === 'Z')) {
          e.preventDefault();
          undo();
      } else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || e.key === 'Y')) {
          e.preventDefault();
          redo();
      } else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'd') {
          e.preventDefault();
          duplicateSelected();
      }
    });

    async function confirmAndClear() {
      let ok = true;
      if (window.Swal) {
        const res = await Swal.fire({
          title: 'Start fresh?',
          text: 'This will remove all balloons from the canvas.',
          icon: 'warning',
          showCancelButton: true,
          confirmButtonText: 'Yes, clear',
          cancelButtonText: 'Cancel'
        });
        ok = res.isConfirmed;
      } else {
        ok = window.confirm('Start fresh? This will remove all balloons from the canvas.');
      }
      if (!ok) return;
      balloons = [];
      selectedIds.clear();
      garlandPath = [];
      updateSelectButtons();
      refreshAll({ refit: true });
      pushHistory();
    }

    clearCanvasBtn?.addEventListener('click', confirmAndClear);
    clearCanvasBtnTop?.addEventListener('click', confirmAndClear);

    saveJsonBtn?.addEventListener('click', saveJson);
    loadJsonInput?.addEventListener('change', loadJson);

    generateLinkBtn?.addEventListener('click', generateShareLink);

    sortUsedToggle?.addEventListener('click', () => {
      usedSortDesc = !usedSortDesc;
      sortUsedToggle.textContent = usedSortDesc ? 'Sort: Most → Least' : 'Sort: Least → Most';
      renderUsedPalette();
      persist();
    });

    function populateReplaceTo() {
      if (!replaceToSel) return;
      replaceToSel.innerHTML = '';
      (window.PALETTE || []).forEach(group => {
        const og = document.createElement('optgroup');
        og.label = group.family;
        (group.colors || []).forEach(c => {
          const idx =
            FLAT_COLORS.find(fc => fc.name === c.name && fc.hex === c.hex && fc.family === group.family)?._idx
            ?? HEX_TO_FIRST_IDX.get(normalizeHex(c.hex)) ?? 0;
          const opt = document.createElement('option');
          opt.value = String(idx);
          opt.textContent = c.name + (c.image ? ' (image)' : '');
          og.appendChild(opt);
        });
        replaceToSel.appendChild(og);
      });
      updateReplaceChips();
    }

    function populateGarlandColorSelects() {
      const addOpts = sel => {
        if (!sel) return;
        sel.innerHTML = '';
        const noneOpt = document.createElement('option');
        noneOpt.value = '-1';
        noneOpt.textContent = 'None (use active color)';
        sel.appendChild(noneOpt);
        FLAT_COLORS.forEach((c, idx) => {
          const opt = document.createElement('option');
          opt.value = String(idx);
          opt.textContent = c.name || c.hex;
          sel.appendChild(opt);
        });
      };
      addOpts(garlandColorMain1Sel);
      addOpts(garlandColorMain2Sel);
      addOpts(garlandColorMain3Sel);
      addOpts(garlandColorMain4Sel);
      addOpts(garlandColorAccentSel);
      if (garlandColorMain1Sel) garlandColorMain1Sel.value = String(garlandMainIdx[0] ?? -1);
      if (garlandColorMain2Sel) garlandColorMain2Sel.value = String(garlandMainIdx[1] ?? -1);
      if (garlandColorMain3Sel) garlandColorMain3Sel.value = String(garlandMainIdx[2] ?? -1);
      if (garlandColorMain4Sel) garlandColorMain4Sel.value = String(garlandMainIdx[3] ?? -1);
      if (garlandColorAccentSel) garlandColorAccentSel.value = String(garlandAccentIdx ?? -1);

      updateGarlandSwatches();
    }

    const updateReplaceChips = () => {
      const fromHex = replaceFromSel?.value;
      const toIdx = parseInt(replaceToSel?.value || '-1', 10);
      const setChip = (chip, hex, meta = null) => {
        if (!chip) return;
        if (meta?.image) {
          chip.style.backgroundImage = `url("${meta.image}")`;
          chip.style.backgroundColor = meta.hex || '#fff';
          chip.style.backgroundSize = 'cover';
        } else {
          chip.style.backgroundImage = 'none';
          chip.style.backgroundColor = hex || '#f1f5f9';
        }
      };
      const toMeta = Number.isInteger(toIdx) && toIdx >= 0 ? FLAT_COLORS[toIdx] : null;
      setChip(replaceFromChip, fromHex || '#f8fafc', null);
      setChip(replaceToChip, toMeta?.hex || '#f8fafc', toMeta);

      // count matches
      const targetHex = normalizeHex(fromHex || '');
      let count = 0;
      if (targetHex) {
        balloons.forEach(b => { if (normalizeHex(b.color) === targetHex) count++; });
      }
      if (replaceCountLabel) replaceCountLabel.textContent = count ? `${count} match${count === 1 ? '' : 'es'}` : '0 matches';
      return count;
    };

    const openReplacePicker = (mode = 'from') => {
      if (!window.openColorPicker) return;
      if (mode === 'from') {
        const used = getUsedColors();
        const items = used.map(u => ({
          label: u.name || NAME_BY_HEX.get(u.hex) || u.hex,
          metaText: `${u.count} in design`,
          hex: u.hex
        }));
        window.openColorPicker({
          title: 'Replace: From color',
          subtitle: 'Pick a color that already exists on canvas',
          items,
          onSelect: (item) => {
            if (!replaceFromSel) return;
            replaceFromSel.value = item.hex;
            updateReplaceChips();
          }
        });
      } else {
        const items = (FLAT_COLORS || []).map((c, idx) => ({
          label: c.name || c.hex,
          metaText: c.family || '',
          idx
        }));
        window.openColorPicker({
          title: 'Replace: To color',
          subtitle: 'Choose any color from the library',
          items,
          onSelect: (item) => {
            if (!replaceToSel) return;
            replaceToSel.value = String(item.idx);
            updateReplaceChips();
          }
        });
      }
    };

    replaceFromChip?.addEventListener('click', () => openReplacePicker('from'));
    replaceToChip?.addEventListener('click', () => openReplacePicker('to'));

    replaceFromSel?.addEventListener('change', updateReplaceChips);
    replaceToSel?.addEventListener('change', updateReplaceChips);

    replaceBtn?.addEventListener('click', () => {
      const fromHex = replaceFromSel?.value;
      const toIdx = parseInt(replaceToSel?.value || '', 10);
      if (!fromHex || Number.isNaN(toIdx)) { if (replaceMsg) replaceMsg.textContent = 'Pick both colors.'; return; }

      let count = 0;
      balloons.forEach(b => {
        if (normalizeHex(b.color) === normalizeHex(fromHex)) {
          const toMeta = FLAT_COLORS[toIdx];
          b.color = toMeta.hex;
          b.image = toMeta.image || null;
          b.colorIdx = toMeta._idx;
          count++;
        }
      });

      if (count > 0) {
        if (count > 80) {
          const ok = window.confirm(`Replace ${count} balloons? This cannot be undone except via Undo.`);
          if (!ok) return;
        }
        pushHistory();
        if (replaceMsg) replaceMsg.textContent = `Replaced ${count} balloon${count === 1 ? '' : 's'}.`;
        if (normalizeHex(FLAT_COLORS[selectedColorIdx]?.hex) === normalizeHex(fromHex)) selectedColorIdx = toIdx;
        refreshAll();
        renderAllowedPalette();
      } else {
        if (replaceMsg) replaceMsg.textContent = 'Nothing to replace.';
      }
      updateReplaceChips();
    });

    // ====== Init ======
    sizePresetGroup && (sizePresetGroup.innerHTML = '');
    SIZE_PRESETS.forEach(di => {
      const btn = document.createElement('button');
      btn.className = 'tool-btn';
      btn.textContent = `${di}"`;
      btn.setAttribute('aria-pressed', String(di === currentDiameterInches));
      btn.addEventListener('click', () => {
        currentDiameterInches = di;
        currentRadius = inchesToRadiusPx(di);
        [...sizePresetGroup.querySelectorAll('button')].forEach(b => b.setAttribute('aria-pressed', 'false'));
        btn.setAttribute('aria-pressed', 'true');
        persist();
      });
      sizePresetGroup?.appendChild(btn);
    });

    toggleShineCheckbox?.addEventListener('change', e => {
      const on = !!e.target.checked;
      window.syncAppShine(on);
    });

    mode = 'draw'; // force default tool on load
    renderAllowedPalette();
    resizeCanvas();
    loadFromUrl();
    renderUsedPalette();
    // Initialize wall designer if available (wall.js sets this)
    window.WallDesigner?.init?.();
    setMode('draw');
    updateSelectButtons();
    populateReplaceTo();
    populateGarlandColorSelects();

    // default to canvas-first on mobile; no expansion toggles remain

    // Initialize shine state from localStorage for both panels
    let initialShineState = true;
    try {
      const saved = localStorage.getItem('app:shineEnabled:v1');
      if (saved !== null) initialShineState = JSON.parse(saved);
    } catch {}

    // Set Organic panel's internal state and UI
    isShineEnabled = initialShineState;
    if (toggleShineCheckbox) toggleShineCheckbox.checked = isShineEnabled;

    // Set Classic panel's UI checkbox (its script will read this too)
    const classicCb = document.getElementById('classic-shine-enabled');
    if (classicCb) classicCb.checked = isShineEnabled;

    // ===============================
    // ===== TAB SWITCHING (UI) ======
    // ===============================
    const orgSection = document.getElementById('tab-organic');
    const claSection = document.getElementById('tab-classic');
    const wallSection = document.getElementById('tab-wall');
    const tabBtns    = document.querySelectorAll('#mode-tabs .tab-btn');
    const ACTIVE_TAB_KEY = 'balloonDesigner:activeTab:v1';

    // Tab/mobile logic lives in script.js
  });
})();
