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

  // -----------------------------
  // Organic app logic
  // -----------------------------
  document.addEventListener('DOMContentLoaded', () => {
    // ====== GLOBAL SCALE ======
    const PX_PER_INCH = 4;
    const SIZE_PRESETS = [24, 18, 11, 9, 5];

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

    // ====== Texture defaults ======
    const TEXTURE_ZOOM_DEFAULT = 1.8;
    const TEXTURE_FOCUS_DEFAULT = { x: 0.5, y: 0.5 };
    const SWATCH_TEXTURE_ZOOM = 2.5;

    const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
    const clamp01 = v => clamp(v, 0, 1);

    const QUERY_KEY = 'd';

    // ====== Flatten palette & maps ======
    const FLAT_COLORS = [];
    const NAME_BY_HEX = new Map();
    const HEX_TO_FIRST_IDX = new Map();
    const allowedSet = new Set();

    (function buildFlat() {
      if (!Array.isArray(window.PALETTE)) return;
      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);
        });
      });
    })();

    // ====== Image cache ======
    const IMG_CACHE = new Map();
    function getImage(path) {
      if (!path) return null;
      let img = IMG_CACHE.get(path);
      if (!img) {
        img = new Image();
        img.decoding = 'async';
        img.loading = 'eager';
        img.src = path;
        img.onload = () => draw();
        IMG_CACHE.set(path, img);
      }
      return img;
    }

    // ====== 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');

    // tool buttons
    const toolDrawBtn = document.getElementById('tool-draw');
    const toolEraseBtn = document.getElementById('tool-erase');
    const toolSelectBtn = document.getElementById('tool-select');

    // 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 sizePresetGroup = document.getElementById('size-preset-group');
    const toggleShineBtn = null;
    const toggleShineCheckbox = document.getElementById('toggle-shine-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');

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

    // delegate export buttons (now by data-export to allow multiple)
    document.body.addEventListener('click', e => {
      const btn = e.target.closest('[data-export]');
      if (!btn) return;
      const type = btn.dataset.export;
      if (type === 'png') exportPng();
      else if (type === 'svg') exportSvg();
    });

    const generateLinkBtn = document.getElementById('generate-link-btn');
    const shareLinkOutput = document.getElementById('share-link-output');
    const copyMessage = document.getElementById('copy-message');

    // 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 dpr = 1;
    let mode = 'draw';
    let eraserRadius = parseInt(eraserSizeInput?.value || '40', 10);
    let mouseInside = false;
    let mousePos = { x: 0, y: 0 };
    let selectedBalloonId = null;
    let usedSortDesc = true;

    // History for Undo/Redo
    const historyStack = [];
    let historyPointer = -1;

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

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

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

    // Bind Undo/Redo Buttons
    document.getElementById('tool-undo')?.addEventListener('click', undo);
    document.getElementById('tool-redo')?.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');
        }
    });


    // ====== Helpers ======
    const normalizeHex = h => (h || '').toLowerCase();
    function hexToRgb(hex) {
      const h = normalizeHex(hex).replace('#','');
      if (h.length === 3) {
        const r = parseInt(h[0] + h[0], 16);
        const g = parseInt(h[1] + h[1], 16);
        const b = parseInt(h[2] + h[2], 16);
        return { r, g, b };
      }
      if (h.length === 6) {
        const r = parseInt(h.slice(0,2), 16);
        const g = parseInt(h.slice(2,4), 16);
        const b = parseInt(h.slice(4,6), 16);
        return { r, g, b };
      }
      return { r: 0, g: 0, b: 0 };
    }
    function luminance(hex) {
      const { r, g, b } = hexToRgb(hex || '#000');
      const norm = [r,g,b].map(v => {
        const c = v / 255;
        return c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4);
      });
      return 0.2126*norm[0] + 0.7152*norm[1] + 0.0722*norm[2];
    }
    function inchesToRadiusPx(diam) { return (diam * PX_PER_INCH) / 2; }
    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) { if (!messageModal) return; modalText.textContent = msg; messageModal.classList.remove('hidden'); }
    function hideModal() { 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) {
      mode = next;
      toolDrawBtn?.setAttribute('aria-pressed', String(mode === 'draw'));
      toolEraseBtn?.setAttribute('aria-pressed', String(mode === 'erase'));
      toolSelectBtn?.setAttribute('aria-pressed', String(mode === 'select'));
      toolEyedropperBtn?.setAttribute('aria-pressed', String(mode === 'eyedropper'));
      
      eraserControls?.classList.toggle('hidden', mode !== 'erase');
      selectControls?.classList.toggle('hidden', mode !== 'select');
      
      if (mode === 'erase') canvas.style.cursor = 'none';
      else if (mode === 'select') canvas.style.cursor = 'default'; // will be move over items
      else if (mode === 'eyedropper') canvas.style.cursor = 'cell';
      else canvas.style.cursor = 'crosshair';
      
      draw();
      persist();
    }

    function updateSelectButtons() {
      const has = !!selectedBalloonId;
      if (deleteSelectedBtn) deleteSelectedBtn.disabled = !has;
      if (duplicateSelectedBtn) duplicateSelectedBtn.disabled = !has;
      if (selectedSizeInput) selectedSizeInput.disabled = !has;
      if (bringForwardBtn) bringForwardBtn.disabled = !has;
      if (sendBackwardBtn) sendBackwardBtn.disabled = !has;
      if (applyColorBtn) applyColorBtn.disabled = !has;
      if (has && selectedSizeInput && selectedSizeLabel) {
        const b = balloons.find(bb => bb.id === selectedBalloonId);
        if (b) {
          selectedSizeInput.value = Math.round(b.radius);
          selectedSizeLabel.textContent = `${Math.round(b.radius)}`;
        }
      }
    }

    // ====== Pointer Events ======
    let pointerDown = false;
    let isDragging = false;
    let dragStartPos = { x: 0, y: 0 };
    let initialBalloonPos = { x: 0, y: 0 };

    canvas.addEventListener('pointerdown', e => {
      e.preventDefault();
      canvas.setPointerCapture?.(e.pointerId);
      mouseInside = true;
      mousePos = getMousePos(e);

      if (e.altKey || mode === 'eyedropper') { 
          pickColorAt(mousePos.x, mousePos.y); 
          if (mode === 'eyedropper') setMode('draw'); // Auto-switch back? or stay? Let's stay for multi-pick, or switch for quick workflow. Let's switch back for now.
          return; 
      }

      if (mode === 'erase') { 
          pointerDown = true; 
          pushHistory(); // Save state before erasing
          eraseAt(mousePos.x, mousePos.y); 
          return; 
      }

      if (mode === 'select') {
        const clickedIdx = findBalloonIndexAt(mousePos.x, mousePos.y);
        
        if (clickedIdx !== -1) {
            // We clicked on a balloon
            const b = balloons[clickedIdx];
            if (selectedBalloonId !== b.id) {
                selectedBalloonId = b.id;
                updateSelectButtons();
                draw();
            }
            // Start Dragging
            isDragging = true;
            pointerDown = true;
            dragStartPos = { ...mousePos };
            initialBalloonPos = { x: b.x, y: b.y };
            pushHistory(); // Save state before move
        } else {
            // Clicked empty space -> deselect
            if (selectedBalloonId) {
                selectedBalloonId = null;
                updateSelectButtons();
                draw();
            }
            // Perhaps handle panning here later?
        }
        return;
      }

      // draw mode: add
      pushHistory(); // Save state before add
      addBalloon(mousePos.x, mousePos.y);
      pointerDown = true; // track for potential continuous drawing or other gestures?
    }, { passive: false });

    canvas.addEventListener('pointermove', e => {
      mousePos = getMousePos(e);
      
      if (mode === 'select') {
          if (isDragging && selectedBalloonId) {
              const dx = mousePos.x - dragStartPos.x;
              const dy = mousePos.y - dragStartPos.y;
              const b = balloons.find(bb => bb.id === selectedBalloonId);
              if (b) {
                  b.x = initialBalloonPos.x + dx;
                  b.y = initialBalloonPos.y + dy;
                  draw();
              }
          } else {
              // Hover cursor
              const hoverIdx = findBalloonIndexAt(mousePos.x, mousePos.y);
              canvas.style.cursor = (hoverIdx !== -1) ? 'move' : 'default';
          }
      }
      
      if (mode === 'erase') {
        if (pointerDown) eraseAt(mousePos.x, mousePos.y);
        else draw();
      }
    }, { passive: true });

    canvas.addEventListener('pointerup', e => {
      pointerDown = false;
      isDragging = false;
      canvas.releasePointerCapture?.(e.pointerId);
    }, { passive: true });

    canvas.addEventListener('pointerleave', () => {
      mouseInside = false;
      if (mode === 'erase') draw();
    }, { passive: true });

    // ====== 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();
            ctx.drawImage(img, srcX, srcY, srcW, srcH, b.x - b.radius, b.y - b.radius, b.radius * 2, b.radius * 2);
            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();
            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();
          ctx.shadowBlur = 0;
        }

        if (isShineEnabled) {
          const isBright = luminance(b.color) > 0.75;
          const shineFill = isBright ? 'rgba(0,0,0,0.55)' : `rgba(255,255,255,${SHINE_ALPHA})`;
          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 (isBright) {
            ctx.strokeStyle = 'rgba(0,0,0,0.45)';
            ctx.lineWidth = 1.5;
            ctx.stroke();
          }
          ctx.fill();
          ctx.restore();
        }
      });

      // selection ring
      if (selectedBalloonId) {
        const b = balloons.find(bb => bb.id === selectedBalloonId);
        if (b) {
          ctx.save();
          ctx.beginPath();
          ctx.arc(b.x, b.y, b.radius + 3, 0, Math.PI * 2);
          // White halo
          ctx.lineWidth = 4 / view.s;
          ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
          ctx.stroke();
          // Blue ring
          ctx.lineWidth = 2 / view.s;
          ctx.strokeStyle = '#3b82f6';
          ctx.stroke();
          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();
    }

    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 };
      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;
        if (typeof s.usedSortDesc === 'boolean') {
          usedSortDesc = s.usedSortDesc;
          if (sortUsedToggle) sortUsedToggle.textContent = usedSortDesc ? 'Sort: Most → Least' : 'Sort: Least → Most';
        }
      } catch {}
    }

    loadAppState();

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

    // ====== Balloon Ops & Data/Export ======
    function addBalloon(x, y) {
      const meta = FLAT_COLORS[selectedColorIdx] || FLAT_COLORS[0];
      balloons.push({
        x, y,
        radius: currentRadius,
        color: meta.hex,
        image: meta.image || null,
        colorIdx: meta._idx,
        id: crypto.randomUUID()
      });
      ensureVisibleAfterAdd(balloons[balloons.length - 1]);
      refreshAll();
    }

    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);
      selectedBalloonId = (i !== -1) ? balloons[i].id : null;
      updateSelectButtons();
      draw();
    }

    function moveSelected(dx, dy) {
      if (!selectedBalloonId) return;
      const b = balloons.find(bb => bb.id === selectedBalloonId);
      if (!b) return;
      b.x += dx;
      b.y += dy;
      refreshAll();
    }

    function resizeSelected(newRadius) {
      if (!selectedBalloonId) return;
      const b = balloons.find(bb => bb.id === selectedBalloonId);
      if (!b) return;
      b.radius = clamp(newRadius, 5, 200);
      refreshAll();
      updateSelectButtons();
    }

    function bringSelectedForward() {
      if (!selectedBalloonId) return;
      const idx = balloons.findIndex(bb => bb.id === selectedBalloonId);
      if (idx === -1 || idx === balloons.length - 1) return;
      const [b] = balloons.splice(idx, 1);
      balloons.push(b);
      refreshAll();
    }

    function sendSelectedBackward() {
      if (!selectedBalloonId) return;
      const idx = balloons.findIndex(bb => bb.id === selectedBalloonId);
      if (idx <= 0) return;
      const [b] = balloons.splice(idx, 1);
      balloons.unshift(b);
      refreshAll();
    }

    function applyColorToSelected() {
      if (!selectedBalloonId) return;
      const b = balloons.find(bb => bb.id === selectedBalloonId);
      const meta = FLAT_COLORS[selectedColorIdx] || FLAT_COLORS[0];
      if (!b || !meta) return;
      b.color = meta.hex;
      b.image = meta.image || null;
      b.colorIdx = meta._idx;
      refreshAll();
    }

    function deleteSelected() {
      if (!selectedBalloonId) return;
      balloons = balloons.filter(b => b.id !== selectedBalloonId);
      selectedBalloonId = null;
      updateSelectButtons();
      refreshAll();
    }

    function duplicateSelected() {
      if (!selectedBalloonId) return;
      const b = balloons.find(bb => bb.id === selectedBalloonId);
      if (!b) return;
      const copy = { ...b, x: b.x + 10, y: b.y + 10, id: crypto.randomUUID() };
      balloons.push(copy);
      selectedBalloonId = copy.id;
      refreshAll();
    }

    function eraseAt(x, y) {
      balloons = balloons.filter(b => Math.hypot(x - b.x, y - b.y) > eraserRadius);
      if (selectedBalloonId && !balloons.find(b => b.id === selectedBalloonId)) {
        selectedBalloonId = null;
        updateSelectButtons();
      }
      refreshAll();
    }

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

    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;
      const a = document.createElement('a');
      a.href = href;
      a.download = finalName;
      a.click();
      a.remove();
    }

    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);
          balloons = 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: crypto.randomUUID()
                };
              })
            : [];
          selectedBalloonId = null;
          updateSelectButtons();
          refreshAll({ refit: true });
          persist();
        } catch {
          showModal('Error parsing JSON file.');
        }
      };
      reader.readAsText(file);
    }

    // ====== Export helpers ======
    const DATA_URL_CACHE = new Map();
    const XLINK_NS = 'http://www.w3.org/1999/xlink';
    let lastActiveTab = '#tab-organic';

    function getImageHref(el) {
      return el.getAttribute('href') || el.getAttributeNS(XLINK_NS, 'href');
    }
    function setImageHref(el, val) {
      el.setAttribute('href', val);
      el.setAttributeNS(XLINK_NS, 'xlink:href', val);
    }
    const blobToDataUrl = blob => new Promise((resolve, reject) => {
      const r = new FileReader();
      r.onloadend = () => resolve(r.result);
      r.onerror = reject;
      r.readAsDataURL(blob);
    });

    function imageToDataUrl(img) {
      if (!img || !img.complete || img.naturalWidth === 0) return null;
      try {
        const c = document.createElement('canvas');
        c.width = img.naturalWidth;
        c.height = img.naturalHeight;
        c.getContext('2d').drawImage(img, 0, 0);
        return c.toDataURL('image/png');
      } catch (err) {
        console.warn('[Export] imageToDataUrl failed:', err);
        return null;
      }
    }

    async function imageUrlToDataUrl(src) {
      if (!src || src.startsWith('data:')) return src;
      if (DATA_URL_CACHE.has(src)) return DATA_URL_CACHE.get(src);
      const cachedImg = IMG_CACHE.get(src);
      const cachedUrl = imageToDataUrl(cachedImg);
      if (cachedUrl) {
        DATA_URL_CACHE.set(src, cachedUrl);
        return cachedUrl;
      }
      const abs = (() => { try { return new URL(src, window.location.href).href; } catch { return src; } })();
      let dataUrl = null;
      try {
        const resp = await fetch(abs);
        if (!resp.ok) throw new Error(`Status ${resp.status}`);
        dataUrl = await blobToDataUrl(await resp.blob());
      } catch (err) {
        console.warn('[Export] Fetch failed for', abs, err);
        // Fallback: draw to a canvas to capture even when fetch is blocked (e.g., file://)
        dataUrl = await new Promise(resolve => {
          const img = new Image();
          img.crossOrigin = 'anonymous';
          img.onload = () => {
            try {
              const c = document.createElement('canvas');
              c.width = img.naturalWidth || 1;
              c.height = img.naturalHeight || 1;
              c.getContext('2d').drawImage(img, 0, 0);
              resolve(c.toDataURL('image/png'));
            } catch (e) {
              console.error('[Export] Canvas fallback failed for', abs, e);
              resolve(null);
            }
          };
          img.onerror = () => resolve(null);
          img.src = abs;
        });
      }
      if (!dataUrl) dataUrl = abs;
      DATA_URL_CACHE.set(src, dataUrl);
      return dataUrl;
    }

    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 = 20;
      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();

      balloons.forEach(b => {
        let fill = b.color;
        if (b.image) {
          const patternKey = `${b.colorIdx}|${b.image}`;
          if (!patterns.has(patternKey)) {
            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);
            const imageHref = dataUrlMap.get(b.image) || b.image;
            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>`;
          }
          fill = `url(#${patterns.get(patternKey)})`;
        }
        elements += `<circle cx="${b.x}" cy="${b.y}" r="${b.radius}" fill="${fill}" stroke="#111827" stroke-width="2" />`;

        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 isBright = luminance(b.color) > 0.75;
          const shineFill = isBright ? 'rgba(0,0,0,0.55)' : `rgba(255,255,255,${SHINE_ALPHA})`;
          const stroke = isBright ? ' stroke="rgba(0,0,0,0.45)" stroke-width="1.5"' : '';
          elements += `<ellipse cx="${sx}" cy="${sy}" rx="${rx}" ry="${ry}" fill="${shineFill}"${stroke} 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 };
    }

    async function buildClassicSvgPayload() {
      const svgElement = document.querySelector('#classic-display svg');
      if (!svgElement) throw new Error('Classic design not found. Please create a design first.');
      const clonedSvg = svgElement.cloneNode(true);

      // Inline pattern images and any other <image> nodes
      const allImages = Array.from(clonedSvg.querySelectorAll('image'));
      await Promise.all(allImages.map(async img => {
        const href = getImageHref(img);
        if (!href || href.startsWith('data:')) return;
        const dataUrl = await imageUrlToDataUrl(href);
        if (dataUrl) setImageHref(img, dataUrl);
      }));

      // Ensure required namespaces are present
      const viewBox = (clonedSvg.getAttribute('viewBox') || '0 0 1000 1000').split(/\s+/).map(Number);
      const vbX = isFinite(viewBox[0]) ? viewBox[0] : 0;
      const vbY = isFinite(viewBox[1]) ? viewBox[1] : 0;
      const vbW = isFinite(viewBox[2]) ? viewBox[2] : (svgElement.clientWidth || 1000);
      const vbH = isFinite(viewBox[3]) ? viewBox[3] : (svgElement.clientHeight || 1000);
      clonedSvg.setAttribute('width', vbW);
      clonedSvg.setAttribute('height', vbH);
      if (!clonedSvg.getAttribute('xmlns')) clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
      if (!clonedSvg.getAttribute('xmlns:xlink')) clonedSvg.setAttribute('xmlns:xlink', XLINK_NS);

      // Some viewers ignore external styles; bake key style attributes directly
      clonedSvg.querySelectorAll('g.balloon, path.balloon, ellipse.balloon, circle.balloon').forEach(el => {
        if (!el.getAttribute('stroke')) el.setAttribute('stroke', '#111827');
        if (!el.getAttribute('stroke-width')) el.setAttribute('stroke-width', '2');
        if (!el.getAttribute('paint-order')) el.setAttribute('paint-order', 'stroke fill');
        if (!el.getAttribute('vector-effect')) el.setAttribute('vector-effect', 'non-scaling-stroke');
      });

      const svgString = new XMLSerializer().serializeToString(clonedSvg);
      return { svgString, width: vbW, height: vbH, minX: vbX, minY: vbY };
    }

    async function svgStringToPng(svgString, width, height) {
      const img = new Image();
      const scale = 2;
      const canvasEl = document.createElement('canvas');
      canvasEl.width = Math.max(1, Math.round(width * scale));
      canvasEl.height = Math.max(1, Math.round(height * scale));
      const ctx2 = canvasEl.getContext('2d');
      const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
      await new Promise((resolve, reject) => {
        img.onload = resolve;
        img.onerror = () => reject(new Error('Could not rasterize SVG.'));
        img.src = dataUrl;
      });
      ctx2.drawImage(img, 0, 0, canvasEl.width, canvasEl.height);
      return canvasEl.toDataURL('image/png');
    }

    function detectCurrentTab() {
      const bodyActive = document.body?.dataset?.activeTab;
      const activeBtn = document.querySelector('#mode-tabs .tab-btn.tab-active');
      const classicVisible = !document.getElementById('tab-classic')?.classList.contains('hidden');
      const organicVisible = !document.getElementById('tab-organic')?.classList.contains('hidden');

      let id = bodyActive || activeBtn?.dataset?.target;
      if (!id) {
        if (classicVisible && !organicVisible) id = '#tab-classic';
        else if (organicVisible && !classicVisible) id = '#tab-organic';
      }
      if (!id) id = lastActiveTab || '#tab-organic';
      lastActiveTab = id;
      if (document.body) document.body.dataset.activeTab = id;
      return id;
    }

    function updateSheets() {
      const tab = detectCurrentTab();
      const hide = !window.matchMedia('(min-width: 1024px)').matches && document.body?.dataset?.controlsHidden === '1';
      if (orgSheet) orgSheet.classList.toggle('hidden', hide || tab !== '#tab-organic');
      if (claSheet) claSheet.classList.toggle('hidden', hide || tab !== '#tab-classic');
    }

    async function exportPng() {
      try {
        const currentTab = detectCurrentTab();

        if (currentTab === '#tab-classic') {
          const { svgString, width, height } = await buildClassicSvgPayload();
          const pngUrl = await svgStringToPng(svgString, width, height);
          download(pngUrl, 'classic_design.png');
          return;
        }

        const { svgString, width, height } = await buildOrganicSvgPayload();
        const pngUrl = await svgStringToPng(svgString, width, height);
        download(pngUrl, 'balloon_design.png');
      } catch (err) {
        console.error('[Export PNG] Failed:', err);
        showModal(err.message || 'Could not export PNG. Check console for details.');
      }
    }

    function downloadSvg(svgString, filename) {
      const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
      const url = URL.createObjectURL(blob);
      download(url, filename);
      setTimeout(() => URL.revokeObjectURL(url), 20000);
    }

    async function exportSvg() {
      try {
        const currentTab = detectCurrentTab();

        if (currentTab === '#tab-classic') {
          const { svgString, width, height } = await buildClassicSvgPayload();
          try {
            const pngUrl = await svgStringToPng(svgString, width, height);
            const cleanSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
              <image href="${pngUrl}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet" />
            </svg>`;
            downloadSvg(cleanSvg, 'classic_design.svg');
            return;
          } catch (pngErr) {
            console.warn('[Export SVG] PNG embed failed, falling back to vector-only SVG', pngErr);
            downloadSvg(svgString, 'classic_design.svg');
            return;
          }
        }

        const { svgString } = await buildOrganicSvgPayload();
        downloadSvg(svgString, 'organic_design.svg');
      } catch (err) {
        console.error('[Export] A critical error occurred during SVG export:', err);
        showModal(err.message || 'An unexpected error occurred during SVG export. Check console for details.');
      }
    }

    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: crypto.randomUUID() };
      });
    }

    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);
        balloons = 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: crypto.randomUUID() };
            })
          : compactToDesign(data);
        refreshAll({ refit: true });
        persist();
        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; 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(1, isFinite(sFit) && sFit > 0 ? sFit : 1);

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

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

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

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

    toolDrawBtn?.addEventListener('click', () => setMode('draw'));
    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();
    });

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

    bringForwardBtn?.addEventListener('click', bringSelectedForward);
    sendBackwardBtn?.addEventListener('click', sendSelectedBackward);
    applyColorBtn?.addEventListener('click', applyColorToSelected);

    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 === 'Escape') {
        if (selectedBalloonId) {
            selectedBalloonId = null;
            updateSelectButtons();
            draw();
        } else if (mode !== 'draw') {
            setMode('draw');
        }
      } else if (e.key === 'Delete' || e.key === 'Backspace') {
        if (selectedBalloonId) { 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();
      }
    });

    clearCanvasBtn?.addEventListener('click', () => {
      balloons = [];
      selectedBalloonId = null;
      updateSelectButtons();
      refreshAll({ refit: true });
    });

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

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

    // ====== 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();
    setMode('draw');
    updateSelectButtons();
    populateReplaceTo();

    // 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 tabBtns    = document.querySelectorAll('#mode-tabs .tab-btn');
    const ACTIVE_TAB_KEY = 'balloonDesigner:activeTab:v1';

    function updateMobileStacks(tabName) {
      const orgPanel = document.getElementById('controls-panel');
      const claPanel = document.getElementById('classic-controls-panel');
      const currentTab = detectCurrentTab();
      const panel = currentTab === '#tab-classic' ? claPanel : orgPanel;
      const target = tabName || document.body?.dataset?.mobileTab || 'controls';
      const isHidden = document.body?.dataset?.controlsHidden === '1';
      if (!panel) return;
      const stacks = Array.from(panel.querySelectorAll('.control-stack'));
      if (!stacks.length) return;
      stacks.forEach(stack => {
        if (isHidden) {
          stack.style.display = 'none';
        } else {
          const show = stack.dataset.mobileTab === target;
          stack.style.display = show ? 'block' : 'none';
        }
      });
    }

    function setMobileTab(tab) {
      const name = tab || 'controls';
      const isDesktop = window.matchMedia('(min-width: 1024px)').matches;
      if (document.body) {
        document.body.dataset.mobileTab = name;
        delete document.body.dataset.controlsHidden;
      }
      updateSheets();
      updateMobileStacks(name);
      const buttons = document.querySelectorAll('#mobile-tabbar .mobile-tab-btn');
      buttons.forEach(btn => btn.setAttribute('aria-pressed', String(btn.dataset.mobileTab === name)));
    }
    window.__setMobileTab = setMobileTab;

    let floatingNudgeCollapsed = false;
    function updateFloatingNudge() {
      const el = document.getElementById('floating-topper-nudge');
      if (!el) return;
      const isMobile = !window.matchMedia('(min-width: 1024px)').matches;
      const classicActive = document.body?.dataset.activeTab === '#tab-classic';
      const topperActive = document.body?.dataset.topperOverlay === '1';
      const shouldShow = isMobile && classicActive && topperActive;
      el.classList.toggle('hidden', !shouldShow);
      el.classList.toggle('collapsed', floatingNudgeCollapsed);
      const toggle = document.getElementById('floating-nudge-toggle');
      if (toggle) toggle.textContent = floatingNudgeCollapsed ? 'Show' : 'Hide';
    }
    window.__updateFloatingNudge = updateFloatingNudge;

    if (orgSection && claSection && tabBtns.length > 0) {
      let current = '#tab-organic';

      function setTab(id, isInitial = false) {
        if (!id || !document.querySelector(id)) id = '#tab-organic';
        current = id;
        lastActiveTab = id;
        if (document.body) document.body.dataset.activeTab = id;
        
        // Reset minimized state on tab switch
        orgSheet?.classList.remove('minimized');
        claSheet?.classList.remove('minimized');

        orgSection.classList.toggle('hidden', id !== '#tab-organic');
        claSection.classList.toggle('hidden', id !== '#tab-classic');
        updateSheets();
        updateFloatingNudge();

        tabBtns.forEach(btn => {
          const active = btn.dataset.target === id;
          btn.classList.toggle('tab-active', active);
          btn.classList.toggle('tab-idle', !active);
          btn.setAttribute('aria-pressed', String(active));
        });

        if (!isInitial) {
          try { localStorage.setItem(ACTIVE_TAB_KEY, id); } catch {}
        }

        if (document.body) delete document.body.dataset.controlsHidden;
        setMobileTab(document.body?.dataset?.mobileTab || 'controls');
        orgSheet?.classList.toggle('hidden', id !== '#tab-organic');
        claSheet?.classList.toggle('hidden', id !== '#tab-classic');

        if (window.updateExportButtonVisibility) window.updateExportButtonVisibility();
      }

      tabBtns.forEach(btn => {
        btn.addEventListener('click', (e) => {
          const button = e.target.closest('button[data-target]');
          if (button) setTab(button.dataset.target);
        });
      });

      let savedTab = null;
      try { savedTab = localStorage.getItem(ACTIVE_TAB_KEY); } catch {}
      setTab(savedTab || '#tab-organic', true);

      window.__whichTab = () => current;
      // ensure mobile default
      if (!document.body?.dataset?.mobileTab) document.body.dataset.mobileTab = 'controls';
      setMobileTab(document.body.dataset.mobileTab);
      updateSheets();
      updateMobileStacks(document.body.dataset.mobileTab);
    }

    // ===============================
    // ===== Mobile bottom tabs ======
    // ===============================
    (function initMobileTabs() {
      const buttons = Array.from(document.querySelectorAll('#mobile-tabbar .mobile-tab-btn'));
      if (!buttons.length) return;

      buttons.forEach(btn => {
        btn.addEventListener('click', () => {
          const tab = btn.dataset.mobileTab || 'controls';
          const panel = (!document.getElementById('tab-classic')?.classList.contains('hidden')
            ? document.getElementById('classic-controls-panel')
            : document.getElementById('controls-panel'));
            
          const currentTab = document.body.dataset.mobileTab;
          
          if (tab === currentTab) {
              // Toggle minimized state
              panel.classList.toggle('minimized');
          } else {
              // Switch tab and ensure expanded
              panel.classList.remove('minimized');
              setMobileTab(tab);
              panel.scrollTop = 0;
          }
        });
      });

      const mq = window.matchMedia('(min-width: 1024px)');
      const sync = () => {
        if (mq.matches) {
          document.body?.removeAttribute('data-mobile-tab');
          updateMobileStacks('controls'); // keep controls visible on desktop
        } else {
          setMobileTab(document.body?.dataset?.mobileTab || 'controls');
        }
        updateFloatingNudge();
      };
      mq.addEventListener('change', sync);
      setMobileTab(document.body?.dataset?.mobileTab || 'controls');
      sync();

      const nudgeToggle = document.getElementById('floating-nudge-toggle');
      nudgeToggle?.addEventListener('click', () => {
        floatingNudgeCollapsed = !floatingNudgeCollapsed;
        updateFloatingNudge();
      });
    })();
  });
})();
