// 1Radar — Whiteboard (Miro-style infinite canvas)
// Pan/zoom, sticky notes, text blocks, shapes, frames, pen, images, connectors.
// Connectors: hover any element to reveal 4 side-ports, drag from port to port.
// Pen strokes: selectable, movable, editable like other elements.

const WB_STICKY_PALETTE = ['#FFCB5C','#FF8FA3','#A5D8FF','#B7F0AD','#F3A0F9','#FFD6B0','#FFFFFF','#1A1A2E'];
const WB_SHAPE_PALETTE  = ['#335CFF','#7B5BFF','#10B981','#F59E0B','#E5484D','#F97316','#06B6D4','#EC4899','#FFFFFF','#1A1A2E'];
const WB_PEN_PALETTE    = ['#1A1A2E','#335CFF','#E5484D','#10B981','#F59E0B','#7B5BFF','#F97316','#FFFFFF'];
const WB_TEXT_PALETTE   = ['#1A1A2E','#335CFF','#E5484D','#10B981','#F59E0B','#7B5BFF','#94A3B8','#FFFFFF'];
const WB_FRAME_PALETTE  = ['#335CFF','#7B5BFF','#10B981','#F59E0B','#E5484D','#F97316','#EC4899','#94A3B8'];

let _wbIdCtr = 1;
const wbId = () => `w${_wbIdCtr++}`;

const INITIAL_WB_ELEMENTS = [
  { id:'wi0', type:'frame',  x:60,  y:80,  w:500, h:340, title:'Campaign Brief', color:'#335CFF' },
  { id:'wi1', type:'sticky', x:100, y:140, w:185, h:145, text:'Target audience:\nWomen 25–40, health-conscious urban lifestyle', color:'#FFCB5C', size:13, align:'left' },
  { id:'wi2', type:'sticky', x:310, y:140, w:200, h:145, text:'Key message:\n"Feel good in your skin — naturally"', color:'#A5D8FF', size:13, align:'left' },
  { id:'wi3', type:'sticky', x:100, y:310, w:410, h:80,  text:'Budget: $15k/month · Channels: Meta + TikTok · Launch: Q2', color:'#B7F0AD', size:13, align:'left' },
  { id:'wi4', type:'text',   x:630, y:90,  w:280, h:46,  text:'Mood Board', size:30, bold:true, color:'#1A1A2E', align:'left' },
  { id:'wi5', type:'shape',  x:630, y:155, w:120, h:120, shape:'rect',     fill:'#335CFF', stroke:'none', opacity:1 },
  { id:'wi6', type:'shape',  x:770, y:155, w:120, h:120, shape:'circle',   fill:'#10B981', stroke:'none', opacity:1 },
  { id:'wi7', type:'shape',  x:630, y:295, w:120, h:120, shape:'diamond',  fill:'#F59E0B', stroke:'none', opacity:1 },
  { id:'wi8', type:'shape',  x:770, y:295, w:120, h:120, shape:'triangle', fill:'#E5484D', stroke:'none', opacity:1 },
  { id:'wi9', type:'sticky', x:60,  y:500, w:190, h:155, text:'Competitor A:\n→ Too formal\n→ Weak UGC\n→ No storytelling', color:'#FFD6B0', size:12, align:'left' },
  { id:'wi10',type:'sticky', x:280, y:500, w:190, h:155, text:'Competitor B:\n→ Good hooks\n→ Strong CTA\n→ High spend', color:'#F3A0F9', size:12, align:'left' },
  { id:'wi11',type:'sticky', x:500, y:500, w:190, h:155, text:'Our advantage:\n→ Authentic UGC\n→ Strong brand story\n→ Better targeting', color:'#FFCB5C', size:12, align:'left' },
  { id:'wi12',type:'text',   x:60,  y:685, w:200, h:36,  text:'Next Steps', size:20, bold:true, color:'#1A1A2E', align:'left' },
  { id:'wi13',type:'text',   x:60,  y:725, w:620, h:30,  text:'1. Finalize creative brief  ·  2. Book UGC creators  ·  3. Set up Meta campaigns  ·  4. Review on May 30', size:13, bold:false, color:'#64748B', align:'left' },
];

// Connectors now carry fromPort / toPort ('top'|'right'|'bottom'|'left')
const INITIAL_WB_CONNECTORS = [
  { id:'wc1', fromId:'wi1', fromPort:'right',  toId:'wi2',  toPort:'left',  style:'arrow',  color:'#94A3B8' },
  { id:'wc2', fromId:'wi0', fromPort:'bottom', toId:'wi9',  toPort:'top',   style:'dashed', color:'#94A3B8' },
  { id:'wc3', fromId:'wi0', fromPort:'bottom', toId:'wi10', toPort:'top',   style:'dashed', color:'#94A3B8' },
  { id:'wc4', fromId:'wi0', fromPort:'bottom', toId:'wi11', toPort:'top',   style:'dashed', color:'#94A3B8' },
];

// ─────────────────────────────────────────────────────────────────────────────
const MINI_W = 168, MINI_H = 105;
const SNAP_PX = 6;
const SIDES = ['top','right','bottom','left'];

// Port position on an element (canvas coords)
const portPos = (el, side) => {
  const cx = el.x + el.w / 2, cy = el.y + el.h / 2;
  switch (side) {
    case 'top':    return { x: cx,       y: el.y       };
    case 'right':  return { x: el.x+el.w, y: cy        };
    case 'bottom': return { x: cx,       y: el.y+el.h  };
    case 'left':   return { x: el.x,     y: cy         };
    default:       return { x: cx,       y: cy         };
  }
};

// Control-point direction per port side (unit vector)
const portDir = (side) => {
  switch (side) {
    case 'top':    return { dx:  0, dy: -1 };
    case 'right':  return { dx:  1, dy:  0 };
    case 'bottom': return { dx:  0, dy:  1 };
    case 'left':   return { dx: -1, dy:  0 };
    default:       return { dx:  1, dy:  0 };
  }
};

// Bounding box for a pen stroke
const penBBox = (el) => {
  if (!el.points?.length) return { x:0, y:0, w:0, h:0 };
  const xs = el.points.map(p => p.x), ys = el.points.map(p => p.y);
  const x = Math.min(...xs), y = Math.min(...ys);
  return { x, y, w: Math.max(...xs)-x, h: Math.max(...ys)-y };
};

// ── Custom range slider (matches workflow/canvas design) ─────────────────────
const WbSlider = ({ value, min=0, max=100, step=1, unit='', onChange, onCommit }) => {
  const pct = Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100));
  return (
    <div style={{display:'flex',alignItems:'center',gap:8}}>
      <input type="range" min={min} max={max} step={step} value={value}
        onChange={e=>onChange(parseFloat(e.target.value))}
        onMouseUp={onCommit}
        style={{
          flex:1, height:4, WebkitAppearance:'none', appearance:'none', border:'none',
          background:`linear-gradient(to right,var(--accent) ${pct}%,var(--border) ${pct}%)`,
          borderRadius:999, outline:'none', cursor:'pointer',
        }}/>
      <span style={{minWidth:34,textAlign:'right',fontFamily:'var(--font-mono)',fontSize:11,
        fontWeight:600,color:'var(--text-2)'}}>{step<1?value.toFixed(1):Math.round(value)}{unit}</span>
    </div>
  );
};

// ── Saved Ads side panel ─────────────────────────────────────────────────────
const WbAdsPanel = ({ onAdd, onClose }) => {
  const [search,      setSearch]      = React.useState('');
  const [openFolders, setOpenFolders] = React.useState(new Set(['all']));

  const folders = (window.SAVE_FOLDERS_V3 || []).filter(f => f.id !== 'trash');
  const allAds  = window.ADS_V3 || [];

  const toggle = (id) => setOpenFolders(s => {
    const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n;
  });

  const getFolderAds = (folderId) => {
    if (folderId === 'all') return allAds;
    const fi = folders.findIndex(f => f.id === folderId);
    const seed = (fi + 1) * 7;
    return allAds.filter((_, i) => (i * seed) % 11 < 5);
  };

  const filterAds = (ads) => !search ? ads :
    ads.filter(a =>
      a.brand.toLowerCase().includes(search.toLowerCase()) ||
      (a.headline || '').toLowerCase().includes(search.toLowerCase())
    );

  const AdGrid = ({ folderId }) => {
    const ads = filterAds(getFolderAds(folderId));
    if (!ads.length) return (
      <div style={{padding:'6px 12px 10px',fontSize:11,color:'var(--text-faint)'}}>No ads found</div>
    );
    return (
      <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:5,padding:'5px 8px 10px'}}>
        {ads.map(ad => (
          <div key={ad.id} onClick={() => onAdd(ad)}
            title={`${ad.brand} — ${ad.headline || ad.primaryText}`}
            style={{borderRadius:7,overflow:'hidden',border:'1px solid var(--border)',
              cursor:'pointer',background:'var(--bg-soft)',transition:'transform 80ms,box-shadow 80ms'}}
            onMouseEnter={e=>{e.currentTarget.style.transform='scale(1.04)';e.currentTarget.style.boxShadow='0 4px 14px rgba(0,0,0,0.13)';}}
            onMouseLeave={e=>{e.currentTarget.style.transform='';e.currentTarget.style.boxShadow='';}}>
            <img src={ad.thumb} alt={ad.brand}
              style={{width:'100%',height:78,objectFit:'cover',display:'block'}}/>
            <div style={{padding:'4px 6px 5px'}}>
              <div style={{fontSize:10,fontWeight:700,color:'var(--text-1)',
                whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{ad.brand}</div>
              <div style={{fontSize:9,color:'var(--text-muted)',marginTop:1,
                whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{ad.type} · {ad.platform}</div>
            </div>
          </div>
        ))}
      </div>
    );
  };

  function FolderRow({ folder, depth }) {
    const isOpen = openFolders.has(folder.id);
    const subFolders = folders.filter(f => f.parentId === folder.id);
    return (
      <>
        <button onClick={() => toggle(folder.id)}
          style={{display:'flex',alignItems:'center',gap:7,
            width:'100%',padding:`6px 10px 6px ${10 + depth * 16}px`,
            border:'none',cursor:'pointer',textAlign:'left',
            background:isOpen?'var(--accent-soft)':'transparent',
            color:isOpen?'var(--accent)':'var(--text)',transition:'background 100ms'}}
          onMouseEnter={e=>{if(!isOpen)e.currentTarget.style.background='var(--bg-soft)';}}
          onMouseLeave={e=>{if(!isOpen)e.currentTarget.style.background='transparent';}}>
          <span style={{color:folder.color||'var(--text-faint)',flexShrink:0}}>
            <Icon name={folder.icon||'folder'} size={depth>0?12:14}/>
          </span>
          <span style={{flex:1,fontSize:depth>0?12:13,fontWeight:500,
            overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{folder.name}</span>
          <span style={{fontSize:10,color:'var(--text-faint)',marginRight:4}}>{folder.count}</span>
          <Icon name={isOpen?'chevron-down':'chevron-right'} size={11}
            style={{color:'var(--text-faint)',flexShrink:0}}/>
        </button>
        {isOpen && (
          <>
            {subFolders.map(sf => <FolderRow key={sf.id} folder={sf} depth={depth + 1}/>)}
            <AdGrid folderId={folder.id}/>
          </>
        )}
      </>
    );
  }

  const sysAll   = folders.find(f => f.id === 'all');
  const rootFolders = folders.filter(f => !f.system && !f.parentId);
  const isAllOpen = openFolders.has('all');

  return (
    <div onMouseDown={e=>e.stopPropagation()}
      style={{position:'absolute',left:58,top:0,bottom:0,width:272,
        background:'var(--bg-surface)',borderRight:'1px solid var(--border)',
        display:'flex',flexDirection:'column',zIndex:55,
        boxShadow:'4px 0 24px rgba(0,0,0,0.10)'}}>

      {/* Header */}
      <div style={{padding:'10px 12px',borderBottom:'1px solid var(--border)',
        display:'flex',alignItems:'center',gap:8,flexShrink:0}}>
        <Icon name="bookmark" size={14}/>
        <span style={{fontWeight:700,fontSize:13,flex:1}}>Saved Ads</span>
        <button onClick={onClose} title="Close"
          style={{width:22,height:22,border:'1px solid var(--border)',borderRadius:6,
            cursor:'pointer',background:'var(--bg-soft)',color:'var(--text-faint)',
            display:'grid',placeItems:'center',flexShrink:0}}>
          <Icon name="close" size={12}/>
        </button>
      </div>

      {/* Search */}
      <div style={{padding:'7px 10px',borderBottom:'1px solid var(--border)',flexShrink:0}}>
        <div style={{display:'flex',alignItems:'center',gap:6,height:30,padding:'0 9px',
          background:'var(--bg-soft)',border:'1px solid var(--border)',borderRadius:7}}>
          <Icon name="search" size={12} style={{color:'var(--text-faint)',flexShrink:0}}/>
          <input value={search} onChange={e=>setSearch(e.target.value)}
            placeholder="Search brand or headline…"
            style={{flex:1,background:'none',border:'none',outline:'none',
              fontSize:12,color:'var(--text)',minWidth:0}}/>
          {search && (
            <button onClick={()=>setSearch('')}
              style={{border:0,background:'none',cursor:'pointer',color:'var(--text-faint)',
                fontSize:14,lineHeight:1,padding:'0 2px',flexShrink:0}}>×</button>
          )}
        </div>
      </div>

      {/* Folder tree */}
      <div className="scroll" style={{flex:1,overflowY:'auto',overflowX:'hidden'}}>
        {/* All saved */}
        {sysAll && (
          <>
            <button onClick={()=>toggle('all')}
              style={{display:'flex',alignItems:'center',gap:7,
                width:'100%',padding:'6px 10px',border:'none',cursor:'pointer',textAlign:'left',
                background:isAllOpen?'var(--accent-soft)':'transparent',
                color:isAllOpen?'var(--accent)':'var(--text)',transition:'background 100ms'}}
              onMouseEnter={e=>{if(!isAllOpen)e.currentTarget.style.background='var(--bg-soft)';}}
              onMouseLeave={e=>{if(!isAllOpen)e.currentTarget.style.background='transparent';}}>
              <Icon name="bookmark" size={14}/>
              <span style={{flex:1,fontSize:13,fontWeight:600}}>All saved</span>
              <span style={{fontSize:10,color:'var(--text-faint)',marginRight:4}}>{sysAll.count}</span>
              <Icon name={isAllOpen?'chevron-down':'chevron-right'} size={11}
                style={{color:'var(--text-faint)',flexShrink:0}}/>
            </button>
            {isAllOpen && <AdGrid folderId="all"/>}
          </>
        )}

        {/* User folder tree */}
        {rootFolders.length > 0 && (
          <div style={{borderTop:'1px solid var(--border)',paddingTop:2}}>
            {rootFolders.map(f => <FolderRow key={f.id} folder={f} depth={0}/>)}
          </div>
        )}
      </div>

      {/* Footer hint */}
      <div style={{padding:'7px 12px',borderTop:'1px solid var(--border)',flexShrink:0,
        fontSize:10,color:'var(--text-faint)',textAlign:'center'}}>
        Click an ad to place it on the canvas
      </div>
    </div>
  );
};

// ─────────────────────────────────────────────────────────────────────────────
const WhiteboardV1 = () => {
  const [elements,    setElements]    = React.useState(INITIAL_WB_ELEMENTS);
  const [connectors,  setConnectors]  = React.useState(INITIAL_WB_CONNECTORS);
  const [selected,    setSelected]    = React.useState(new Set());
  const [zoom,        setZoom]        = React.useState(0.85);
  const [pan,         setPan]         = React.useState({ x: 60, y: 20 });
  const [tool,        setTool]        = React.useState('select');
  const [shapeType,   setShapeType]   = React.useState('rect');
  const [editingId,   setEditingId]   = React.useState(null);
  const [panning,     setPanning]     = React.useState(false);
  const [boxSel,      setBoxSel]      = React.useState(null);
  const [drawing,     setDrawing]     = React.useState(null);
  // { fromId, fromPort } while dragging a new connector from a port
  const [connecting,     setConnecting]    = React.useState(null);
  const [selectedConns,  setSelectedConns] = React.useState(new Set());
  const [editingConnId,  setEditingConnId] = React.useState(null);
  const [editLabel,      setEditLabel]     = React.useState('');
  // mouse position in viewport-relative coords for in-progress connector
  const [connMouse,      setConnMouse]     = React.useState({ x:0, y:0 });
  // which element is hovered (to show its 4 ports)
  const [hoveredElId, setHoveredElId] = React.useState(null);
  const [stickyColor, setStickyColor] = React.useState('#FFCB5C');
  const [shapeColor,  setShapeColor]  = React.useState('#335CFF');
  const [penColor,    setPenColor]    = React.useState('#1A1A2E');
  const [penWidth,    setPenWidth]    = React.useState(2.5);
  const [textColor,   setTextColor]   = React.useState('#1A1A2E');
  const [showShapePicker, setShowShapePicker] = React.useState(false);
  const [guides,      setGuides]      = React.useState({ x:null, y:null });
  const [showMini,    setShowMini]    = React.useState(true);
  const [showAdsPanel, setShowAdsPanel] = React.useState(false);

  const viewportRef    = React.useRef(null);
  const spaceRef       = React.useRef(false);
  const elementsRef    = React.useRef(elements);
  const connectorsRef  = React.useRef(connectors);
  const connectingRef  = React.useRef(null); // mirrors connecting for port handlers
  const connCompletedRef = React.useRef(false); // did a port mouseup complete the conn?
  const imgInputRef    = React.useRef(null);

  React.useEffect(() => { elementsRef.current = elements;     }, [elements]);
  React.useEffect(() => { connectorsRef.current = connectors; }, [connectors]);
  React.useEffect(() => { connectingRef.current = connecting; }, [connecting]);

  // ── Undo / Redo ─────────────────────────────────────────────────────────────
  const histRef = React.useRef([{ elements: INITIAL_WB_ELEMENTS, connectors: INITIAL_WB_CONNECTORS }]);
  const histIdx = React.useRef(0);

  const pushHist = (els, cons) => {
    const h = histRef.current.slice(0, histIdx.current + 1);
    h.push({ elements: els, connectors: cons });
    if (h.length > 80) h.shift();
    histRef.current = h;
    histIdx.current = h.length - 1;
  };

  const undoFn = React.useRef(null);
  const redoFn = React.useRef(null);
  undoFn.current = () => {
    if (histIdx.current <= 0) return;
    histIdx.current--;
    const s = histRef.current[histIdx.current];
    setElements(s.elements); setConnectors(s.connectors);
    setSelected(new Set()); setEditingId(null);
  };
  redoFn.current = () => {
    if (histIdx.current >= histRef.current.length - 1) return;
    histIdx.current++;
    const s = histRef.current[histIdx.current];
    setElements(s.elements); setConnectors(s.connectors);
    setSelected(new Set()); setEditingId(null);
  };

  // ── Coordinate helpers ───────────────────────────────────────────────────────
  const toCanvas = React.useCallback((sx, sy) => {
    const rect = viewportRef.current?.getBoundingClientRect() || { left:0, top:0 };
    return { x: (sx - rect.left - pan.x) / zoom, y: (sy - rect.top - pan.y) / zoom };
  }, [pan, zoom]);

  // Canvas coords → viewport-relative
  const toViewport = (cx, cy) => ({ x: cx * zoom + pan.x, y: cy * zoom + pan.y });

  // ── Wheel zoom ───────────────────────────────────────────────────────────────
  React.useEffect(() => {
    const el = viewportRef.current; if (!el) return;
    const handler = (e) => {
      e.preventDefault();
      const rect = el.getBoundingClientRect();
      const mx = e.clientX - rect.left, my = e.clientY - rect.top;
      const factor = e.deltaY > 0 ? 0.9 : 1.11;
      setZoom(z => {
        const nz = Math.min(5, Math.max(0.08, z * factor));
        const scale = nz / z;
        setPan(p => ({ x: mx - (mx - p.x)*scale, y: my - (my - p.y)*scale }));
        return nz;
      });
    };
    el.addEventListener('wheel', handler, { passive: false });
    return () => el.removeEventListener('wheel', handler);
  }, []);

  // ── Space key ────────────────────────────────────────────────────────────────
  React.useEffect(() => {
    const dn = (e) => {
      if (e.code !== 'Space') return;
      const t = e.target?.tagName?.toLowerCase();
      if (t === 'input' || t === 'textarea' || e.target?.isContentEditable) return;
      if (!spaceRef.current) spaceRef.current = true;
      e.preventDefault();
    };
    const up = (e) => { if (e.code === 'Space') spaceRef.current = false; };
    window.addEventListener('keydown', dn);
    window.addEventListener('keyup',   up);
    return () => { window.removeEventListener('keydown', dn); window.removeEventListener('keyup', up); };
  }, []);

  // ── Keyboard shortcuts ───────────────────────────────────────────────────────
  React.useEffect(() => {
    const onKey = (e) => {
      const tag = e.target?.tagName?.toLowerCase();
      if (tag === 'input' || tag === 'textarea' || e.target?.isContentEditable) return;

      if ((e.metaKey||e.ctrlKey) && e.key==='z' && !e.shiftKey) { e.preventDefault(); undoFn.current(); return; }
      if ((e.metaKey||e.ctrlKey) && (e.key==='y' || (e.key==='z' && e.shiftKey))) { e.preventDefault(); redoFn.current(); return; }
      if ((e.metaKey||e.ctrlKey) && e.key==='a') {
        e.preventDefault();
        setSelected(new Set(elementsRef.current.map(el=>el.id)));
        return;
      }
      if ((e.metaKey||e.ctrlKey) && e.key==='d') {
        e.preventDefault();
        const sel=[...selected]; if (!sel.length) return;
        const copies = elementsRef.current.filter(el=>sel.includes(el.id))
          .map(el => ({ ...el, id:wbId(), x:el.x+24, y:el.y+24,
            ...(el.type==='pen' ? { points: el.points.map(p=>({...p, x:p.x+24, y:p.y+24})) } : {}) }));
        const newEls = [...elementsRef.current, ...copies];
        setElements(newEls); pushHist(newEls, connectorsRef.current);
        setSelected(new Set(copies.map(c=>c.id)));
        return;
      }
      if (e.key==='Escape') {
        if (connecting) { setConnecting(null); return; }
        setTool('select'); setSelected(new Set()); setSelectedConns(new Set()); setEditingId(null);
        return;
      }
      if ((e.key==='Delete'||e.key==='Backspace') && (selected.size || selectedConns.size)) {
        const ids = selected;
        const connIds = selectedConns;
        const newEls  = elementsRef.current.filter(el => !ids.has(el.id));
        const newCons = connectorsRef.current.filter(c =>
          !ids.has(c.fromId) && !ids.has(c.toId) && !connIds.has(c.id)
        );
        setElements(newEls); setConnectors(newCons);
        pushHist(newEls, newCons); setSelected(new Set()); setSelectedConns(new Set());
        return;
      }
      if (!e.metaKey && !e.ctrlKey && !e.altKey) {
        if (e.key==='v'||e.key==='V') setTool('select');
        if (e.key==='h'||e.key==='H') setTool('grab');
        if (e.key==='s'||e.key==='S') setTool('sticky');
        if (e.key==='t'||e.key==='T') setTool('text');
        if (e.key==='r'||e.key==='R') { setTool('shape'); setShapeType('rect'); }
        if (e.key==='p'||e.key==='P') setTool('pen');
        if (e.key==='f'||e.key==='F') setTool('frame');
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [selected, connecting, selectedConns]);

  // ── Snap helpers ─────────────────────────────────────────────────────────────
  const snapToOthers = (x, y, w, h, excludeSet) => {
    const others = elementsRef.current.filter(e => !excludeSet.has(e.id) && e.type!=='pen' && e.w && e.h);
    let nx=x, ny=y, gx=null, gy=null, bestX=SNAP_PX+1, bestY=SNAP_PX+1;
    for (const o of others) {
      for (const [a,b] of [[x,o.x],[x+w,o.x],[x,o.x+o.w],[x+w,o.x+o.w],[x+w/2,o.x+o.w/2]]) {
        const d=Math.abs(a-b); if (d<bestX){bestX=d;nx=x+(b-a);gx=b;}
      }
      for (const [a,b] of [[y,o.y],[y+h,o.y],[y,o.y+o.h],[y+h,o.y+o.h],[y+h/2,o.y+o.h/2]]) {
        const d=Math.abs(a-b); if (d<bestY){bestY=d;ny=y+(b-a);gy=b;}
      }
    }
    return { x:bestX<=SNAP_PX?nx:x, y:bestY<=SNAP_PX?ny:y, gx:bestX<=SNAP_PX?gx:null, gy:bestY<=SNAP_PX?gy:null };
  };

  // ── Zoom to fit ──────────────────────────────────────────────────────────────
  const zoomFit = () => {
    const all = elementsRef.current;
    const vp = viewportRef.current?.getBoundingClientRect();
    if (!vp || !all.length) { setZoom(1); setPan({x:0,y:0}); return; }
    const bboxes = all.map(e => e.type==='pen' ? penBBox(e) : {x:e.x,y:e.y,w:e.w||0,h:e.h||0});
    const xs = bboxes.flatMap(b=>[b.x,b.x+b.w]);
    const ys = bboxes.flatMap(b=>[b.y,b.y+b.h]);
    const bx=Math.min(...xs), by=Math.min(...ys), bw=Math.max(...xs)-bx, bh=Math.max(...ys)-by;
    const z = Math.max(0.08, Math.min((vp.width-80)/(bw||1),(vp.height-80)/(bh||1),1.5));
    setZoom(z); setPan({ x:(vp.width-bw*z)/2-bx*z, y:(vp.height-bh*z)/2-by*z });
  };

  // ── Element mutations ────────────────────────────────────────────────────────
  const addEl = (el) => {
    const newEls = [...elementsRef.current, el];
    setElements(newEls); pushHist(newEls, connectorsRef.current);
  };
  const patchEl = (id, patch) =>
    setElements(es => es.map(e => e.id===id ? {...e,...patch} : e));
  const patchAndPush = (id, patch) => {
    const newEls = elementsRef.current.map(e => e.id===id ? {...e,...patch} : e);
    setElements(newEls); pushHist(newEls, connectorsRef.current);
  };

  const addAdToCanvas = (ad) => {
    const vp = viewportRef.current?.getBoundingClientRect() || { width: 900, height: 600 };
    const p = toCanvas(
      vp.width  / 2 + (Math.random() * 60 - 30),
      vp.height / 2 + (Math.random() * 60 - 30),
    );
    const id = wbId();
    addEl({ id, type: 'ad', x: p.x - 110, y: p.y - 160, w: 220, h: 310, ad });
    setSelected(new Set([id]));
  };

  // ── Drag elements ────────────────────────────────────────────────────────────
  const startDrag = (e, id) => {
    if (e.button!==0 || spaceRef.current || tool==='grab') return;
    if (editingId) return;
    setSelectedConns(new Set());

    const wasSel = selected.has(id);
    if (!wasSel) {
      if (!e.shiftKey) setSelected(new Set([id]));
      else setSelected(s => { const n=new Set(s); n.has(id)?n.delete(id):n.add(id); return n; });
    }

    const currentSel = wasSel ? new Set(selected) : new Set([id]);
    const startMX=e.clientX, startMY=e.clientY;
    // Store starting positions AND pen points
    const startPos = new Map(
      elementsRef.current.filter(el=>currentSel.has(el.id)).map(el => [
        el.id,
        el.type==='pen'
          ? { penPoints: el.points.map(p=>({...p})) }
          : { x:el.x, y:el.y }
      ])
    );
    let moved = false;
    // Accumulated canvas-space displacement from auto-scroll panning
    let autoPanX = 0, autoPanY = 0;
    let lastEv = { clientX: e.clientX, clientY: e.clientY };
    let rafId = null;

    // Apply positions given current mouse + accumulated auto-pan
    const applyPositions = (evX, evY) => {
      const dx = (evX - startMX) / zoom + autoPanX;
      const dy = (evY - startMY) / zoom + autoPanY;

      // Single-element snap (only for non-pen)
      if (currentSel.size===1) {
        const el = elementsRef.current.find(x=>x.id===id);
        if (el && el.type!=='pen') {
          const sp = startPos.get(id);
          const { x:sx, y:sy, gx, gy } = snapToOthers(sp.x+dx, sp.y+dy, el.w, el.h, currentSel);
          setGuides({ x:gx, y:gy });
          const snapDx=sx-sp.x, snapDy=sy-sp.y;
          setElements(es => es.map(el2 => {
            const sp2=startPos.get(el2.id); if(!sp2) return el2;
            if (el2.id===id) return {...el2, x:sx, y:sy};
            return el2.type==='pen'
              ? {...el2, points: sp2.penPoints.map(p=>({x:p.x+snapDx, y:p.y+snapDy}))}
              : {...el2, x:sp2.x+snapDx, y:sp2.y+snapDy};
          }));
          return;
        }
      }

      setGuides({ x:null, y:null });
      setElements(es => es.map(el2 => {
        const sp=startPos.get(el2.id); if(!sp) return el2;
        return el2.type==='pen'
          ? {...el2, points: sp.penPoints.map(p=>({x:p.x+dx, y:p.y+dy}))}
          : {...el2, x:sp.x+dx, y:sp.y+dy};
      }));
    };

    // Edge auto-scroll: speed proportional to proximity to edge
    const EDGE_MARGIN = 64, MAX_SPEED = 14;
    const edgeSpeed = (pos, lo, hi) => {
      if (pos < lo + EDGE_MARGIN) return MAX_SPEED * (1 - (pos - lo) / EDGE_MARGIN);
      if (pos > hi - EDGE_MARGIN) return -MAX_SPEED * (1 - (hi - pos) / EDGE_MARGIN);
      return 0;
    };

    const autoScroll = () => {
      const vpRect = viewportRef.current?.getBoundingClientRect();
      if (!vpRect) { rafId = null; return; }
      const dpx = edgeSpeed(lastEv.clientX, vpRect.left, vpRect.right);
      const dpy = edgeSpeed(lastEv.clientY, vpRect.top,  vpRect.bottom);
      if (dpx !== 0 || dpy !== 0) {
        // Pan the canvas (screen px), and accumulate the inverse in canvas coords
        // so the element follows the cursor into the newly revealed area
        autoPanX -= dpx / zoom;
        autoPanY -= dpy / zoom;
        setPan(p => ({ x: p.x + dpx, y: p.y + dpy }));
        applyPositions(lastEv.clientX, lastEv.clientY);
        rafId = requestAnimationFrame(autoScroll);
      } else {
        rafId = null;
      }
    };

    const onMove = (ev) => {
      lastEv = ev;
      if (!moved && Math.abs(ev.clientX-startMX)<2 && Math.abs(ev.clientY-startMY)<2) return;
      moved = true;
      applyPositions(ev.clientX, ev.clientY);
      // Kick off auto-scroll loop if mouse is near a viewport edge
      const vpRect = viewportRef.current?.getBoundingClientRect();
      if (vpRect) {
        const near = ev.clientX < vpRect.left + EDGE_MARGIN || ev.clientX > vpRect.right - EDGE_MARGIN
                  || ev.clientY < vpRect.top  + EDGE_MARGIN || ev.clientY > vpRect.bottom - EDGE_MARGIN;
        if (near && !rafId) rafId = requestAnimationFrame(autoScroll);
      }
    };

    const onUp = () => {
      if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
      setGuides({x:null,y:null});
      if (moved) pushHist(elementsRef.current, connectorsRef.current);
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup',   onUp);
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup',   onUp);
    e.stopPropagation();
  };

  // ── Resize ───────────────────────────────────────────────────────────────────
  const startResize = (e, id, handle) => {
    e.stopPropagation(); e.preventDefault();
    const el = elementsRef.current.find(x=>x.id===id); if(!el) return;
    const startX=e.clientX, startY=e.clientY;
    const orig={x:el.x,y:el.y,w:el.w,h:el.h};
    const onMove = (ev) => {
      const dx=(ev.clientX-startX)/zoom, dy=(ev.clientY-startY)/zoom;
      let {x,y,w,h}=orig;
      if(handle.includes('e')) w=Math.max(60,orig.w+dx);
      if(handle.includes('s')) h=Math.max(30,orig.h+dy);
      if(handle.includes('w')){x=orig.x+dx;w=Math.max(60,orig.w-dx);}
      if(handle.includes('n')){y=orig.y+dy;h=Math.max(30,orig.h-dy);}
      patchEl(id,{x,y,w,h});
    };
    const onUp = () => {
      pushHist(elementsRef.current, connectorsRef.current);
      window.removeEventListener('mousemove',onMove); window.removeEventListener('mouseup',onUp);
    };
    window.addEventListener('mousemove',onMove); window.addEventListener('mouseup',onUp);
  };

  // ── Port connection handlers ─────────────────────────────────────────────────
  const onPortMD = (elId, side, e) => {
    e.stopPropagation(); e.preventDefault();
    connCompletedRef.current = false;
    setConnecting({ fromId:elId, fromPort:side });
    const rect = viewportRef.current?.getBoundingClientRect()||{left:0,top:0};
    setConnMouse({ x:e.clientX-rect.left, y:e.clientY-rect.top });

    // Cancel the connection if the mouse is released anywhere except a port
    const onUp = () => {
      if (!connCompletedRef.current) setConnecting(null);
      window.removeEventListener('mouseup', onUp);
    };
    window.addEventListener('mouseup', onUp);
  };

  const onPortMU = (elId, side, e) => {
    e.stopPropagation();
    const conn = connectingRef.current;
    if (!conn) return;
    if (conn.fromId===elId) { setConnecting(null); return; }
    connCompletedRef.current = true;
    const newCon = {
      id:wbId(), fromId:conn.fromId, fromPort:conn.fromPort,
      toId:elId, toPort:side, style:'arrow', color:'#94A3B8',
    };
    const newCons = [...connectorsRef.current, newCon];
    setConnectors(newCons); pushHist(elementsRef.current, newCons);
    setConnecting(null);
  };

  // ── Canvas mouse down ────────────────────────────────────────────────────────
  const onCanvasMD = (e) => {
    if (e.button!==0 && e.button!==1) return;
    if (editingId) { setEditingId(null); return; }

    const wantPan = e.button===1 || spaceRef.current || tool==='grab';
    if (wantPan) {
      e.preventDefault(); setPanning(true);
      const ox=pan.x,oy=pan.y,sx=e.clientX,sy=e.clientY;
      const onMove=(ev)=>setPan({x:ox+ev.clientX-sx,y:oy+ev.clientY-sy});
      const onUp=()=>{setPanning(false);window.removeEventListener('mousemove',onMove);window.removeEventListener('mouseup',onUp);};
      window.addEventListener('mousemove',onMove); window.addEventListener('mouseup',onUp);
      return;
    }
    if (e.button!==0) return;
    const p = toCanvas(e.clientX, e.clientY);

    if (tool==='pen') {
      e.preventDefault();
      const id=wbId();
      setElements(es=>[...es,{id,type:'pen',points:[p],color:penColor,width:penWidth}]);
      const onMove=(ev)=>{
        const pp=toCanvas(ev.clientX,ev.clientY);
        setElements(es=>es.map(el=>el.id===id?{...el,points:[...el.points,pp]}:el));
      };
      const onUp=()=>{
        pushHist(elementsRef.current,connectorsRef.current);
        window.removeEventListener('mousemove',onMove); window.removeEventListener('mouseup',onUp);
      };
      window.addEventListener('mousemove',onMove); window.addEventListener('mouseup',onUp);
      return;
    }

    if (tool==='sticky') {
      e.preventDefault();
      const id=wbId();
      const el={id,type:'sticky',x:p.x-90,y:p.y-70,w:185,h:145,text:'',color:stickyColor,size:13,align:'left'};
      addEl(el); setSelected(new Set([id])); setEditingId(id); setTool('select');
      return;
    }

    if (tool==='text') {
      e.preventDefault();
      const id=wbId();
      const el={id,type:'text',x:p.x-100,y:p.y-18,w:220,h:36,text:'',size:18,bold:false,color:textColor,align:'left'};
      addEl(el); setSelected(new Set([id])); setEditingId(id); setTool('select');
      return;
    }

    if (tool==='shape') {
      e.preventDefault();
      const start={...p};
      setDrawing({kind:'shape',x:start.x,y:start.y,w:0,h:0});
      const onMove=(ev)=>{const pp=toCanvas(ev.clientX,ev.clientY);setDrawing({kind:'shape',x:Math.min(start.x,pp.x),y:Math.min(start.y,pp.y),w:Math.abs(pp.x-start.x),h:Math.abs(pp.y-start.y)});};
      const onUp=(ev)=>{
        const pp=toCanvas(ev.clientX,ev.clientY);const w=Math.abs(pp.x-start.x),h=Math.abs(pp.y-start.y);
        if(w>10&&h>10){const id=wbId();addEl({id,type:'shape',x:Math.min(start.x,pp.x),y:Math.min(start.y,pp.y),w,h,shape:shapeType,fill:'none',stroke:'#335CFF',opacity:1});setSelected(new Set([id]));}
        setDrawing(null);setTool('select');window.removeEventListener('mousemove',onMove);window.removeEventListener('mouseup',onUp);
      };
      window.addEventListener('mousemove',onMove);window.addEventListener('mouseup',onUp);
      return;
    }

    if (tool==='frame') {
      e.preventDefault();
      const start={...p};
      setDrawing({kind:'frame',x:start.x,y:start.y,w:0,h:0});
      const onMove=(ev)=>{const pp=toCanvas(ev.clientX,ev.clientY);setDrawing({kind:'frame',x:Math.min(start.x,pp.x),y:Math.min(start.y,pp.y),w:Math.abs(pp.x-start.x),h:Math.abs(pp.y-start.y)});};
      const onUp=(ev)=>{
        const pp=toCanvas(ev.clientX,ev.clientY);const w=Math.abs(pp.x-start.x),h=Math.abs(pp.y-start.y);
        if(w>40&&h>40){const id=wbId();const n=elementsRef.current.filter(e=>e.type==='frame').length+1;addEl({id,type:'frame',x:Math.min(start.x,pp.x),y:Math.min(start.y,pp.y),w,h,title:`Frame ${n}`,color:'#335CFF'});setSelected(new Set([id]));}
        setDrawing(null);setTool('select');window.removeEventListener('mousemove',onMove);window.removeEventListener('mouseup',onUp);
      };
      window.addEventListener('mousemove',onMove);window.addEventListener('mouseup',onUp);
      return;
    }

    // Box select
    if (tool==='select'||tool==='grab') {
      const additive=e.shiftKey;
      const baseSel=additive?new Set(selected):new Set();
      if(!additive){ setSelected(new Set()); setSelectedConns(new Set()); }
      setBoxSel({x1:p.x,y1:p.y,x2:p.x,y2:p.y});
      const onMove=(ev)=>{const pp=toCanvas(ev.clientX,ev.clientY);setBoxSel(b=>b?{...b,x2:pp.x,y2:pp.y}:null);};
      const onUp=(ev)=>{
        const ep=toCanvas(ev.clientX,ev.clientY);
        const moved=Math.abs(ev.clientX-e.clientX)>4||Math.abs(ev.clientY-e.clientY)>4;
        if(moved){
          const xMin=Math.min(p.x,ep.x),xMax=Math.max(p.x,ep.x);
          const yMin=Math.min(p.y,ep.y),yMax=Math.max(p.y,ep.y);
          const hit=elementsRef.current.filter(el=>{
            const bb=el.type==='pen'?penBBox(el):{x:el.x,y:el.y,w:el.w||0,h:el.h||0};
            return bb.x<xMax&&(bb.x+bb.w)>xMin&&bb.y<yMax&&(bb.y+bb.h)>yMin;
          }).map(el=>el.id);
          const next=new Set(baseSel);hit.forEach(id=>next.add(id));setSelected(next);
        }
        setBoxSel(null);window.removeEventListener('mousemove',onMove);window.removeEventListener('mouseup',onUp);
      };
      window.addEventListener('mousemove',onMove);window.addEventListener('mouseup',onUp);
    }
  };

  // ── Connector bezier path ────────────────────────────────────────────────────
  const connBezier = (A, fromPort, B, toPort) => {
    const dx = B.x - A.x, dy = B.y - A.y;
    const dist = Math.max(1, Math.sqrt(dx*dx + dy*dy));
    const dA = portDir(fromPort||'right'), dB = portDir(toPort||'left');
    // Alignment of each port direction with the A→B vector (−1 = opposing, +1 = aligned)
    const alignA = (dA.dx*dx + dA.dy*dy) / dist;
    const alignB = (-dB.dx*dx - dB.dy*dy) / dist;
    // Base is purely proportional — NO fixed minimum.
    // A fixed min (e.g. 60px) causes control points to cross each other on close elements → S-loop.
    // With base = dist*0.42, the max chord usage is 2*0.42*dist = 0.84*dist < dist → never cross.
    const base = dist * 0.42;
    const lenA = Math.max(8, base * Math.max(0.12, (alignA + 1) / 2));
    const lenB = Math.max(8, base * Math.max(0.12, (alignB + 1) / 2));
    const cp1 = { x:A.x+dA.dx*lenA, y:A.y+dA.dy*lenA };
    const cp2 = { x:B.x+dB.dx*lenB, y:B.y+dB.dy*lenB };
    const mid  = { x:(A.x+3*cp1.x+3*cp2.x+B.x)/8, y:(A.y+3*cp1.y+3*cp2.y+B.y)/8 };
    return { d:`M ${A.x} ${A.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${B.x} ${B.y}`, cp1, cp2, mid };
  };

  const deleteConn = (id) => {
    const newCons = connectorsRef.current.filter(c => c.id !== id);
    setConnectors(newCons); pushHist(elementsRef.current, newCons);
    setSelectedConns(new Set()); setEditingConnId(null);
  };

  const patchConn = (id, patch) => {
    const newCons = connectorsRef.current.map(c => c.id===id ? {...c,...patch} : c);
    setConnectors(newCons); pushHist(elementsRef.current, newCons);
  };

  const startConnLabelEdit = (id, current) => {
    setEditingConnId(id); setEditLabel(current || '');
    setSelectedConns(new Set([id])); setSelected(new Set());
  };
  const saveConnLabel = (id) => {
    patchConn(id, { label: editLabel.trim() || undefined });
    setEditingConnId(null);
    setSelectedConns(new Set([id]));  // keep connector selected → toolbar stays visible
  };

  // ── Render connectors ────────────────────────────────────────────────────────
  const renderConnectors = () => connectors.map(con => {
    const fromEl = elementsRef.current.find(e=>e.id===con.fromId);
    const toEl   = elementsRef.current.find(e=>e.id===con.toId);
    if (!fromEl || !toEl) return null;

    const A = portPos(fromEl, con.fromPort||'right');
    const B = portPos(toEl,   con.toPort||'left');
    const { d, cp1, cp2, mid } = connBezier(A, con.fromPort, B, con.toPort);
    const angEnd   = Math.atan2(B.y-cp2.y, B.x-cp2.x)*180/Math.PI;
    const angStart = Math.atan2(A.y-cp1.y, A.x-cp1.x)*180/Math.PI;

    const isConnSel = selectedConns.has(con.id);
    const isElSel   = selected.has(con.fromId) || selected.has(con.toId);
    const isSel     = isConnSel || isElSel;
    const isEditing = editingConnId === con.id;
    const c = con.color || '#94A3B8';

    // Style props (with defaults)
    const lineStyle  = con.lineStyle  || 'solid';
    const endArrow   = con.endArrow   !== undefined ? con.endArrow   : 'arrow';
    const startArrow = con.startArrow || 'none';
    const sw         = con.strokeWidth || 2;
    const dashArr    = lineStyle==='dashed' ? '10,6' : lineStyle==='dotted' ? '2,6' : undefined;

    const hasLabel   = con.label && con.label.trim();
    const showGap    = hasLabel || isEditing;   // cut the line as soon as edit starts
    const labelFs    = con.fontSize || 11;
    // gap half-width scales with font size
    const gapHW      = Math.max(44, labelFs * 4);

    // Tangent direction at t=0.5 on the cubic bezier → used to orient the label gap
    // Derivative at t=0.5: 3*[0.25*(cp1-A) + 0.5*(cp2-cp1) + 0.25*(B-cp2)]
    const tanX = 0.75 * (B.x + cp2.x - cp1.x - A.x);
    const tanY = 0.75 * (B.y + cp2.y - cp1.y - A.y);
    const tanAngle = Math.atan2(tanY, tanX) * 180 / Math.PI;

    // SVG mask id unique per connector
    const maskId = `lm-${con.id}`;

    return (
      <g key={con.id}>
        {/* SVG mask — cuts a gap aligned with the curve tangent */}
        {showGap && (
          <defs>
            <mask id={maskId}>
              <rect x="-99999" y="-99999" width="199999" height="199999" fill="white"/>
              <rect x={mid.x-gapHW} y={mid.y-(labelFs+2)} width={gapHW*2} height={(labelFs+2)*2} fill="black"
                transform={`rotate(${tanAngle},${mid.x},${mid.y})`}/>
            </mask>
          </defs>
        )}

        {/* Wide hit target */}
        <path d={d} stroke="transparent" strokeWidth={16} fill="none" strokeLinecap="round"
          style={{pointerEvents:'visibleStroke', cursor:'pointer'}}
          onMouseDown={ev=>{
            ev.stopPropagation();
            if (isEditing) return;
            setSelected(new Set());
            setSelectedConns(s => {
              const n = ev.shiftKey ? new Set(s) : new Set();
              n.has(con.id) ? n.delete(con.id) : n.add(con.id);
              return n;
            });
          }}
          onDoubleClick={ev=>{ ev.stopPropagation(); startConnLabelEdit(con.id, con.label||''); }}/>

        {/* Selection glow */}
        {isSel && (
          <path d={d} stroke={c} strokeWidth={sw+6} strokeOpacity={0.22}
                fill="none" strokeLinecap="round" style={{pointerEvents:'none'}}
                mask={showGap ? `url(#${maskId})` : undefined}/>
        )}

        {/* Visible stroke */}
        <path d={d} stroke={c} strokeWidth={sw}
              fill="none" strokeDasharray={dashArr} strokeLinecap="round"
              style={{pointerEvents:'none'}}
              mask={showGap ? `url(#${maskId})` : undefined}/>

        {/* End arrowhead */}
        {endArrow!=='none' && (
          <polygon points={`0,-${sw*1.8+1} ${sw*4+1},0 0,${sw*1.8+1}`} fill={c}
                   transform={`translate(${B.x},${B.y}) rotate(${angEnd})`}
                   style={{pointerEvents:'none'}}/>
        )}

        {/* Start arrowhead */}
        {startArrow==='arrow' && (
          <polygon points={`0,-${sw*1.8+1} ${sw*4+1},0 0,${sw*1.8+1}`} fill={c}
                   transform={`translate(${A.x},${A.y}) rotate(${angStart})`}
                   style={{pointerEvents:'none'}}/>
        )}

        {/* Label — inline editable */}
        {isEditing ? (
          <foreignObject
            x={mid.x-gapHW} y={mid.y-(labelFs+4)}
            width={gapHW*2} height={(labelFs+4)*2}
            transform={`rotate(${tanAngle},${mid.x},${mid.y})`}>
            <input autoFocus value={editLabel}
              onChange={e=>setEditLabel(e.target.value)}
              onBlur={()=>saveConnLabel(con.id)}
              onKeyDown={e=>{
                if(e.key==='Enter'||e.key==='Escape') saveConnLabel(con.id);
                e.stopPropagation();
              }}
              style={{width:'100%',height:'100%',textAlign:'center',fontSize:labelFs,
                fontFamily:'inherit',border:'none',borderRadius:4,
                background:'transparent',color:'var(--text-1)',
                padding:'0 6px',outline:'none',boxSizing:'border-box'}}/>
          </foreignObject>
        ) : hasLabel ? (
          <g onDoubleClick={ev=>{ev.stopPropagation();startConnLabelEdit(con.id,con.label);}}
            transform={`rotate(${tanAngle},${mid.x},${mid.y})`}>
            <text x={mid.x} y={mid.y} textAnchor="middle" dominantBaseline="central"
              style={{fontSize:labelFs,fontWeight:500,fill:'var(--text-1)',
                fontFamily:'inherit',pointerEvents:'none',userSelect:'none',letterSpacing:'0.01em'}}>
              {con.label}
            </text>
          </g>
        ) : null}
      </g>
    );
  });

  // ── Minimap ──────────────────────────────────────────────────────────────────
  const miniData = React.useMemo(() => {
    const all = elements; if (!all.length) return null;
    const bboxes = all.map(e => e.type==='pen' ? penBBox(e) : {x:e.x,y:e.y,w:e.w||0,h:e.h||0});
    const xs=bboxes.flatMap(b=>[b.x,b.x+b.w]), ys=bboxes.flatMap(b=>[b.y,b.y+b.h]);
    const bx=Math.min(...xs),by=Math.min(...ys),bw=Math.max(Math.max(...xs)-bx,1),bh=Math.max(Math.max(...ys)-by,1);
    const scale=Math.min((MINI_W-16)/bw,(MINI_H-16)/bh)*0.9;
    const ox=(MINI_W-bw*scale)/2-bx*scale, oy=(MINI_H-bh*scale)/2-by*scale;
    return { bboxes, scale, ox, oy };
  }, [elements]);

  // ── Derived selection ────────────────────────────────────────────────────────
  const selId     = selected.size===1 ? [...selected][0] : null;
  const selEl     = selId ? elements.find(e=>e.id===selId) : null;
  const selConnId = selectedConns.size===1 ? [...selectedConns][0] : null;
  const selConn   = selConnId ? connectors.find(c=>c.id===selConnId) : null;

  // Should ports be visible for a given element?
  const showPortsFor = (elId) => {
    if (tool==='pen'||tool==='shape'||tool==='frame'||tool==='sticky'||tool==='text') return false;
    return hoveredElId===elId || connecting!==null;
  };

  // ── Render one element ───────────────────────────────────────────────────────
  const renderEl = (el) => {
    const isSel = selected.has(el.id);
    const isEdit = editingId===el.id;
    const base = { position:'absolute', left:el.x, top:el.y, width:el.w, height:el.h, boxSizing:'border-box', cursor:'default' };

    if (el.type==='frame') {
      return (
        <div key={el.id}
          onMouseDown={e=>startDrag(e,el.id)}
          onMouseEnter={()=>setHoveredElId(el.id)}
          onMouseLeave={()=>setHoveredElId(v=>v===el.id?null:v)}
          onDoubleClick={()=>{ setEditingId(el.id); setSelected(new Set([el.id])); }}
          style={{...base, borderRadius:12, border:`2.5px solid ${el.color||'#335CFF'}`, background:`${el.color||'#335CFF'}0C`}}>
          <div style={{position:'absolute',top:-26,left:0,display:'flex',alignItems:'center',gap:5,pointerEvents:'none'}}>
            <div style={{width:8,height:8,borderRadius:2,background:el.color||'#335CFF',flexShrink:0}}/>
            {isEdit
              ? <input autoFocus defaultValue={el.title}
                  onBlur={e2=>{patchAndPush(el.id,{title:e2.target.value});setEditingId(null);}}
                  onKeyDown={e2=>{if(e2.key==='Enter'){patchAndPush(el.id,{title:e2.target.value});setEditingId(null);}e2.stopPropagation();}}
                  onMouseDown={e2=>e2.stopPropagation()}
                  style={{pointerEvents:'all',fontSize:12,fontWeight:700,color:el.color||'#335CFF',border:0,outline:0,background:'transparent',fontFamily:'inherit'}}/>
              : <span style={{fontSize:12,fontWeight:700,color:el.color||'#335CFF'}}>{el.title||'Frame'}</span>
            }
          </div>
        </div>
      );
    }

    if (el.type==='sticky') {
      const dark=el.color==='#1A1A2E';
      const textC=dark?'rgba(255,255,255,0.92)':'rgba(20,20,35,0.85)';
      return (
        <div key={el.id}
          onMouseDown={e=>startDrag(e,el.id)}
          onMouseEnter={()=>setHoveredElId(el.id)}
          onMouseLeave={()=>setHoveredElId(v=>v===el.id?null:v)}
          onDoubleClick={()=>{ setEditingId(el.id); setSelected(new Set([el.id])); }}
          style={{...base,background:el.color||'#FFCB5C',borderRadius:3,boxShadow:'3px 5px 16px rgba(0,0,0,0.18)',padding:13,display:'flex',flexDirection:'column',overflow:'hidden'}}>
          <div style={{position:'absolute',top:0,right:0,width:18,height:18,background:'rgba(0,0,0,0.10)',borderBottomLeftRadius:8,pointerEvents:'none'}}/>
          {isEdit
            ? <textarea autoFocus defaultValue={el.text}
                onBlur={e2=>{patchAndPush(el.id,{text:e2.target.value});setEditingId(null);}}
                onMouseDown={e2=>e2.stopPropagation()} onKeyDown={e2=>e2.stopPropagation()}
                style={{flex:1,background:'transparent',border:0,outline:0,resize:'none',overflow:'hidden',
                  fontSize:el.size||13,fontFamily:'inherit',lineHeight:1.55,color:textC,textAlign:el.align||'left',
                  width:'100%',minWidth:0,boxSizing:'border-box'}}/>
            : <div style={{flex:1,fontSize:el.size||13,lineHeight:1.55,whiteSpace:'pre-wrap',color:textC,overflowWrap:'break-word',textAlign:el.align||'left',overflow:'hidden'}}>
                {el.text||<span style={{opacity:0.35}}>Double-click to edit…</span>}
              </div>
          }
        </div>
      );
    }

    if (el.type==='text') {
      return (
        <div key={el.id}
          onMouseDown={e=>startDrag(e,el.id)}
          onMouseEnter={()=>setHoveredElId(el.id)}
          onMouseLeave={()=>setHoveredElId(v=>v===el.id?null:v)}
          onDoubleClick={()=>{ setEditingId(el.id); setSelected(new Set([el.id])); }}
          style={{...base,display:'flex',alignItems:'flex-start',minHeight:el.h||30,overflow:'visible'}}>
          {isEdit
            ? <input autoFocus defaultValue={el.text}
                onBlur={e2=>{patchAndPush(el.id,{text:e2.target.value});setEditingId(null);}}
                onMouseDown={e2=>e2.stopPropagation()}
                onKeyDown={e2=>{if(e2.key==='Enter'){patchAndPush(el.id,{text:e2.target.value});setEditingId(null);}e2.stopPropagation();}}
                style={{width:'100%',minWidth:0,boxSizing:'border-box',background:'transparent',border:0,outline:0,
                  fontSize:el.size||18,fontWeight:el.bold?700:400,fontStyle:el.italic?'italic':'normal',
                  textDecoration:el.strike?'line-through':'none',color:el.color||'#1A1A2E',
                  fontFamily:'inherit',textAlign:el.align||'left'}}/>
            : <div style={{fontSize:el.size||18,fontWeight:el.bold?700:400,fontStyle:el.italic?'italic':'normal',
                textDecoration:el.strike?'line-through':'none',color:el.color||'#1A1A2E',
                lineHeight:1.3,whiteSpace:'pre-wrap',textAlign:el.align||'left',width:'100%'}}>
                {el.text||<span style={{opacity:0.2,fontSize:(el.size||18)*0.7}}>Double-click to type…</span>}
              </div>
          }
        </div>
      );
    }

    if (el.type==='shape') {
      const noFill = !el.fill || el.fill==='none';
      const fillC  = noFill ? 'transparent' : el.fill;
      const strokeC = el.stroke || '#335CFF';
      const sw2 = 2.5;
      const op = el.opacity ?? 1;
      let inner;
      if (el.shape==='circle') {
        inner = <div style={{width:'100%',height:'100%',borderRadius:'50%',background:fillC,
          border:`${sw2}px solid ${strokeC}`,opacity:op,boxSizing:'border-box'}}/>;
      } else if (el.shape==='diamond') {
        const s=Math.min(el.w,el.h)*0.72;
        inner = <div style={{width:'100%',height:'100%',display:'grid',placeItems:'center',opacity:op}}>
          <svg width={s} height={s} viewBox="0 0 100 100">
            <polygon points="50,2 98,50 50,98 2,50"
              fill={fillC} stroke={strokeC} strokeWidth={sw2*(100/s)}/>
          </svg>
        </div>;
      } else if (el.shape==='triangle') {
        inner = <svg width={el.w} height={el.h} style={{display:'block',opacity:op}} viewBox={`0 0 ${el.w} ${el.h}`}>
          <polygon points={`${el.w/2},${sw2} ${el.w-sw2},${el.h-sw2} ${sw2},${el.h-sw2}`}
            fill={fillC} stroke={strokeC} strokeWidth={sw2}/>
        </svg>;
      } else {
        inner = <div style={{width:'100%',height:'100%',borderRadius:8,background:fillC,
          border:`${sw2}px solid ${strokeC}`,opacity:op,boxSizing:'border-box'}}/>;
      }
      return (
        <div key={el.id}
          onMouseDown={e=>startDrag(e,el.id)}
          onMouseEnter={()=>setHoveredElId(el.id)}
          onMouseLeave={()=>setHoveredElId(v=>v===el.id?null:v)}
          style={base}>{inner}</div>
      );
    }

    if (el.type==='image') {
      return (
        <div key={el.id}
          onMouseDown={e=>startDrag(e,el.id)}
          onMouseEnter={()=>setHoveredElId(el.id)}
          onMouseLeave={()=>setHoveredElId(v=>v===el.id?null:v)}
          style={{...base,borderRadius:6,overflow:'hidden',boxShadow:'0 2px 12px rgba(0,0,0,0.15)'}}>
          <img src={el.url} alt={el.name||''} style={{width:'100%',height:'100%',objectFit:'cover',display:'block'}}/>
        </div>
      );
    }

    if (el.type==='ad') {
      const ad = el.ad || {};
      const PLAT_COLOR = { meta:'#1877F2', tiktok:'#000', instagram:'#E1306C', youtube:'#FF0000' };
      return (
        <div key={el.id}
          onMouseDown={e=>startDrag(e,el.id)}
          onMouseEnter={()=>setHoveredElId(el.id)}
          onMouseLeave={()=>setHoveredElId(v=>v===el.id?null:v)}
          style={{...base, borderRadius:10, overflow:'hidden', background:'var(--bg-surface)',
            border:`1.5px solid ${isSel?'var(--accent)':'var(--border)'}`,
            boxShadow: isSel?'0 0 0 3px var(--accent-soft),0 4px 20px rgba(0,0,0,0.13)':'0 2px 12px rgba(0,0,0,0.10)',
            display:'flex', flexDirection:'column',
          }}>
          {/* Thumbnail */}
          <div style={{flex:1,overflow:'hidden',position:'relative',minHeight:0}}>
            <img src={ad.thumb} alt={ad.brand||''} style={{width:'100%',height:'100%',objectFit:'cover',display:'block'}}/>
            {/* Badges */}
            <div style={{position:'absolute',top:6,left:6,display:'flex',gap:4}}>
              {ad.platform&&(
                <span style={{fontSize:9,fontWeight:700,padding:'2px 6px',borderRadius:999,
                  background: PLAT_COLOR[ad.platform]||'#333', color:'#fff', textTransform:'capitalize'}}>
                  {ad.platform}
                </span>
              )}
              {ad.type&&(
                <span style={{fontSize:9,fontWeight:600,padding:'2px 6px',borderRadius:999,
                  background:'rgba(0,0,0,0.55)',color:'#fff'}}>
                  {ad.type}
                </span>
              )}
            </div>
            {ad.daysActive&&(
              <div style={{position:'absolute',bottom:6,right:6,fontSize:9,fontWeight:600,
                padding:'2px 6px',borderRadius:999,background:'rgba(0,0,0,0.55)',color:'#fff'}}>
                {ad.daysActive}d active
              </div>
            )}
          </div>
          {/* Footer */}
          <div style={{flexShrink:0,padding:'7px 9px 8px',borderTop:'1px solid var(--border)',background:'var(--bg-surface)'}}>
            <div style={{fontSize:11,fontWeight:700,color:'var(--text-1)',marginBottom:2,
              whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{ad.brand}</div>
            <div style={{fontSize:10,color:'var(--text-muted)',lineHeight:1.4,
              display:'-webkit-box',WebkitLineClamp:2,WebkitBoxOrient:'vertical',overflow:'hidden'}}>
              {ad.headline||ad.primaryText}
            </div>
          </div>
        </div>
      );
    }

    return null;
  };

  // ── Resize handles ───────────────────────────────────────────────────────────
  const renderHandles = (el) => {
    if (!el.w||!el.h||el.type==='pen') return null;
    const hs=[
      {id:'nw',cx:0,cy:0,cur:'nw-resize'},{id:'n',cx:el.w/2,cy:0,cur:'n-resize'},{id:'ne',cx:el.w,cy:0,cur:'ne-resize'},
      {id:'e',cx:el.w,cy:el.h/2,cur:'e-resize'},{id:'se',cx:el.w,cy:el.h,cur:'se-resize'},
      {id:'s',cx:el.w/2,cy:el.h,cur:'s-resize'},{id:'sw',cx:0,cy:el.h,cur:'sw-resize'},{id:'w',cx:0,cy:el.h/2,cur:'w-resize'},
    ];
    const br=el.type==='sticky'?5:el.type==='frame'?14:4;
    return (
      <React.Fragment key={`h-${el.id}`}>
        <div style={{position:'absolute',left:el.x-2,top:el.y-2,width:el.w+4,height:el.h+4,borderRadius:br+2,border:'2px solid var(--accent)',pointerEvents:'none',boxSizing:'border-box'}}/>
        {hs.map(h=>(
          <div key={h.id} onMouseDown={e=>startResize(e,el.id,h.id)}
            style={{position:'absolute',left:el.x+h.cx-5,top:el.y+h.cy-5,width:10,height:10,borderRadius:2,background:'#fff',border:'2px solid var(--accent)',cursor:h.cur,zIndex:200,boxSizing:'border-box'}}/>
        ))}
      </React.Fragment>
    );
  };

  // ── Port dots (rendered as absolutely positioned divs in canvas space) ───────
  // Always kept in DOM (opacity-only toggle) so the invisible hit area can fire
  // onMouseEnter even after the element's own onMouseLeave has cleared hoveredElId.
  const renderPortDots = (el) => {
    if (!el.w || !el.h) return null;
    if (tool==='pen'||tool==='shape'||tool==='frame'||tool==='sticky'||tool==='text') return null;
    // Hide ports on the selected element (resize handles take priority) unless
    // the user is actively dragging a connector — they may want to drop on it.
    if (selected.has(el.id) && !connecting) return null;
    const visible = hoveredElId===el.id || connecting!==null;
    return SIDES.map(side => {
      const p = portPos(el, side);
      const isFrom = connecting?.fromId===el.id && connecting?.fromPort===side;
      return (
        <div key={`port-${el.id}-${side}`}
          onMouseDown={e=>onPortMD(el.id, side, e)}
          onMouseUp={e=>onPortMU(el.id, side, e)}
          onMouseEnter={()=>setHoveredElId(el.id)}
          style={{
            position:'absolute', left:p.x-9, top:p.y-9,
            width:18, height:18, borderRadius:'50%',
            display:'flex', alignItems:'center', justifyContent:'center',
            cursor: visible ? 'crosshair' : 'default',
            zIndex:500,
            opacity: visible ? 1 : 0,
            transition:'opacity 80ms',
          }}>
          <div style={{
            width:11, height:11, borderRadius:'50%',
            background: isFrom ? 'var(--accent)' : '#fff',
            border:`2.5px solid var(--accent)`,
            boxShadow:'0 1px 5px rgba(0,0,0,0.22)',
            pointerEvents:'none',
          }}/>
        </div>
      );
    });
  };

  // ── Properties panel ─────────────────────────────────────────────────────────
  const renderProps = () => {
    if (!selEl) return null;
    const row = (label, child) => (
      <div key={label}>
        <div style={{fontSize:10,fontWeight:600,color:'var(--text-faint)',textTransform:'uppercase',letterSpacing:'0.06em',marginBottom:5}}>{label}</div>
        {child}
      </div>
    );
    const swatches = (palette, active, onPick) => (
      <div style={{display:'flex',gap:5,flexWrap:'wrap'}}>
        {palette.map(c=>(
          <button key={c} onClick={()=>onPick(c)} style={{width:24,height:24,borderRadius:999,padding:0,cursor:'pointer',background:c,border:c==='#FFFFFF'?'1px solid var(--border)':'1px solid rgba(0,0,0,0.08)',outline:active===c?'2.5px solid var(--accent)':'none',outlineOffset:2}}/>
        ))}
      </div>
    );
    const commit = () => pushHist(elementsRef.current, connectorsRef.current);
    const sections = [];
    if (selEl.type!=='pen'&&selEl.w&&selEl.h) {
      sections.push(row('Layout',
        <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:5}}>
          {[['X','x'],['Y','y'],['W','w'],['H','h']].map(([lbl,k])=>(
            <div key={k}>
              <div style={{fontSize:10,color:'var(--text-faint)',marginBottom:2}}>{lbl}</div>
              <div className="fld" style={{height:26,borderRadius:6,overflow:'hidden'}}>
                <input type="number" value={Math.round(selEl[k]||0)}
                  onChange={e2=>patchEl(selEl.id,{[k]:parseFloat(e2.target.value)||0})}
                  onBlur={commit}
                  style={{fontSize:11,fontFamily:'var(--font-mono)',textAlign:'center',
                    width:'100%',minWidth:0,background:'transparent',border:0,outline:0}}/>
              </div>
            </div>
          ))}
        </div>
      ));
    }
    if (selEl.type==='sticky') {
      sections.push(row('Color', swatches(WB_STICKY_PALETTE,selEl.color,c=>patchAndPush(selEl.id,{color:c}))));
      sections.push(row('Font size',
        <WbSlider value={selEl.size||13} min={10} max={28} step={1} unit="px"
          onChange={v=>patchEl(selEl.id,{size:Math.round(v)})} onCommit={commit}/>
      ));
    }
    if (selEl.type==='text') {
      sections.push(row('Font size',
        <WbSlider value={selEl.size||18} min={10} max={80} step={1} unit="px"
          onChange={v=>patchEl(selEl.id,{size:Math.round(v)})} onCommit={commit}/>
      ));
      sections.push(row('Style',
        <button onClick={()=>patchAndPush(selEl.id,{bold:!selEl.bold})}
          className="btn btn-stroke btn-sm"
          style={{width:'100%',fontWeight:700,justifyContent:'center',
            background:selEl.bold?'var(--accent-soft)':undefined,
            color:selEl.bold?'var(--accent)':undefined}}>Bold</button>
      ));
      sections.push(row('Color', swatches(WB_TEXT_PALETTE,selEl.color,c=>patchAndPush(selEl.id,{color:c}))));
    }
    if (selEl.type==='shape') {
      sections.push(row('Fill', swatches(WB_SHAPE_PALETTE,selEl.fill,c=>patchAndPush(selEl.id,{fill:c}))));
      sections.push(row('Opacity',
        <WbSlider value={Math.round((selEl.opacity??1)*100)} min={5} max={100} step={5} unit="%"
          onChange={v=>patchEl(selEl.id,{opacity:v/100})} onCommit={commit}/>
      ));
      sections.push(row('Shape type',
        <div style={{display:'flex',gap:4}}>
          {[['rect','Rect'],['circle','Circle'],['diamond','Diam.'],['triangle','Tri.']].map(([v,lbl])=>(
            <button key={v} onClick={()=>patchAndPush(selEl.id,{shape:v})}
              className="btn btn-stroke btn-sm"
              style={{flex:1,fontSize:9,padding:'0 3px',justifyContent:'center',
                background:selEl.shape===v?'var(--accent-soft)':undefined,
                color:selEl.shape===v?'var(--accent)':undefined}}>{lbl}</button>
          ))}
        </div>
      ));
    }
    if (selEl.type==='frame') {
      sections.push(row('Frame color', swatches(WB_FRAME_PALETTE,selEl.color,c=>patchAndPush(selEl.id,{color:c}))));
    }
    if (selEl.type==='pen') {
      sections.push(row('Stroke color', swatches(WB_PEN_PALETTE,selEl.color,c=>patchAndPush(selEl.id,{color:c}))));
      sections.push(row('Stroke width',
        <WbSlider value={selEl.width||2} min={1} max={16} step={0.5} unit="px"
          onChange={v=>patchEl(selEl.id,{width:v})} onCommit={commit}/>
      ));
    }
    return sections;
  };

  // ── Viewport cursor ──────────────────────────────────────────────────────────
  const vpCursor = panning?'grabbing':tool==='grab'?'grab':(tool==='shape'||tool==='frame'||tool==='pen')?'crosshair':'default';

  // ── Main render ──────────────────────────────────────────────────────────────
  return (
    <div style={{display:'flex',width:'100%',height:'100%',overflow:'hidden',position:'relative',background:'var(--bg-app)',fontFamily:'inherit',userSelect:'none'}}>

      {/* ── Top bar ─────────────────────────────────────────────────────────── */}
      <div style={{position:'absolute',top:10,left:'50%',transform:'translateX(-50%)',display:'flex',gap:3,alignItems:'center',background:'var(--bg-surface)',border:'1px solid var(--border)',borderRadius:12,padding:'4px 6px',boxShadow:'0 2px 14px rgba(0,0,0,0.09)',zIndex:60}}>
        <button onClick={()=>undoFn.current()} title="Undo ⌘Z" className="btn btn-ghost btn-icon" style={{width:30,height:30}}><Icon name="undo" size={14}/></button>
        <button onClick={()=>redoFn.current()} title="Redo ⌘⇧Z" className="btn btn-ghost btn-icon" style={{width:30,height:30}}><Icon name="redo" size={14}/></button>
        <div style={{width:1,height:18,background:'var(--border)',margin:'0 3px'}}/>
        <button onClick={()=>setZoom(z=>Math.max(0.08,z/1.2))} className="btn btn-ghost btn-icon" style={{width:30,height:30}}><Icon name="minus" size={13}/></button>
        <button onClick={()=>{setZoom(1);setPan({x:0,y:0});}} className="btn btn-ghost" style={{height:30,minWidth:54,fontSize:12,fontFamily:'var(--font-mono)',padding:'0 8px'}}>{Math.round(zoom*100)}%</button>
        <button onClick={()=>setZoom(z=>Math.min(5,z*1.2))} className="btn btn-ghost btn-icon" style={{width:30,height:30}}><Icon name="plus" size={13}/></button>
        <button onClick={zoomFit} title="Fit" className="btn btn-ghost btn-icon" style={{width:30,height:30}}><Icon name="maximize" size={13}/></button>
        <div style={{width:1,height:18,background:'var(--border)',margin:'0 3px'}}/>
        <span style={{fontSize:11,color:'var(--text-faint)',paddingRight:4}}>{elements.length} items</span>
      </div>

      {/* ── Left toolbar ────────────────────────────────────────────────────── */}
      <div style={{position:'absolute',left:10,top:'50%',transform:'translateY(-50%)',display:'flex',flexDirection:'column',gap:2,background:'var(--bg-surface)',border:'1px solid var(--border)',borderRadius:14,padding:5,boxShadow:'0 2px 14px rgba(0,0,0,0.09)',zIndex:60}}>
        {[
          {id:'grab',   icon:'hand',        title:'Hand  H',      sep:false},
          {id:'select', icon:'cursor',      title:'Select  V',    sep:true },
          {id:'sticky', icon:'sticky',      title:'Sticky  S',    sep:false},
          {id:'text',   icon:'text-tool',   title:'Text  T',      sep:false},
          {id:'shape',  icon:'shape-rect',  title:'Shape  R',     sep:false, sub:true},
          {id:'frame',  icon:'frame',       title:'Frame  F',     sep:false},
          {id:'pen',    icon:'pen',         title:'Pen  P',       sep:true },
          {id:'image',  icon:'image',       title:'Insert image', sep:false},
          {id:'__sep__',icon:null,          title:'',             sep:false, divider:true},
          {id:'ads',    icon:'bookmark',    title:'Saved Ads library', sep:false},
        ].map(item=>{
          if (item.divider) return <div key="div-ads" style={{height:1,background:'var(--border)',margin:'2px 0'}}/>;
          const active = item.id==='ads' ? showAdsPanel : tool===item.id;
          const btn=(
            <button key={item.id} title={item.title}
              onClick={()=>{
                if(item.id==='image'){imgInputRef.current?.click();return;}
                if(item.id==='ads'){setShowAdsPanel(v=>!v);return;}
                if(item.id==='shape') setShowShapePicker(v=>!v); else setShowShapePicker(false);
                setTool(item.id); setConnecting(null);
              }}
              className="btn btn-ghost btn-icon"
              style={{width:36,height:36,borderRadius:9,position:'relative',background:active?'var(--accent-soft)':'transparent',color:active?'var(--accent)':'var(--text-2)',outline:active?'1.5px solid var(--accent)':'none',outlineOffset:1}}>
              <Icon name={item.icon} size={17}/>
              {item.sub&&<span style={{position:'absolute',bottom:3,right:3,fontSize:7,opacity:0.5}}>▾</span>}
            </button>
          );
          return item.sep
            ? <React.Fragment key={item.id}>{btn}<div style={{height:1,background:'var(--border)',margin:'2px 0'}}/></React.Fragment>
            : btn;
        })}
        <input ref={imgInputRef} type="file" accept="image/*" hidden onChange={e=>{
          const f=e.target.files?.[0];if(!f)return;
          const url=URL.createObjectURL(f);
          const img=new Image();
          img.onload=()=>{
            const vp=viewportRef.current?.getBoundingClientRect()||{width:900,height:600};
            const s=Math.min(360/img.width,280/img.height,1);
            const w=img.width*s,h=img.height*s;
            const p=toCanvas(vp.width/2,vp.height/2);
            addEl({id:wbId(),type:'image',x:p.x-w/2,y:p.y-h/2,w,h,url,name:f.name});
          };
          img.src=url;e.target.value='';
        }}/>
      </div>

      {/* Shape submenu */}
      {showShapePicker&&tool==='shape'&&(
        <div style={{position:'absolute',left:58,top:'50%',transform:'translateY(-50%)',background:'var(--bg-surface)',border:'1px solid var(--border)',borderRadius:12,padding:5,display:'flex',flexDirection:'column',gap:2,boxShadow:'0 4px 24px rgba(0,0,0,0.14)',zIndex:70}}>
          {[{id:'rect',icon:'shape-rect'},{id:'circle',icon:'circle-shape'},{id:'diamond',icon:'diamond-shape'},{id:'triangle',icon:'triangle-shape'}].map(s=>(
            <button key={s.id} title={s.id} onClick={()=>{setShapeType(s.id);setShowShapePicker(false);}}
              className="btn btn-ghost btn-icon"
              style={{width:36,height:36,borderRadius:8,background:shapeType===s.id?'var(--accent-soft)':'transparent',color:shapeType===s.id?'var(--accent)':'var(--text-2)'}}>
              <Icon name={s.icon} size={16}/>
            </button>
          ))}
        </div>
      )}

      {/* ── Tool color panel (sticky / pen) ─────────────────────────────── */}
      {(tool==='sticky'||tool==='pen')&&!selEl&&(
        <div onMouseDown={e=>e.stopPropagation()}
          style={{position:'absolute',left:58,top:'50%',transform:'translateY(-50%)',
            background:'var(--bg-surface)',border:'1px solid var(--border)',borderRadius:14,
            padding:10,display:'flex',flexDirection:'column',gap:8,
            boxShadow:'0 4px 24px rgba(0,0,0,0.12)',zIndex:70,minWidth:92}}>

          {tool==='sticky'&&<>
            <div style={{fontSize:9,fontWeight:700,color:'var(--text-faint)',textTransform:'uppercase',letterSpacing:'0.08em'}}>Color</div>
            <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:6}}>
              {WB_STICKY_PALETTE.map(c=>(
                <button key={c} onClick={()=>setStickyColor(c)}
                  style={{width:36,height:36,borderRadius:8,padding:0,cursor:'pointer',
                    background:c,border:c==='#FFFFFF'?'1px solid var(--border)':'1px solid rgba(0,0,0,0.06)',
                    outline:stickyColor===c?'2.5px solid var(--accent)':'none',outlineOffset:2}}/>
              ))}
            </div>
          </>}

          {tool==='pen'&&<>
            <div style={{fontSize:9,fontWeight:700,color:'var(--text-faint)',textTransform:'uppercase',letterSpacing:'0.08em'}}>Color</div>
            <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:6}}>
              {WB_PEN_PALETTE.map(c=>(
                <button key={c} onClick={()=>setPenColor(c)}
                  style={{width:36,height:36,borderRadius:8,padding:0,cursor:'pointer',
                    background:c,border:c==='#FFFFFF'?'1px solid var(--border)':'1px solid rgba(0,0,0,0.06)',
                    outline:penColor===c?'2.5px solid var(--accent)':'none',outlineOffset:2}}/>
              ))}
            </div>
            <div style={{fontSize:9,fontWeight:700,color:'var(--text-faint)',textTransform:'uppercase',letterSpacing:'0.08em',marginTop:2}}>Thickness</div>
            <div style={{display:'flex',alignItems:'center',gap:6}}>
              <input type="range" min={1} max={14} step={0.5} value={penWidth}
                onChange={e=>setPenWidth(parseFloat(e.target.value))}
                style={{flex:1,height:4,WebkitAppearance:'none',appearance:'none',border:'none',cursor:'pointer',
                  background:`linear-gradient(to right,var(--accent) ${((penWidth-1)/13)*100}%,var(--border) ${((penWidth-1)/13)*100}%)`,
                  borderRadius:999,outline:'none'}}/>
              <span style={{fontSize:10,fontWeight:600,fontFamily:'var(--font-mono)',color:'var(--text-2)',minWidth:24,textAlign:'right'}}>{penWidth%1===0?penWidth:penWidth.toFixed(1)}</span>
            </div>
          </>}
        </div>
      )}

      {/* ── Viewport ────────────────────────────────────────────────────────── */}
      <div
        ref={viewportRef}
        onMouseDown={onCanvasMD}
        onMouseMove={e=>{
          if (connecting) {
            const rect=viewportRef.current?.getBoundingClientRect()||{left:0,top:0};
            setConnMouse({x:e.clientX-rect.left,y:e.clientY-rect.top});
          }
        }}
        onMouseUp={()=>{ /* port mouseup fires first via stopPropagation; if we reach here, cancel */ }}
        onContextMenu={e=>{e.preventDefault();setConnecting(null);setTool(t=>t==='grab'?'select':'grab');}}
        style={{flex:1,position:'relative',overflow:'hidden',cursor:vpCursor}}>

        {/* Dot grid */}
        <div style={{position:'absolute',inset:0,pointerEvents:'none',backgroundImage:`radial-gradient(circle, var(--border) 1px, transparent 1px)`,backgroundSize:`${22*zoom}px ${22*zoom}px`,backgroundPosition:`${pan.x%(22*zoom)}px ${pan.y%(22*zoom)}px`,opacity:zoom<0.25?0.4:1}}/>

        {/* ── Saved Ads panel ─────────────────────────────────────────────── */}
        {showAdsPanel && <WbAdsPanel onAdd={addAdToCanvas} onClose={()=>setShowAdsPanel(false)}/>}

        {/* Canvas transform root */}
        <div style={{position:'absolute',left:0,top:0,transform:`translate(${pan.x}px,${pan.y}px) scale(${zoom})`,transformOrigin:'0 0',width:0,height:0}}>

          {/* ── SVG: connectors, pen strokes (with hit targets), guides, previews ── */}
          <svg style={{position:'absolute',left:0,top:0,overflow:'visible',zIndex:5}}>

            {/* Connectors */}
            {renderConnectors()}

            {/* In-progress connector */}
            {connecting&&(()=>{
              const fromEl=elementsRef.current.find(e=>e.id===connecting.fromId);
              if(!fromEl||!fromEl.w) return null;
              const A=portPos(fromEl,connecting.fromPort);
              const B={x:(connMouse.x-pan.x)/zoom,y:(connMouse.y-pan.y)/zoom};
              const dist=Math.max(50,Math.sqrt((B.x-A.x)**2+(B.y-A.y)**2)*0.35);
              const dA=portDir(connecting.fromPort);
              const cp1={x:A.x+dA.dx*dist,y:A.y+dA.dy*dist};
              const d=`M ${A.x} ${A.y} C ${cp1.x} ${cp1.y}, ${B.x} ${B.y}, ${B.x} ${B.y}`;
              return (
                <g>
                  <path d={d} stroke="var(--accent)" strokeWidth={2} fill="none" strokeDasharray="6,4" strokeLinecap="round"/>
                  <circle cx={A.x} cy={A.y} r={5} fill="var(--accent)"/>
                  <circle cx={B.x} cy={B.y} r={4} fill="var(--accent)" opacity={0.45}/>
                </g>
              );
            })()}

            {/* Pen strokes — hit target (wide transparent) + visible stroke */}
            {elements.filter(e=>e.type==='pen').map(e=>{
              if(!e.points||e.points.length<2) return null;
              const d=e.points.reduce((acc,p,i)=>i===0?`M ${p.x} ${p.y}`:`${acc} L ${p.x} ${p.y}`,'');
              const isSel=selected.has(e.id);
              return (
                <g key={e.id}>
                  {/* Wide invisible hit target */}
                  <path d={d} stroke="transparent" strokeWidth={Math.max(e.width||2,14)} fill="none"
                    strokeLinecap="round"
                    style={{pointerEvents:'visibleStroke',cursor:'pointer'}}
                    onMouseDown={ev=>{
                      ev.stopPropagation();
                      if(!ev.shiftKey) setSelected(new Set([e.id]));
                      else setSelected(s=>{const n=new Set(s);n.has(e.id)?n.delete(e.id):n.add(e.id);return n;});
                      startDrag(ev,e.id);
                    }}
                  />
                  {/* Visible stroke */}
                  <path d={d} stroke={isSel?'var(--accent)':(e.color||'#1A1A2E')}
                    strokeWidth={isSel?(e.width||2)+1.5:(e.width||2)}
                    fill="none" strokeLinecap="round" strokeLinejoin="round"
                    style={{pointerEvents:'none'}}/>
                  {/* Selection bounds indicator */}
                  {isSel&&(()=>{const bb=penBBox(e);return(<rect x={bb.x-4} y={bb.y-4} width={bb.w+8} height={bb.h+8} fill="none" stroke="var(--accent)" strokeWidth={1} strokeDasharray="4,3" rx={3} style={{pointerEvents:'none'}}/>);})()}
                </g>
              );
            })}

            {/* Snap guides */}
            {guides.x!==null&&<line x1={guides.x} y1={-9999} x2={guides.x} y2={9999} stroke="var(--accent)" strokeWidth={1} opacity={0.45} strokeDasharray="5,4" style={{pointerEvents:'none'}}/>}
            {guides.y!==null&&<line x1={-9999} y1={guides.y} x2={9999} y2={guides.y} stroke="var(--accent)" strokeWidth={1} opacity={0.45} strokeDasharray="5,4" style={{pointerEvents:'none'}}/>}

            {/* Box selection */}
            {boxSel&&<rect x={Math.min(boxSel.x1,boxSel.x2)} y={Math.min(boxSel.y1,boxSel.y2)} width={Math.abs(boxSel.x2-boxSel.x1)} height={Math.abs(boxSel.y2-boxSel.y1)} fill="rgba(51,92,255,0.07)" stroke="var(--accent)" strokeWidth={1.5} rx={4} strokeDasharray="5,3" style={{pointerEvents:'none'}}/>}

            {/* Draw preview */}
            {drawing&&drawing.w>4&&drawing.h>4&&<rect x={drawing.x} y={drawing.y} width={drawing.w} height={drawing.h} fill={drawing.kind==='frame'?'rgba(51,92,255,0.05)':`${shapeColor}28`} stroke={drawing.kind==='frame'?'#335CFF':shapeColor} strokeWidth={2} rx={drawing.kind==='frame'?10:6} strokeDasharray="6,4" style={{pointerEvents:'none'}}/>}
          </svg>

          {/* Frames (behind) */}
          {elements.filter(e=>e.type==='frame').map(el=>renderEl(el))}

          {/* Non-frame, non-pen elements */}
          {elements.filter(e=>e.type!=='frame'&&e.type!=='pen').map(el=>renderEl(el))}

          {/* Selection handles */}
          {selEl&&selected.size===1&&renderHandles(selEl)}

          {/* Multi-select outlines */}
          {selected.size>1&&elements.filter(e=>selected.has(e.id)&&e.type!=='pen'&&e.w&&e.h).map(el=>(
            <div key={`ms-${el.id}`} style={{position:'absolute',left:el.x-2,top:el.y-2,width:el.w+4,height:el.h+4,border:'2px solid var(--accent)',borderRadius:el.type==='frame'?14:el.type==='sticky'?5:4,pointerEvents:'none',boxSizing:'border-box'}}/>
          ))}

          {/* Port dots — rendered last so they're above everything */}
          {elements.filter(e=>e.type!=='pen'&&e.w&&e.h).map(el=>renderPortDots(el))}
        </div>

        {/* Connection hint */}
        {connecting&&(
          <div style={{position:'absolute',bottom:20,left:'50%',transform:'translateX(-50%)',background:'var(--accent)',color:'#fff',borderRadius:8,padding:'7px 16px',fontSize:12,fontWeight:600,pointerEvents:'none',zIndex:80,letterSpacing:0.01}}>
            Hover another element and release on its port · Esc to cancel
          </div>
        )}

      </div>{/* ── end viewport ── */}

      {/* ── Floating connector toolbar ───────────────────────────────────── */}
      {selConn&&(()=>{
        const C = selConn;
        const fromEl = elementsRef.current.find(e=>e.id===C.fromId);
        const toEl   = elementsRef.current.find(e=>e.id===C.toId);
        if (!fromEl || !toEl) return null;
        const A = portPos(fromEl, C.fromPort||'right');
        const B = portPos(toEl,   C.toPort||'left');
        const {mid} = connBezier(A, C.fromPort, B, C.toPort);
        const rawVp  = toViewport(mid.x, mid.y);
        const vpRect = viewportRef.current?.getBoundingClientRect() || {left:0,top:0,right:1200,bottom:800};
        const winCX  = vpRect.left + rawVp.x;
        const winCY  = vpRect.top  + rawVp.y;
        const H      = 46; const HW_C = 200;
        const showBelowC = winCY < vpRect.top + H + 8;
        const rawTopC    = showBelowC ? winCY + 80 : winCY - H;
        const vp     = {
          x: Math.max(HW_C + 8, Math.min(window.innerWidth  - HW_C - 8, winCX)),
          y: Math.max(vpRect.top + 8, Math.min(window.innerHeight - H - 8, rawTopC)),
        };
          const lineStyle  = C.lineStyle  || 'solid';
          const endArrow   = C.endArrow   !== undefined ? C.endArrow   : 'arrow';
          const startArrow = C.startArrow || 'none';
          const sw         = C.strokeWidth || 2;
          const col        = C.color || '#94A3B8';
          const sep = <div style={{width:1,height:20,background:'var(--border)',flexShrink:0}}/>;
          const btnBase = (active) => ({
            height:28,minWidth:28,padding:'0 6px',borderRadius:6,border:'1px solid',cursor:'pointer',fontSize:13,fontWeight:700,
            display:'flex',alignItems:'center',justifyContent:'center',gap:3,flexShrink:0,
            background:active?'var(--accent-soft)':'transparent',
            color:active?'var(--accent)':'var(--text-2)',
            borderColor:active?'var(--accent)':'transparent',
          });
          return (
            <div onMouseDown={e=>e.stopPropagation()}
              style={{
                position:'fixed',
                left: vp.x,
                top:  vp.y,
                transform:'translateX(-50%)',
                zIndex:200,
                display:'flex',alignItems:'center',gap:3,
                background:'var(--bg-surface)',
                border:'1px solid var(--border)',
                borderRadius:10,
                padding:'4px 6px',
                boxShadow:'0 4px 20px rgba(0,0,0,0.15)',
                whiteSpace:'nowrap',
              }}>

              {/* Arrow direction */}
              {[
                {label:'→', sa:'none',  ea:'arrow', title:'End arrow'},
                {label:'←', sa:'arrow', ea:'none',  title:'Start arrow'},
                {label:'↔', sa:'arrow', ea:'arrow', title:'Both arrows'},
                {label:'—', sa:'none',  ea:'none',  title:'No arrows'},
              ].map(opt=>{
                const active = startArrow===opt.sa && endArrow===opt.ea;
                return (
                  <button key={opt.label} onClick={()=>patchConn(C.id,{startArrow:opt.sa,endArrow:opt.ea})}
                    title={opt.title} style={btnBase(active)}>{opt.label}</button>
                );
              })}

              {sep}

              {/* Line style */}
              {[
                {id:'solid',  title:'Solid',  svg:<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" strokeWidth="2"/>},
                {id:'dashed', title:'Dashed', svg:<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" strokeWidth="2" strokeDasharray="5,3"/>},
                {id:'dotted', title:'Dotted', svg:<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" strokeWidth="2.5" strokeDasharray="2,4" strokeLinecap="round"/>},
              ].map(opt=>{
                const active = lineStyle===opt.id;
                return (
                  <button key={opt.id} onClick={()=>patchConn(C.id,{lineStyle:opt.id})}
                    title={opt.title} style={{...btnBase(active),padding:'0 4px'}}>
                    <svg viewBox="0 0 24 12" width={24} height={12}>{opt.svg}</svg>
                  </button>
                );
              })}

              {sep}

              {/* Thickness mini-slider */}
              <div style={{display:'flex',alignItems:'center',gap:5,padding:'0 4px'}}>
                <svg width={13} height={13} viewBox="0 0 13 13" style={{color:'var(--text-2)',flexShrink:0}}>
                  <line x1="1" y1="6.5" x2="12" y2="6.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
                </svg>
                <input type="range" min={1} max={8} step={0.5} value={sw}
                  onChange={e=>patchConn(C.id,{strokeWidth:parseFloat(e.target.value)})}
                  style={{
                    width:64,height:4,WebkitAppearance:'none',appearance:'none',border:'none',cursor:'pointer',
                    background:`linear-gradient(to right,var(--accent) ${((sw-1)/7)*100}%,var(--border) ${((sw-1)/7)*100}%)`,
                    borderRadius:999,outline:'none',
                  }}/>
                <span style={{fontSize:10,fontWeight:600,fontFamily:'var(--font-mono)',color:'var(--text-2)',minWidth:20}}>{sw%1===0?sw:sw.toFixed(1)}</span>
              </div>

              {sep}

              {/* Color swatches */}
              <div style={{display:'flex',gap:3,alignItems:'center',padding:'0 2px'}}>
                {['#94A3B8','#1A1A2E','#335CFF','#10B981','#F59E0B','#E5484D','#7B5BFF','#EC4899'].map(c=>(
                  <button key={c} onClick={()=>patchConn(C.id,{color:c})}
                    style={{width:16,height:16,borderRadius:999,padding:0,cursor:'pointer',border:'1px solid rgba(0,0,0,0.1)',
                      background:c,outline:col===c?'2px solid var(--accent)':'none',outlineOffset:1.5,flexShrink:0}}/>
                ))}
              </div>

              {sep}

              {/* Label edit button */}
              <button onClick={()=>startConnLabelEdit(C.id, C.label||'')}
                title="Edit label"
                style={{...btnBase(!!C.label),padding:'0 6px',fontSize:11,gap:4}}>
                <Icon name="text-tool" size={11}/> Label
              </button>

              {/* Font size stepper — shown as soon as editing starts or label exists */}
              {(C.label||editingConnId===C.id)&&(
                <div style={{display:'flex',alignItems:'center',gap:1}}>
                  <button
                    onMouseDown={e=>{e.preventDefault();patchConn(C.id,{fontSize:Math.max(8,(C.fontSize||11)-1)});}}
                    title="Smaller" style={{...btnBase(false),minWidth:20,padding:'0 3px',fontSize:12}}>−</button>
                  <span style={{fontSize:10,fontWeight:600,fontFamily:'var(--font-mono)',color:'var(--text-2)',minWidth:22,textAlign:'center'}}>{C.fontSize||11}</span>
                  <button
                    onMouseDown={e=>{e.preventDefault();patchConn(C.id,{fontSize:Math.min(28,(C.fontSize||11)+1)});}}
                    title="Larger" style={{...btnBase(false),minWidth:20,padding:'0 3px',fontSize:12}}>+</button>
                </div>
              )}

              {sep}

              {/* Delete */}
              <button onClick={()=>deleteConn(C.id)} title="Delete connector"
                style={{...btnBase(false),color:'var(--danger)',borderColor:'transparent'}}>
                <Icon name="trash" size={13}/>
              </button>
            </div>
          );
        })()}

        {/* ── Floating element toolbar ─────────────────────────────────── */}
        {selEl&&(()=>{
          const bbox   = selEl.type==='pen' ? penBBox(selEl) : {x:selEl.x,y:selEl.y,w:selEl.w||0,h:selEl.h||0};
          const vpRect = viewportRef.current?.getBoundingClientRect() || {left:0,top:0,right:1200,bottom:800};
          const cTop   = toViewport(bbox.x+bbox.w/2, bbox.y);
          const cBot   = toViewport(bbox.x+bbox.w/2, bbox.y+bbox.h);
          const winX   = vpRect.left + cTop.x;
          const winTopY= vpRect.top  + cTop.y;
          const winBotY= vpRect.top  + cBot.y;
          const H = 46;
          const showBelow = winTopY < vpRect.top + H + 8;
          const rawTop    = showBelow ? winBotY + 10 : winTopY - H;
          const HW_E = 280; // half-width estimate for widest toolbar (shape)
          const vp = {
            x: Math.max(HW_E + 8, Math.min(window.innerWidth  - HW_E - 8, winX)),
            y: Math.max(vpRect.top + 8, Math.min(window.innerHeight - H - 8, rawTop)),
          };
          const commit = ()=>pushHist(elementsRef.current, connectorsRef.current);
          const sep = <div style={{width:1,height:20,background:'var(--border)',flexShrink:0}}/>;
          const btnSt = (active) => ({
            height:28,minWidth:28,padding:'0 6px',borderRadius:6,border:'1px solid',cursor:'pointer',fontSize:12,fontWeight:600,
            display:'flex',alignItems:'center',justifyContent:'center',gap:3,flexShrink:0,
            background:active?'var(--accent-soft)':'transparent',
            color:active?'var(--accent)':'var(--text-2)',
            borderColor:active?'var(--accent)':'transparent',
          });
          const sw = (c, active, fn) => (
            <button key={c} onClick={fn} style={{width:20,height:20,borderRadius:999,padding:0,cursor:'pointer',flexShrink:0,
              background:c,border:c==='#FFFFFF'?'1px solid var(--border)':'1px solid rgba(0,0,0,0.1)',
              outline:active?'2.5px solid var(--accent)':'none',outlineOffset:2}}/>
          );
          const slider = (value, min, max, step, unit, onChange) => {
            const pct=((value-min)/(max-min))*100;
            return (
              <div style={{display:'flex',alignItems:'center',gap:4,padding:'0 2px'}}>
                <input type="range" min={min} max={max} step={step} value={value}
                  onChange={e=>onChange(parseFloat(e.target.value))} onMouseUp={commit}
                  style={{width:60,height:4,WebkitAppearance:'none',appearance:'none',border:'none',cursor:'pointer',
                    background:`linear-gradient(to right,var(--accent) ${pct}%,var(--border) ${pct}%)`,
                    borderRadius:999,outline:'none'}}/>
                <span style={{fontSize:10,fontWeight:600,fontFamily:'var(--font-mono)',color:'var(--text-2)',minWidth:26,textAlign:'right'}}>
                  {step<1?value.toFixed(1):Math.round(value)}{unit}
                </span>
              </div>
            );
          };
          return (
            <div onMouseDown={e=>e.stopPropagation()}
              style={{position:'fixed',left:vp.x,top:vp.y,transform:'translateX(-50%)',
                zIndex:200,display:'flex',alignItems:'center',gap:4,flexWrap:'nowrap',
                background:'var(--bg-surface)',border:'1px solid var(--border)',borderRadius:10,
                padding:'5px 10px',boxShadow:'0 4px 20px rgba(0,0,0,0.15)',whiteSpace:'nowrap'}}>

              {/* sticky */}
              {selEl.type==='sticky'&&<>
                <div style={{display:'flex',gap:5,alignItems:'center'}}>
                  {WB_STICKY_PALETTE.map(c=>sw(c,selEl.color===c,()=>patchAndPush(selEl.id,{color:c})))}
                </div>
                {sep}
                {slider(selEl.size||13,10,28,1,'px',v=>patchEl(selEl.id,{size:Math.round(v)}))}
                {sep}
              </>}

              {/* text */}
              {selEl.type==='text'&&<>
                {slider(selEl.size||18,10,80,1,'px',v=>patchEl(selEl.id,{size:Math.round(v)}))}
                {sep}
                <button onClick={()=>patchAndPush(selEl.id,{bold:!selEl.bold})}       style={{...btnSt(!!selEl.bold),fontWeight:700,fontSize:13}}>B</button>
                <button onClick={()=>patchAndPush(selEl.id,{italic:!selEl.italic})}   style={{...btnSt(!!selEl.italic),fontStyle:'italic',fontSize:13}}>I</button>
                <button onClick={()=>patchAndPush(selEl.id,{strike:!selEl.strike})}   style={{...btnSt(!!selEl.strike),textDecoration:'line-through',fontSize:13}}>S</button>
                {sep}
                <div style={{display:'flex',gap:5,alignItems:'center'}}>
                  {WB_TEXT_PALETTE.map(c=>sw(c,selEl.color===c,()=>patchAndPush(selEl.id,{color:c})))}
                </div>
                {sep}
              </>}

              {/* shape */}
              {selEl.type==='shape'&&<>
                {/* stroke color */}
                <div style={{display:'flex',gap:5,alignItems:'center'}}>
                  {WB_SHAPE_PALETTE.map(c=>sw(c,(selEl.stroke||'#335CFF')===c,()=>patchAndPush(selEl.id,{stroke:c})))}
                </div>
                {sep}
                {/* fill: no-fill toggle + palette */}
                <button onClick={()=>patchAndPush(selEl.id,{fill:'none'})}
                  title="No fill"
                  style={{...btnSt(!selEl.fill||selEl.fill==='none'),width:20,height:20,borderRadius:999,padding:0,
                    border:'1.5px dashed var(--border)',fontSize:10,minWidth:20}}>∅</button>
                <div style={{display:'flex',gap:5,alignItems:'center'}}>
                  {WB_SHAPE_PALETTE.map(c=>sw(c,selEl.fill===c,()=>patchAndPush(selEl.id,{fill:c})))}
                </div>
                {sep}
                {/* shape type */}
                {[['rect','▭'],['circle','○'],['diamond','◇'],['triangle','△']].map(([v,lbl])=>(
                  <button key={v} onClick={()=>patchAndPush(selEl.id,{shape:v})} title={v}
                    style={{...btnSt((selEl.shape||'rect')===v),fontSize:14,minWidth:28}}>{lbl}</button>
                ))}
                {sep}
                {slider(Math.round((selEl.opacity??1)*100),5,100,5,'%',v=>patchEl(selEl.id,{opacity:v/100}))}
                {sep}
              </>}

              {/* frame */}
              {selEl.type==='frame'&&<>
                <input
                  defaultValue={selEl.title||'Frame'}
                  key={selEl.id}
                  onBlur={e=>patchAndPush(selEl.id,{title:e.target.value||'Frame'})}
                  onKeyDown={e=>{if(e.key==='Enter')e.target.blur();e.stopPropagation();}}
                  onMouseDown={e=>e.stopPropagation()}
                  placeholder="Frame name"
                  style={{height:26,padding:'0 8px',borderRadius:6,border:'1px solid var(--border)',
                    background:'var(--bg-soft)',color:'var(--text)',fontSize:12,outline:'none',
                    fontFamily:'inherit',width:110,minWidth:0}}/>
                {sep}
                <div style={{display:'flex',gap:5,alignItems:'center'}}>
                  {WB_FRAME_PALETTE.map(c=>sw(c,selEl.color===c,()=>patchAndPush(selEl.id,{color:c})))}
                </div>
                {sep}
              </>}

              {/* pen */}
              {selEl.type==='pen'&&<>
                <div style={{display:'flex',gap:5,alignItems:'center'}}>
                  {WB_PEN_PALETTE.map(c=>sw(c,selEl.color===c,()=>patchAndPush(selEl.id,{color:c})))}
                </div>
                {sep}
                {slider(selEl.width||2,1,16,0.5,'px',v=>patchEl(selEl.id,{width:v}))}
                {sep}
              </>}

              {/* duplicate + delete */}
              <button onClick={()=>{
                const copy={...selEl,id:wbId(),x:selEl.x+24,y:selEl.y+24,
                  ...(selEl.type==='pen'?{points:selEl.points.map(p=>({...p,x:p.x+24,y:p.y+24}))}:{})};
                const newEls=[...elementsRef.current,copy];
                setElements(newEls);pushHist(newEls,connectorsRef.current);setSelected(new Set([copy.id]));
              }} style={btnSt(false)} title="Duplicate ⌘D"><Icon name="copy" size={13}/></button>
              <button onClick={()=>{
                const newEls=elementsRef.current.filter(e=>e.id!==selEl.id);
                const newCons=connectorsRef.current.filter(c=>c.fromId!==selEl.id&&c.toId!==selEl.id);
                setElements(newEls);setConnectors(newCons);pushHist(newEls,newCons);setSelected(new Set());
              }} style={{...btnSt(false),color:'var(--danger)'}} title="Delete Del"><Icon name="trash" size={13}/></button>
            </div>
          );
        })()}

        {/* ── Floating multi-select toolbar ────────────────────────────── */}
        {!selEl&&selected.size>1&&(()=>{
          const ids=[...selected];
          const els=elements.filter(e=>ids.includes(e.id));
          const bboxes=els.map(e=>e.type==='pen'?penBBox(e):{x:e.x,y:e.y,w:e.w||0,h:e.h||0});
          const minX=Math.min(...bboxes.map(b=>b.x));
          const maxX=Math.max(...bboxes.map(b=>b.x+b.w));
          const minY=Math.min(...bboxes.map(b=>b.y));
          const maxY=Math.max(...bboxes.map(b=>b.y+b.h));
          const vpTop=toViewport((minX+maxX)/2,minY);
          const vpBot=toViewport((minX+maxX)/2,maxY);
          const msRect=viewportRef.current?.getBoundingClientRect()||{left:0,top:0,right:1200,bottom:800};
          const msWinX=msRect.left+vpTop.x;
          const msWinTopY=msRect.top+vpTop.y;
          const msWinBotY=msRect.top+vpBot.y;
          const msH=46;
          const showBelow=msWinTopY<msRect.top+msH+8;
          const rawTop=showBelow?msWinBotY+10:msWinTopY-msH;
          const msTop=Math.max(msRect.top+8,Math.min(window.innerHeight-msH-8,rawTop));
          const msLeft=Math.max(160+8,Math.min(window.innerWidth-160-8,msWinX));
          const btnSt=()=>({height:28,padding:'0 10px',borderRadius:6,border:'1px solid transparent',cursor:'pointer',fontSize:12,fontWeight:500,display:'flex',alignItems:'center',gap:5,background:'transparent',color:'var(--text-2)'});
          return (
            <div onMouseDown={e=>e.stopPropagation()}
              style={{position:'fixed',left:msLeft,top:msTop,transform:'translateX(-50%)',
                zIndex:200,display:'flex',alignItems:'center',gap:3,
                background:'var(--bg-surface)',border:'1px solid var(--border)',borderRadius:10,
                padding:'4px 8px',boxShadow:'0 4px 20px rgba(0,0,0,0.15)',whiteSpace:'nowrap'}}>
              <span style={{fontSize:12,color:'var(--text-2)',fontWeight:500,padding:'0 4px'}}>{selected.size} elements</span>
              <div style={{width:1,height:20,background:'var(--border)',flexShrink:0}}/>
              <button onClick={()=>{
                const copies=elementsRef.current.filter(e=>ids.includes(e.id)).map(e=>({...e,id:wbId(),x:e.x+24,y:e.y+24,...(e.type==='pen'?{points:e.points.map(p=>({...p,x:p.x+24,y:p.y+24}))}:{})}));
                const newEls=[...elementsRef.current,...copies];
                setElements(newEls);pushHist(newEls,connectorsRef.current);setSelected(new Set(copies.map(c=>c.id)));
              }} style={btnSt()}><Icon name="copy" size={13}/> Duplicate</button>
              <button onClick={()=>{
                const newEls=elementsRef.current.filter(e=>!selected.has(e.id));
                const newCons=connectorsRef.current.filter(c=>!selected.has(c.fromId)&&!selected.has(c.toId));
                setElements(newEls);setConnectors(newCons);pushHist(newEls,newCons);setSelected(new Set());
              }} style={{...btnSt(),color:'var(--danger)'}}><Icon name="trash" size={13}/> Delete</button>
            </div>
          );
        })()}

      {/* ── Minimap ──────────────────────────────────────────────────────────── */}
      {showMini&&miniData&&(
        <div style={{position:'absolute',bottom:12,right:12,zIndex:60,width:MINI_W,height:MINI_H,background:'var(--bg-surface)',border:'1px solid var(--border)',borderRadius:10,overflow:'hidden',boxShadow:'0 2px 14px rgba(0,0,0,0.10)'}}>
          <svg width={MINI_W} height={MINI_H}>
            {miniData.bboxes.map((bb,i)=>{
              const el=elements[i];
              const rx=bb.x*miniData.scale+miniData.ox,ry=bb.y*miniData.scale+miniData.oy;
              const rw=Math.max(2,bb.w*miniData.scale),rh=Math.max(2,bb.h*miniData.scale);
              const fill=el.type==='sticky'?(el.color||'#FFCB5C'):el.type==='shape'?(el.fill||'#335CFF'):el.type==='frame'?`${el.color||'#335CFF'}22`:el.type==='pen'?(el.color||'#1A1A2E'):'#94A3B8';
              return <rect key={el.id} x={rx} y={ry} width={rw} height={Math.max(1,rh)} rx={1} fill={fill} stroke={el.type==='frame'?(el.color||'#335CFF'):'none'} strokeWidth={0.5} opacity={0.85}/>;
            })}
            {(()=>{const vp=viewportRef.current?.getBoundingClientRect();if(!vp)return null;const vx=(-pan.x/zoom)*miniData.scale+miniData.ox,vy=(-pan.y/zoom)*miniData.scale+miniData.oy,vw=(vp.width/zoom)*miniData.scale,vh=(vp.height/zoom)*miniData.scale;return<rect x={vx} y={vy} width={vw} height={vh} fill="rgba(51,92,255,0.07)" stroke="var(--accent)" strokeWidth={1.5} rx={2}/>;})()}
          </svg>
          <button onClick={()=>setShowMini(false)} style={{position:'absolute',top:3,right:3,width:16,height:16,border:0,background:'var(--bg-soft)',borderRadius:4,cursor:'pointer',display:'grid',placeItems:'center',color:'var(--text-faint)',fontSize:10}}>×</button>
        </div>
      )}
      {!showMini&&(
        <button onClick={()=>setShowMini(true)} title="Minimap" style={{position:'absolute',bottom:12,right:12,zIndex:60,width:30,height:30,border:'1px solid var(--border)',background:'var(--bg-surface)',borderRadius:8,cursor:'pointer',display:'grid',placeItems:'center',color:'var(--text-faint)',boxShadow:'0 2px 8px rgba(0,0,0,0.08)'}}>
          <Icon name="map" size={13}/>
        </button>
      )}
    </div>
  );
};

Object.assign(window, { WhiteboardV1 });
