// v-map.jsx — Map Campaigns: real Milton Keynes map, zoom/pan, estate
// intelligence, your Stadium MK restaurant, live Lasso drivers + rival nodes.

const MAP_LAYERS = [
  { id:'heatmap',    label:'Demand heatmap',     icon:I.droplet },
  { id:'density',    label:'Customer density',   icon:I.density },
  { id:'cuisine',    label:'Cuisine demand',     icon:I.pizza },
  { id:'competitor', label:'Competitor pressure',icon:I.swords },
  { id:'influence',  label:'Vendor influence',   icon:I.target },
];

const MAP_IMG_ASPECT = 1275 / 1233;   // native px of mk-map.png

// Heat blob descriptor for a given layer
function blobFor(e, layer) {
  if (layer==='heatmap')    return { v:e.opportunity/100, color:'255,150,60' };
  if (layer==='density')    return { v:e.customers/1240,  color:'90,170,255' };
  if (layer==='cuisine')    return { v:e.interest/402,    color:'255,140,40' };
  if (layer==='influence')  return { v:e.influence/45,    color:'90,220,140' };
  if (layer==='competitor') return { v: e.competitor==='High'?0.95:e.competitor==='Medium'?0.5:0.18, color:'255,90,70' };
  return null;
}

// ─── Hover insight popover (mini card attached to a node) ────────────
function NodePopover({ e, below }) {
  const s = NODE_STATE[e.state];
  const compTone = e.competitor==='High'?'#FF6B57':e.competitor==='Medium'?'#FFC15A':'#7FE3A0';
  return (
    <div style={{
      position:'absolute', left:'50%', bottom: below?'auto':'calc(100% + 13px)', top: below?'calc(100% + 13px)':'auto',
      width:218, transformOrigin:'center', animation:'v-popover 160ms var(--ease-out)',
      background:'rgba(22,15,11,0.94)', backdropFilter:'blur(8px)', border:'1px solid rgba(255,255,255,0.12)',
      borderRadius:13, padding:'12px 13px 11px', boxShadow:'0 14px 34px -8px rgba(0,0,0,0.65)', zIndex:60, pointerEvents:'none', color:'#F5EFE7',
    }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:8, marginBottom:9 }}>
        <span style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:14, letterSpacing:'-0.01em' }}>{e.name}</span>
        <span style={{ display:'inline-flex', alignItems:'center', gap:4, fontFamily:'var(--font-display)', fontWeight:700, fontSize:9.5, letterSpacing:'0.04em', textTransform:'uppercase', color:s.fill }}>
          <span style={{ width:7, height:7, borderRadius:'50%', background:s.fill }}/>{s.label}
        </span>
      </div>
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'9px 10px' }}>
        {[
          ['Opportunity', e.opportunity, '#FFB061'],
          ['Your influence', e.influence+'%', s.fill],
          ['Lasso customers', e.customers.toLocaleString(), '#8FC4FF'],
          ['Want your food', e.interest, '#FFB061'],
        ].map(([l,v,c])=>(
          <div key={l}>
            <div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:16, color:c, lineHeight:1 }}>{v}</div>
            <div style={{ fontFamily:'var(--font-body)', fontSize:9.5, color:'#A89B8E', marginTop:2 }}>{l}</div>
          </div>
        ))}
      </div>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginTop:11, paddingTop:9, borderTop:'1px solid rgba(255,255,255,0.10)' }}>
        <span style={{ display:'inline-flex', alignItems:'center', gap:5, fontFamily:'var(--font-body)', fontSize:10.5, color:'#A89B8E' }}>
          <I.swords c={compTone} s={12}/> {e.competitor} rivalry
        </span>
        <span style={{ display:'inline-flex', alignItems:'center', gap:3, fontFamily:'var(--font-display)', fontWeight:800, fontSize:11, color: e.trendDir==='down'?'#FF8E78':'#7FE3A0' }}>
          {e.trendDir==='up'?<I.trendUp c="#7FE3A0" s={13}/>:e.trendDir==='down'?<I.trendDown c="#FF8E78" s={13}/>:<I.flat c="#A89B8E" s={13}/>}
          {e.trendDir!=='flat'?(e.trendDir==='down'?'−':'+')+e.trendPct+'%':'Flat'}
        </span>
      </div>
      <div style={{ marginTop:9, textAlign:'center', fontFamily:'var(--font-display)', fontWeight:700, fontSize:10, letterSpacing:'0.04em', textTransform:'uppercase', color:'#F2A35B' }}>Click for full insight →</div>
    </div>
  );
}

// ─── A single estate node (counter-scales with zoom → constant size) ──
function EstateNode({ e, selected, dim, invK, onClick, hasCampaign }) {
  const [hov, setHov] = React.useState(false);
  const s = NODE_STATE[e.state];
  const r = 16 + (e.customers/1240)*13;          // 16–29
  const size = r*2;
  const ringR = r + 5;
  const circ = 2*Math.PI*ringR;
  const off = circ*(1 - e.influence/100);
  const active = hov || selected;
  return (
    <div data-node style={{
      position:'absolute', left:`${e.x}%`, top:`${e.y}%`,
      transform:`translate(-50%,-50%) scale(${invK})`, transformOrigin:'center',
      zIndex: active ? 70 : 22, pointerEvents:'none',
    }}>
      <button data-node onClick={(ev)=>{ ev.stopPropagation(); onClick(e.id); }}
        onMouseEnter={()=>setHov(true)} onMouseLeave={()=>setHov(false)}
        style={{
          position:'relative', background:'none', border:'none', cursor:'pointer', padding:0, pointerEvents:'auto',
          opacity: dim ? 0.34 : 1, transition:'opacity 240ms, transform 200ms var(--ease-out)',
          transform:`scale(${active?1.1:1})`, filter: dim?'grayscale(0.55)':'none',
        }}>
        <div style={{ position:'relative', width:size+12, height:size+12, display:'flex', alignItems:'center', justifyContent:'center' }}>
          {(hasCampaign || e.state==='purple') && <span style={{ position:'absolute', inset:-2, borderRadius:'50%', border:'2px solid #8E6BD9', animation:'v-pulse 2s var(--ease-out) infinite' }}/>}
          {active && <span style={{ position:'absolute', inset:-6, borderRadius:'50%', boxShadow:`0 0 0 3px ${s.fill}, 0 0 22px 5px ${s.fill}`, opacity:0.9, animation:'v-glowpulse 1.6s ease-in-out infinite' }}/>}
          <svg width={size+12} height={size+12} style={{ position:'absolute', transform:'rotate(-90deg)' }}>
            <circle cx={(size+12)/2} cy={(size+12)/2} r={ringR} fill="none" stroke="rgba(255,255,255,0.16)" strokeWidth="3"/>
            <circle cx={(size+12)/2} cy={(size+12)/2} r={ringR} fill="none" stroke={s.fill} strokeWidth="3" strokeLinecap="round" strokeDasharray={circ} strokeDashoffset={off}/>
          </svg>
          <div style={{ width:size, height:size, borderRadius:'50%', background:s.fill, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', boxShadow:`0 4px 14px -2px ${s.fill}, inset 0 1px 0 rgba(255,255,255,0.25)`, color:'#fff', border:'2px solid rgba(255,255,255,0.85)' }}>
            <span style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize: r>25?14:12, lineHeight:1 }}>{e.customers>=1000?(e.customers/1000).toFixed(1)+'k':e.customers}</span>
            {e.trendDir!=='flat' && <span style={{ marginTop:1 }}>{e.trendDir==='up'?<I.trendUp c="#fff" s={10} w={2.6}/>:<I.trendDown c="#fff" s={10} w={2.6}/>}</span>}
          </div>
          {hasCampaign && <span style={{ position:'absolute', top:-2, right:-2, width:19, height:19, borderRadius:'50%', background:'#7A57C9', border:'2px solid #fff', display:'flex', alignItems:'center', justifyContent:'center', boxShadow:'0 2px 7px rgba(0,0,0,0.45)' }}><I.megaphone c="#fff" s={11}/></span>}
        </div>
        <div style={{ position:'absolute', top:size+11, left:'50%', transform:'translateX(-50%)', whiteSpace:'nowrap', background:'rgba(12,8,6,0.78)', backdropFilter:'blur(2px)', borderRadius:6, padding:'2px 7px', border:'1px solid rgba(255,255,255,0.10)' }}>
          <span style={{ fontFamily:'var(--font-display)', fontWeight:700, fontSize:10.5, color:'#F5EFE7' }}>{e.name}</span>
        </div>
        {hov && <NodePopover e={e} below={e.y<26}/>}
      </button>
    </div>
  );
}

// ─── Rival vendor node ───────────────────────────────────────────────
function RivalNode({ r, invK, dim }) {
  const [hov, setHov] = React.useState(false);
  const tone = r.pressure==='High'?'#E0492B':r.pressure==='Medium'?'#C9722A':'#7C675B';
  const sz = 30;
  return (
    <div data-node style={{ position:'absolute', left:`${r.x}%`, top:`${r.y}%`, transform:`translate(-50%,-50%) scale(${invK})`, transformOrigin:'center', zIndex: hov?69:18, opacity:dim?0.3:1, pointerEvents:'none', transition:'opacity 240ms' }}>
      <button data-node onMouseEnter={()=>setHov(true)} onMouseLeave={()=>setHov(false)}
        style={{ position:'relative', background:'none', border:'none', padding:0, cursor:'pointer', pointerEvents:'auto', transform:`scale(${hov?1.12:1})`, transition:'transform 180ms var(--ease-out)' }}>
        <div style={{ width:sz, height:sz, borderRadius:9, transform:'rotate(45deg)', background:'rgba(20,14,11,0.92)', border:`2px solid ${tone}`, boxShadow: hov?`0 0 16px 2px ${tone}`:'0 3px 9px rgba(0,0,0,0.5)', display:'flex', alignItems:'center', justifyContent:'center' }}>
          <span style={{ transform:'rotate(-45deg)', display:'flex' }}><I.swords c={tone} s={15}/></span>
        </div>
        {hov && (
          <div style={{ position:'absolute', left:'50%', bottom:'calc(100% + 12px)', transform:'translateX(-50%)', width:200, animation:'v-popover 160ms var(--ease-out)',
            background:'rgba(22,15,11,0.94)', backdropFilter:'blur(8px)', border:`1px solid ${tone}66`, borderRadius:12, padding:'11px 12px', boxShadow:'0 14px 32px -8px rgba(0,0,0,0.65)', pointerEvents:'none', zIndex:60 }}>
            <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:8 }}>
              <span style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:13.5, color:'#F5EFE7' }}>{r.name}</span>
              <span style={{ fontFamily:'var(--font-display)', fontWeight:800, fontSize:9, letterSpacing:'0.05em', textTransform:'uppercase', color:tone, border:`1px solid ${tone}66`, borderRadius:5, padding:'1px 5px' }}>Rival</span>
            </div>
            <div style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'#A89B8E', marginTop:2 }}>{r.cuisine}</div>
            <div style={{ display:'flex', alignItems:'center', gap:14, marginTop:9, paddingTop:9, borderTop:'1px solid rgba(255,255,255,0.10)' }}>
              <div><div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:15, color:'#fff' }}>{r.share}%</div><div style={{ fontFamily:'var(--font-body)', fontSize:9, color:'#A89B8E' }}>area share</div></div>
              <div><div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:15, color:tone }}>{r.pressure}</div><div style={{ fontFamily:'var(--font-body)', fontSize:9, color:'#A89B8E' }}>pressure</div></div>
            </div>
            <div style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'#CDBEAE', marginTop:9, lineHeight:1.45 }}>{r.note}</div>
            <div style={{ fontFamily:'var(--font-body)', fontSize:9, color:'#8A786B', marginTop:8, paddingTop:7, borderTop:'1px solid rgba(255,255,255,0.08)' }}>Saved competitor intel — not live monitoring</div>
          </div>
        )}
      </button>
    </div>
  );
}

// ─── Your restaurant — Stadium MK home base ──────────────────────────
function RestaurantNode({ invK }) {
  const [hov, setHov] = React.useState(false);
  return (
    <div data-node style={{ position:'absolute', left:`${HQ.x}%`, top:`${HQ.y}%`, transform:`translate(-50%,-50%) scale(${invK})`, transformOrigin:'center', zIndex: hov?72:40, pointerEvents:'none' }}>
      <button data-node onMouseEnter={()=>setHov(true)} onMouseLeave={()=>setHov(false)}
        style={{ position:'relative', background:'none', border:'none', padding:0, cursor:'pointer', pointerEvents:'auto', display:'flex', flexDirection:'column', alignItems:'center' }}>
        <span style={{ position:'absolute', top:-3, left:'50%', transform:'translate(-50%,-50%)', width:54, height:54, borderRadius:'50%', background:'rgba(242,140,27,0.30)', animation:'v-ping 2.8s var(--ease-out) infinite' }}/>
        <div style={{ position:'relative', width:48, height:48, borderRadius:'50%', background:'linear-gradient(135deg,#F8A640,#D85A14)', border:'3px solid #fff', boxShadow:'0 6px 18px -3px rgba(216,90,20,0.8), 0 0 0 2px rgba(216,90,20,0.4)', display:'flex', alignItems:'center', justifyContent:'center' }}>
          <I.flame c="#fff" s={24} f/>
        </div>
        <div style={{ marginTop:8, whiteSpace:'nowrap', background:'linear-gradient(135deg,#F28C1B,#D85A14)', borderRadius:8, padding:'3px 10px', boxShadow:'0 4px 12px -2px rgba(216,90,20,0.6)' }}>
          <span style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:11.5, color:'#fff', letterSpacing:'-0.01em' }}>Fire &amp; Dough</span>
        </div>
        <div style={{ marginTop:3, whiteSpace:'nowrap', fontFamily:'var(--font-display)', fontWeight:700, fontSize:9, letterSpacing:'0.06em', textTransform:'uppercase', color:'#F2A35B' }}>Stadium MK · your kitchen</div>
        {hov && (
          <div style={{ position:'absolute', left:'50%', bottom:'calc(100% + 10px)', transform:'translateX(-50%)', width:210, animation:'v-popover 160ms var(--ease-out)',
            background:'rgba(22,15,11,0.95)', backdropFilter:'blur(8px)', border:'1px solid rgba(242,140,27,0.5)', borderRadius:12, padding:'12px 13px', boxShadow:'0 14px 32px -8px rgba(0,0,0,0.65)', pointerEvents:'none' }}>
            <div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:14, color:'#F5EFE7' }}>Fire &amp; Dough</div>
            <div style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'#A89B8E', marginBottom:9 }}>{HQ.sub}</div>
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:8, paddingTop:9, borderTop:'1px solid rgba(255,255,255,0.10)' }}>
              {[[HQ.ordersToday,'orders'],[HQ.driversOut,'drivers out'],[HQ.avgDeliver,'avg time']].map(([v,l])=>(
                <div key={l}><div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:15, color:'#F8A640', lineHeight:1 }}>{v}</div><div style={{ fontFamily:'var(--font-body)', fontSize:9, color:'#A89B8E', marginTop:2 }}>{l}</div></div>
              ))}
            </div>
          </div>
        )}
      </button>
    </div>
  );
}

// ─── Live Lasso drivers (direct-DOM RAF, never re-renders React) ─────
function DriverLayer({ viewRef, baseRef }) {
  const layerRef = React.useRef(null);
  React.useEffect(()=>{
    const layer = layerRef.current; if(!layer) return;
    const hubs = [...ESTATES.map(e=>({x:e.x,y:e.y})), {x:HQ.x,y:HQ.y}];
    const HQpt = { x:HQ.x, y:HQ.y };
    const rnd = (a,b)=> a + Math.random()*(b-a);
    const drivers = [];
    const N = 13;
    for (let i=0;i<N;i++){
      const route = [];
      const deliver = i % 2 === 0;              // half radiate from the kitchen
      const stops = 3 + (i%3);
      for (let j=0;j<stops;j++){
        if (deliver && j%2===0) route.push({ ...HQpt });
        else { const h = hubs[(Math.floor(Math.random()*hubs.length))]; route.push({ x:h.x+rnd(-3,3), y:h.y+rnd(-3,3) }); }
      }
      const el = document.createElement('div');
      el.className = 'v-driver';
      el.innerHTML = '<span class="v-driver-ping"></span><span class="v-driver-core"></span>';
      layer.appendChild(el);
      drivers.push({ el, route, seg:Math.floor(Math.random()*route.length), t:Math.random(), speed:rnd(0.10,0.20) });
    }
    let raf, last=performance.now();
    const tick=(now)=>{
      const dt = Math.min((now-last)/1000, 0.05); last=now;
      const invK = 1 / (viewRef.current?.k || 1);
      for (const d of drivers){
        const a = d.route[d.seg], b = d.route[(d.seg+1)%d.route.length];
        const dist = Math.max(0.5, Math.hypot(b.x-a.x, b.y-a.y));
        d.t += (d.speed*8/dist)*dt;
        if (d.t>=1){ d.t=0; d.seg=(d.seg+1)%d.route.length; }
        const ease = d.t<0.5 ? 2*d.t*d.t : 1-Math.pow(-2*d.t+2,2)/2;
        const x = a.x+(b.x-a.x)*ease, y = a.y+(b.y-a.y)*ease;
        d.el.style.left = x+'%'; d.el.style.top = y+'%';
        d.el.style.transform = `translate(-50%,-50%) scale(${invK})`;
      }
      raf=requestAnimationFrame(tick);
    };
    raf=requestAnimationFrame(tick);
    return ()=>{ cancelAnimationFrame(raf); drivers.forEach(d=>d.el.remove()); };
  }, []);
  return <div ref={layerRef} style={{ position:'absolute', inset:0, pointerEvents:'none', zIndex:16 }}/>;
}

// ─── The map canvas (zoom / pan world) ───────────────────────────────
function MapCanvas({ app, layers, campaignOnly, showDrivers, showRivals, campaignAreas }) {
  const vpRef = React.useRef(null);
  const worldRef = React.useRef(null);
  const [base, setBase] = React.useState({ w:0, h:0 });   // fitted world px
  const [view, setView] = React.useState({ k:1, tx:0, ty:0 });
  const viewRef = React.useRef(view);  viewRef.current = view;
  const baseRef = React.useRef(base);  baseRef.current = base;
  const drag = React.useRef(null);
  const activeLayers = Object.keys(layers).filter(k=>layers[k]);
  const invK = 1/view.k;

  // Fit world to viewport (contain) + centre
  const fit = React.useCallback(()=>{
    const vp = vpRef.current; if(!vp) return;
    const W = vp.clientWidth, H = vp.clientHeight;
    let w, h;
    if (W/H > MAP_IMG_ASPECT) { h = H; w = H*MAP_IMG_ASPECT; }
    else { w = W; h = W/MAP_IMG_ASPECT; }
    setBase({ w, h });
    setView({ k:1, tx:(W-w)/2, ty:(H-h)/2 });
  }, []);
  React.useEffect(()=>{ fit(); const ro=new ResizeObserver(fit); if(vpRef.current) ro.observe(vpRef.current); return ()=>ro.disconnect(); }, [fit]);

  const clamp = (v, base)=>{
    const vp = vpRef.current; if(!vp) return v;
    const W=vp.clientWidth, H=vp.clientHeight;
    const cx = v.tx + base.w*v.k/2, cy = v.ty + base.h*v.k/2;
    let { tx, ty } = v;
    if (cx<0) tx -= cx; if (cx>W) tx -= (cx-W);
    if (cy<0) ty -= cy; if (cy>H) ty -= (cy-H);
    return { ...v, tx, ty };
  };

  // wheel zoom toward cursor (native listener so we can preventDefault)
  React.useEffect(()=>{
    const vp = vpRef.current; if(!vp) return;
    const onWheel=(ev)=>{
      ev.preventDefault();
      const rect = vp.getBoundingClientRect();
      const cx = ev.clientX-rect.left, cy = ev.clientY-rect.top;
      setView(v=>{
        const k2 = Math.min(5, Math.max(1, v.k * (ev.deltaY<0 ? 1.14 : 1/1.14)));
        const wx = (cx - v.tx)/v.k, wy = (cy - v.ty)/v.k;
        let nv = { k:k2, tx: cx - wx*k2, ty: cy - wy*k2 };
        return clamp(nv, baseRef.current);
      });
    };
    vp.addEventListener('wheel', onWheel, { passive:false });
    return ()=> vp.removeEventListener('wheel', onWheel);
  }, []);

  const onPointerDown=(ev)=>{
    if (ev.target.closest('[data-node]')) return;      // let nodes handle clicks
    drag.current = { x:ev.clientX, y:ev.clientY };
    ev.currentTarget.setPointerCapture(ev.pointerId);
  };
  const onPointerMove=(ev)=>{
    if(!drag.current) return;
    const dx=ev.clientX-drag.current.x, dy=ev.clientY-drag.current.y;
    drag.current={ x:ev.clientX, y:ev.clientY };
    setView(v=> clamp({ ...v, tx:v.tx+dx, ty:v.ty+dy }, baseRef.current));
  };
  const onPointerUp=()=>{ drag.current=null; };

  const zoomBy=(f)=> setView(v=>{
    const vp=vpRef.current; const W=vp.clientWidth, H=vp.clientHeight;
    const k2=Math.min(5,Math.max(1,v.k*f));
    const wx=(W/2-v.tx)/v.k, wy=(H/2-v.ty)/v.k;
    return clamp({ k:k2, tx:W/2-wx*k2, ty:H/2-wy*k2 }, baseRef.current);
  });

  return (
    <div ref={vpRef} onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerLeave={onPointerUp}
      onClick={(e)=>{ if(!e.target.closest('[data-node]')) app.setSelected(null); }}
      style={{ position:'relative', flex:1, minWidth:0, borderRadius:16, overflow:'hidden',
        background:'#081230', border:'1px solid var(--soft-line)', boxShadow:'var(--shadow-sm)',
        cursor: drag.current?'grabbing':'grab', touchAction:'none' }}>

      {/* WORLD — everything geographic lives here and shares the transform */}
      <div ref={worldRef} style={{
        position:'absolute', left:0, top:0, width:base.w, height:base.h,
        transform:`translate(${view.tx}px,${view.ty}px) scale(${view.k})`, transformOrigin:'0 0',
        willChange:'transform',
      }}>
        {/* the Milton Keynes vector basemap (crisp at any zoom) */}
        <MKVectorMap/>

        {/* Delivery radius from the kitchen (geographic — scales with map) */}
        <div style={{ position:'absolute', left:`${HQ.x}%`, top:`${HQ.y}%`, width:`${HQ.radius*2}%`, height:`${HQ.radius*2}%`, transform:'translate(-50%,-50%)',
          borderRadius:'50%', border:'1.5px dashed rgba(242,140,27,0.55)', background:'radial-gradient(circle, rgba(242,140,27,0.10) 0%, rgba(242,140,27,0) 70%)', pointerEvents:'none', zIndex:8 }}/>

        {/* Heat layers (geographic, glow over dark map) */}
        {activeLayers.map(lid=>(
          <div key={lid} style={{ position:'absolute', inset:0, pointerEvents:'none', mixBlendMode:'screen', opacity: activeLayers.length>1?0.72:0.92, zIndex:9 }}>
            {ESTATES.map(e=>{
              const b = blobFor(e, lid); if(!b || b.v<0.12) return null;
              const rad = 9 + b.v*15;
              return <div key={e.id} style={{ position:'absolute', left:`${e.x}%`, top:`${e.y}%`, width:`${rad*2}%`, height:`${rad*2}%`, transform:'translate(-50%,-50%)',
                background:`radial-gradient(circle, rgba(${b.color},${0.14+b.v*0.5}) 0%, rgba(${b.color},0) 66%)`, borderRadius:'50%' }}/>;
            })}
          </div>
        ))}

        {showDrivers && <DriverLayer viewRef={viewRef} baseRef={baseRef}/>}
        {showRivals && RIVALS.map(r=><RivalNode key={r.id} r={r} invK={invK} dim={campaignOnly}/>)}

        {ESTATES.map(e=>(
          <EstateNode key={e.id} e={e} selected={app.selected===e.id} hasCampaign={campaignAreas.has(e.name)} dim={campaignOnly && !campaignAreas.has(e.name)} invK={invK} onClick={app.setSelected}/>
        ))}

        <RestaurantNode invK={invK}/>
      </div>

      {/* Zoom controls (outside the scaled world) */}
      <div style={{ position:'absolute', left:14, bottom:14, display:'flex', flexDirection:'column', gap:6, zIndex:50 }}>
        <ZoomBtn onClick={()=>zoomBy(1.4)}><I.plus c="#F5EFE7" s={17}/></ZoomBtn>
        <ZoomBtn onClick={()=>zoomBy(1/1.4)}><span style={{ width:13, height:2.4, borderRadius:2, background:'#F5EFE7' }}/></ZoomBtn>
        <ZoomBtn onClick={fit}><I.target c="#F5EFE7" s={16}/></ZoomBtn>
      </div>

      {/* zoom readout + hint */}
      <div style={{ position:'absolute', left:60, bottom:16, display:'flex', alignItems:'center', gap:10, zIndex:50, pointerEvents:'none' }}>
        <span style={{ fontFamily:'var(--font-mono)', fontSize:11, color:'#F5EFE7', background:'rgba(12,8,6,0.6)', borderRadius:7, padding:'4px 9px', backdropFilter:'blur(4px)' }}>{view.k.toFixed(1)}×</span>
        <span style={{ fontFamily:'var(--font-body)', fontSize:11, color:'rgba(245,239,231,0.62)' }}>Scroll to zoom · drag to pan</span>
      </div>
    </div>
  );
}

function ZoomBtn({ children, onClick }) {
  return (
    <button onClick={(e)=>{ e.stopPropagation(); onClick(); }} style={{
      width:36, height:36, borderRadius:10, border:'1px solid rgba(255,255,255,0.16)', background:'rgba(16,11,8,0.74)', backdropFilter:'blur(6px)',
      display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', boxShadow:'0 3px 10px rgba(0,0,0,0.4)',
    }}>{children}</button>
  );
}

// ─── Legend (collapsible so it never covers nodes) ───────────────────
function MapLegend({ right=14 }) {
  const [open, setOpen] = React.useState(false);
  return (
    <div style={{ position:'absolute', bottom:14, right, zIndex:48, display:'flex', flexDirection:'column-reverse', alignItems:'flex-end', transition:'right 520ms var(--ease-out)' }}>
      <button onClick={(e)=>{ e.stopPropagation(); setOpen(o=>!o); }} style={{
        display:'flex', alignItems:'center', gap:7, padding:'7px 11px', borderRadius:10, cursor:'pointer',
        background:'rgba(16,11,8,0.82)', backdropFilter:'blur(8px)', border:'1px solid rgba(255,255,255,0.14)',
        boxShadow:'var(--shadow-md)', fontFamily:'var(--font-display)', fontWeight:700, fontSize:11.5, color:'#F5EFE7', marginLeft:'auto' }}>
        <I.info c="#F2A35B" s={14}/> Legend <I.chevD c="rgba(245,239,231,0.7)" s={13} style={{ transform: open?'rotate(180deg)':'none' }}/>
      </button>
      {open && (
        <div onClick={(e)=>e.stopPropagation()} style={{ marginBottom:8, background:'rgba(16,11,8,0.88)', backdropFilter:'blur(8px)', border:'1px solid rgba(255,255,255,0.12)', borderRadius:12, padding:'12px 14px', boxShadow:'var(--shadow-md)', width:236, animation:'v-popover 160ms var(--ease-out)' }}>
          <Eyebrow color="rgba(245,239,231,0.7)" style={{ marginBottom:8, fontSize:9.5 }}>Node state</Eyebrow>
          <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'5px 14px' }}>
            {Object.entries(NODE_STATE).map(([k,v])=>(
              <div key={k} style={{ display:'flex', alignItems:'center', gap:6 }}>
                <span style={{ width:9, height:9, borderRadius:'50%', background:v.fill, flexShrink:0 }}/>
                <span style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'#E9DFD3' }}>{v.label}</span>
              </div>
            ))}
          </div>
          <div style={{ marginTop:9, paddingTop:9, borderTop:'1px solid rgba(255,255,255,0.12)', display:'flex', flexDirection:'column', gap:7 }}>
            <div style={{ display:'flex', alignItems:'center', gap:8 }}>
              <span style={{ width:16, height:16, borderRadius:'50%', background:'linear-gradient(135deg,#F8A640,#D85A14)', border:'2px solid #fff', flexShrink:0 }}/>
              <span style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'#E9DFD3' }}>Your kitchen · Stadium MK</span>
            </div>
            <div style={{ display:'flex', alignItems:'center', gap:8 }}>
              <span style={{ width:13, height:13, borderRadius:3, transform:'rotate(45deg)', background:'rgba(20,14,11,0.92)', border:'2px solid #E0492B', flexShrink:0, marginLeft:1 }}/>
              <span title="Manually saved competitor intelligence — not automated market scraping" style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'#E9DFD3', marginLeft:2, cursor:'help' }}>Rival vendor · saved intel</span>
            </div>
            <div style={{ display:'flex', alignItems:'center', gap:8 }}>
              <span style={{ width:11, height:11, borderRadius:'50%', background:'radial-gradient(circle at 35% 30%,#FFC074,#E8650F)', border:'1.5px solid #FFF3E6', flexShrink:0, marginLeft:2 }}/>
              <span style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'#E9DFD3', marginLeft:1 }}>Live Lasso driver</span>
            </div>
          </div>
          <div style={{ marginTop:9, paddingTop:9, borderTop:'1px solid rgba(255,255,255,0.12)', display:'flex', alignItems:'center', gap:7 }}>
            <svg width="16" height="16" style={{ transform:'rotate(-90deg)' }}><circle cx="8" cy="8" r="6" fill="none" stroke="rgba(255,255,255,0.18)" strokeWidth="2.4"/><circle cx="8" cy="8" r="6" fill="none" stroke="var(--brand-orange)" strokeWidth="2.4" strokeDasharray="38" strokeDashoffset="13" strokeLinecap="round"/></svg>
            <span style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'#E9DFD3' }}>Ring = your influence %</span>
          </div>
        </div>
      )}
    </div>
  );
}

// ─── Intelligence panel (right) ──────────────────────────────────────
function StatRow({ label, value, sub, icon:Icon }) {
  return (
    <div style={{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', gap:12, padding:'9px 0', borderBottom:'1px solid var(--soft-line)' }}>
      <div style={{ display:'flex', alignItems:'center', gap:8 }}>
        {Icon && <Icon c="var(--warm-stone)" s={15}/>}
        <span style={{ fontFamily:'var(--font-body)', fontSize:12.5, color:'var(--warm-stone)' }}>{label}</span>
      </div>
      <div style={{ textAlign:'right' }}>
        <div style={{ fontFamily:'var(--font-display)', fontWeight:800, fontSize:13.5, color:'var(--fg-primary)' }}>{value}</div>
        {sub && <div style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'var(--warm-stone)' }}>{sub}</div>}
      </div>
    </div>
  );
}

function IntelPanel({ app, e }) {
  const s = NODE_STATE[e.state];
  const lvl = influenceLevel(e.influence);
  const est = estimateFor(e, 200);
  const compTone = e.competitor==='High'?'red':e.competitor==='Medium'?'amber':'green';
  const lvlPct = { Cold:12, Warming:38, Active:64, Stronghold:90 }[lvl];
  return (
    <div style={{ width:362, flexShrink:0, height:'100%', overflowY:'auto', background:'rgba(27,18,13,0.62)', backdropFilter:'blur(22px) saturate(140%)', WebkitBackdropFilter:'blur(22px) saturate(140%)', border:'1px solid rgba(255,255,255,0.12)', borderRadius:22, boxShadow:'0 24px 60px -18px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.08)' }}>
      <div style={{ position:'relative', minHeight:132, borderTopLeftRadius:22, borderTopRightRadius:22, overflow:'hidden', isolation:'isolate' }}>
        <image-slot id={`intel-cover-${e.id}`} fit="cover" radius="0" placeholder="Drop a cover photo" style={{ position:'absolute', inset:0, width:'100%', height:'100%', zIndex:0 }}></image-slot>
        <div style={{ position:'absolute', inset:0, zIndex:1, pointerEvents:'none', background:'linear-gradient(180deg, rgba(20,14,11,0.08) 0%, rgba(20,14,11,0.46) 54%, rgba(20,14,11,0.88) 100%)' }}></div>
        <button onClick={()=>app.setSelected(null)} style={{ position:'absolute', top:12, right:12, zIndex:3, width:30, height:30, borderRadius:9, border:'1px solid rgba(255,255,255,0.35)', background:'rgba(16,11,8,0.55)', backdropFilter:'blur(6px)', WebkitBackdropFilter:'blur(6px)', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer' }}><I.x c="#F5EFE7" s={15}/></button>
        <div style={{ position:'relative', zIndex:2, padding:'18px 20px 15px', marginTop:62, pointerEvents:'none' }}>
          <Eyebrow color="rgba(245,239,231,0.82)" style={{ marginBottom:6 }}>Area intelligence</Eyebrow>
          <h2 style={{ margin:0, fontFamily:'var(--font-display)', fontWeight:900, fontSize:24, letterSpacing:'-0.01em', color:'#FFFFFF', textShadow:'0 2px 14px rgba(0,0,0,0.55)' }}>{e.name}</h2>
          <div style={{ display:'flex', gap:7, marginTop:11, flexWrap:'wrap', pointerEvents:'auto' }}>
            <Pill tone={e.state==='grey'?'grey':e.state} dot>{s.label}</Pill>
            {e.campaign && <Pill tone="purple" icon={<I.play c="#6243ad" s={11}/>}>{e.campaign}</Pill>}
          </div>
        </div>
      </div>

      <div style={{ padding:'14px 20px 16px' }}>
        <div style={{ background:'rgba(250,246,240,0.95)', border:'1px solid rgba(255,255,255,0.5)', borderRadius:16, padding:'2px 14px 12px', boxShadow:'0 10px 26px -14px rgba(0,0,0,0.5)' }}>
        <div style={{ display:'flex', gap:12, margin:'14px 0 6px' }}>
          <div style={{ flex:1, background:s.soft, borderRadius:12, padding:'12px 13px' }}>
            <div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:24, color:'var(--fg-primary)', lineHeight:1 }}>{e.opportunity}</div>
            <div style={{ fontFamily:'var(--font-body)', fontSize:11, color:'var(--warm-stone)', marginTop:3 }}>Opportunity score</div>
          </div>
          <div style={{ flex:1, background:'var(--bg-app)', borderRadius:12, padding:'12px 13px' }}>
            <div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:24, color:'var(--fg-primary)', lineHeight:1 }}>{e.influence}%</div>
            <div style={{ fontFamily:'var(--font-body)', fontSize:11, color:'var(--warm-stone)', marginTop:3 }}>Your influence · {lvl}</div>
          </div>
        </div>

        <StatRow icon={I.users}  label="Active Lasso customers"  value={e.customers.toLocaleString()}/>
        <StatRow icon={I.pizza}  label="Interested in your cuisine" value={e.interest} sub={`${Math.round(e.interest/e.customers*100)}% of area`}/>
        <StatRow icon={I.tag}    label="Avg order value"         value={`£${e.aov[0]}–£${e.aov[1]}`}/>
        <StatRow icon={I.clock}  label="Peak order window"       value={e.peak}/>
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:12, padding:'9px 0', borderBottom:'1px solid var(--soft-line)' }}>
          <div style={{ display:'flex', alignItems:'center', gap:8 }}><I.trendUp c="var(--warm-stone)" s={15}/><span style={{ fontFamily:'var(--font-body)', fontSize:12.5, color:'var(--warm-stone)' }}>Recent demand trend</span></div>
          <div style={{ display:'flex', alignItems:'center', gap:8 }}><Spark data={e.demand} color={e.trendDir==='down'?'#C13B1F':'#2E8B4E'} h={22} w={56}/><Trend dir={e.trendDir} pct={e.trendPct}/></div>
        </div>
        <StatRow icon={I.swords} label="Competitor pressure" value={<Pill tone={compTone}>{e.competitor}</Pill>}/>

        <div style={{ padding:'13px 0 6px' }}>
          <div style={{ display:'flex', justifyContent:'space-between', marginBottom:7 }}>
            <span style={{ fontFamily:'var(--font-body)', fontSize:12.5, color:'var(--warm-stone)' }}>Vendor influence level</span>
            <span style={{ fontFamily:'var(--font-display)', fontWeight:800, fontSize:12.5, color:s.fill }}>{lvl}</span>
          </div>
          <Bar pct={lvlPct} color={s.fill} h={7}/>
          <div style={{ display:'flex', justifyContent:'space-between', marginTop:5 }}>
            {['Cold','Warming','Active','Stronghold'].map(t=>(
              <span key={t} style={{ fontFamily:'var(--font-body)', fontSize:9, color: t===lvl?s.fill:'var(--fg-muted)', fontWeight:t===lvl?700:400 }}>{t}</span>
            ))}
          </div>
        </div>
        </div>

        {(()=>{ const lc=ACTIVE_CAMPAIGNS.find(c=>(c.status==='live'||c.status==='ending')&&c.area===e.name); if(!lc) return null; const roas=(lc.revenue/lc.spend).toFixed(1); return (

          <div style={{ marginTop:8, background:'rgba(244,240,251,0.96)', border:'1px solid rgba(122,87,201,0.32)', borderRadius:16, padding:'14px 15px', boxShadow:'0 10px 26px -14px rgba(0,0,0,0.5)' }}>
            <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:10 }}>
              <Eyebrow><I.megaphone c="#6243ad" s={12}/> &nbsp;Campaign performance</Eyebrow>
              <Pill tone="purple" dot>{lc.status==='live'?'Live':'Ending'}</Pill>
            </div>
            <div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:14.5, color:'var(--fg-primary)', marginBottom:11 }}>{lc.name}</div>
            <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:'10px 12px', marginBottom:12 }}>
              {[['Revenue','£'+lc.revenue.toLocaleString()],['ROAS',roas+'×'],['Orders',lc.orders]].map(([l,v])=>(
                <div key={l}><div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:16, color:'var(--fg-primary)' }}>{v}</div><div style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'var(--warm-stone)' }}>{l}</div></div>
              ))}
            </div>
            <Btn kind="white" full size="sm" icon={<I.arrow c="var(--fg-primary)" s={15}/>} onClick={()=>app.openCampaign(lc.id)}>View campaign performance</Btn>
          </div>
        ); })()}

        <div style={{ marginTop:8, background:'rgba(250,246,240,0.95)', borderRadius:16, padding:'14px 15px', border:'1px solid rgba(255,255,255,0.5)', boxShadow:'0 10px 26px -14px rgba(0,0,0,0.5)' }}>
          <Eyebrow style={{ marginBottom:9 }}><I.zap c="var(--brand-orange)" s={12} f/> &nbsp;Recommended campaign</Eyebrow>
          <div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:15, color:'var(--fg-primary)', marginBottom:11 }}>{e.rec}</div>
          <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'10px 12px' }}>
            {[['Estimated reach', est.reach.toLocaleString()],['Estimated orders','+'+est.orders],['Estimated revenue','£'+est.revenue.toLocaleString()],['Campaign cost','£'+est.cost]].map(([l,v])=>(
              <div key={l}>
                <div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:16, color:'var(--fg-primary)' }}>{v}</div>
                <div style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'var(--warm-stone)' }}>{l}</div>
              </div>
            ))}
          </div>
          <div style={{ marginTop:12, display:'flex', alignItems:'center', justifyContent:'space-between', padding:'9px 12px', background:'rgba(46,139,78,0.10)', borderRadius:10 }}>
            <span style={{ fontFamily:'var(--font-body)', fontSize:12, color:'var(--horse-brown)' }}>Expected ROI range</span>
            <span style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:16, color:'#247a41' }}>{est.roiLow}–{est.roiHigh}×</span>
          </div>
          <Btn kind="accent" full size="md" style={{ marginTop:12 }} icon={<I.rocket c="#fff" s={16}/>} onClick={()=>app.openBuilder(e.id)}>Build campaign for {e.name}</Btn>
        </div>

        <div style={{ marginTop:13, display:'flex', gap:9, alignItems:'flex-start', padding:'11px 13px', background:'rgba(139,178,209,0.14)', border:'1px solid rgba(139,178,209,0.3)', borderRadius:12 }}>
          <I.lock c="#8FC4FF" s={15}/>
          <div style={{ fontFamily:'var(--font-body)', fontSize:11.5, color:'#A8C6DE', lineHeight:1.45 }}>
            <strong>Area-level insight only.</strong> Customer addresses are hidden. Lasso never shows individual pins or contact details.
          </div>
        </div>
      </div>
    </div>
  );
}

function IntelEmpty() {
  return (
    <div style={{ width:362, flexShrink:0, height:'100%', background:'rgba(27,18,13,0.62)', backdropFilter:'blur(22px) saturate(140%)', WebkitBackdropFilter:'blur(22px) saturate(140%)', border:'1px dashed rgba(255,255,255,0.22)', borderRadius:22, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', textAlign:'center', padding:30, gap:14 }}>
      <div style={{ width:56, height:56, borderRadius:16, background:'rgba(242,140,27,0.14)', display:'flex', alignItems:'center', justifyContent:'center' }}><I.pin c="var(--sun-orange)" s={26}/></div>
      <div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:17, color:'var(--dust-cream)' }}>Hover, then select an estate</div>
      <div style={{ fontFamily:'var(--font-body)', fontSize:13, color:'#B6A595', maxWidth:236, lineHeight:1.5 }}>Hover any estate node for a quick read; click it to pull full anonymised demand, competitor pressure and a recommended campaign here.</div>
      <div style={{ display:'flex', alignItems:'center', gap:7, marginTop:4 }}><I.lock c="#9F8979" s={13}/><span style={{ fontFamily:'var(--font-body)', fontSize:11, color:'#9F8979' }}>Addresses are never shown</span></div>
    </div>
  );
}

// ─── Leaflet live-tile map (crisp at every zoom, fully controllable) ──
function hexToRgb(h){ const n=parseInt(h.slice(1),16); return `${(n>>16)&255},${(n>>8)&255},${n&255}`; }

function buildEstateIcon(e, { selected, camp, dim }) {
  const s = NODE_STATE[e.state];
  const rgb = hexToRgb(s.fill);
  const r = Math.round(20 + (e.customers/1240)*11);
  const size = r*2, ringSize = size+10, rr = (ringSize-3)/2;
  const circ = 2*Math.PI*rr, off = circ*(1 - e.influence/100);
  const num = e.customers>=1000 ? (e.customers/1000).toFixed(1)+'k' : e.customers;
  const arrow = e.trendDir==='up' ? '▲' : e.trendDir==='down' ? '▼' : '';
  const mega = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11v2a1 1 0 0 0 1 1h2l5 4V6L6 10H4a1 1 0 0 0-1 1z"/><path d="M16 8a5 5 0 0 1 0 8"/></svg>';
  const html =
    `<div style="position:absolute;transform:translate(-50%,-50%);">`+
      `<div class="lf-node ${selected?'sel':''} ${dim?'dim':''}" style="--c:${s.fill};--crgb:${rgb};">`+
        `<div class="lf-bubblewrap" style="width:${size}px;height:${size}px;">`+
          (camp?`<span class="lf-camppulse"></span>`:``)+
          `<svg class="lf-ring" width="${ringSize}" height="${ringSize}"><circle cx="${ringSize/2}" cy="${ringSize/2}" r="${rr}" fill="none" stroke="rgba(255,255,255,0.18)" stroke-width="3"/><circle cx="${ringSize/2}" cy="${ringSize/2}" r="${rr}" fill="none" stroke="${s.fill}" stroke-width="3" stroke-linecap="round" stroke-dasharray="${circ}" stroke-dashoffset="${off}" style="filter:drop-shadow(0 0 4px rgba(${rgb},0.7))"/></svg>`+
          `<div class="lf-bubble" style="width:${size}px;height:${size}px;font-size:${r>26?14:12}px;">${num}${arrow?`<span style="font-size:9px;margin-top:1px;color:#fff;">${arrow}</span>`:``}</div>`+
          (camp?`<span class="lf-badge">${mega}</span>`:``)+
        `</div>`+
        `<div class="lf-label">${e.name}</div>`+
      `</div>`+
    `</div>`;
  return L.divIcon({ html, className:'lf-icon', iconSize:[0,0], iconAnchor:[0,0] });
}

function estateTooltip(e) {
  const s = NODE_STATE[e.state];
  const tr = e.trendDir==='up' ? `<span style="color:#7FE3A0">+${e.trendPct}%</span>` : e.trendDir==='down' ? `<span style="color:#FF8E78">−${e.trendPct}%</span>` : `<span style="color:#A89B8E">Flat</span>`;
  return `<div style="min-width:170px"><div style="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:7px"><b style="font-family:var(--font-display);font-size:13px">${e.name}</b><span style="font-size:9px;letter-spacing:.04em;text-transform:uppercase;color:${s.fill}">●&nbsp;${s.label}</span></div>`+
    `<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px 10px">`+
    `<div><div style="font-family:var(--font-display);font-weight:900;font-size:15px;color:#FFB061">${e.opportunity}</div><div style="font-size:9px;color:#A89B8E">Opportunity</div></div>`+
    `<div><div style="font-family:var(--font-display);font-weight:900;font-size:15px;color:${s.fill}">${e.influence}%</div><div style="font-size:9px;color:#A89B8E">Your influence</div></div>`+
    `<div><div style="font-family:var(--font-display);font-weight:900;font-size:15px;color:#8FC4FF">${e.customers.toLocaleString()}</div><div style="font-size:9px;color:#A89B8E">Customers</div></div>`+
    `<div><div style="font-family:var(--font-display);font-weight:900;font-size:15px;color:#FFB061">${e.interest}</div><div style="font-size:9px;color:#A89B8E">Want your food</div></div>`+
    `</div><div style="margin-top:8px;padding-top:7px;border-top:1px solid rgba(255,255,255,.12);display:flex;justify-content:space-between;font-size:10px;color:#A89B8E">${e.competitor} rivalry · 7d ${tr}</div>`+
    `<div style="margin-top:6px;text-align:center;font-size:9px;letter-spacing:.04em;text-transform:uppercase;color:#F2A35B">Click for full insight →</div></div>`;
}

function LeafletMap({ app, layers, campaignAreas, campaignOnly, showDrivers, showRivals, liveCount, area, setArea, navOffset=260, rightInset=16, fullscreen=false, onToggleFullscreen, active=true }) {
  const elRef = React.useRef(null);
  const mapRef = React.useRef(null);
  const est = React.useRef({});       // id -> marker
  const heat = React.useRef({});      // lid -> layerGroup
  const driverG = React.useRef(null);
  const rivalG = React.useRef(null);
  const flashRef = React.useRef(null);
  const estIdx = React.useRef(-1);
  const quadIdx = React.useRef(-1);
  const tourFlight = React.useRef(false);
  const rafRef = React.useRef(null);
  const pr = React.useRef({});
  pr.current = { app, campaignAreas, campaignOnly, setArea };

  // init once
  React.useEffect(()=>{
    if (!window.L || !elRef.current) return;
    const map = L.map(elRef.current, { zoomControl:false, attributionControl:true, minZoom:10, maxZoom:17, zoomSnap:0.25, wheelPxPerZoomLevel:120, touchZoom:true, dragging:true, bounceAtZoomLimits:true, inertia:true }).setView(MK_CENTER, 12.4);
    mapRef.current = map;
    L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
      subdomains:'abcd', maxZoom:19, attribution:'© OpenStreetMap © CARTO',
    }).addTo(map);
    L.control.zoom({ position:'bottomleft' }).addTo(map);

    // dark tooltip style
    const st = document.createElement('style');
    st.textContent = '.leaflet-tooltip.lf-tip{background:rgba(22,15,11,.95);border:1px solid rgba(255,255,255,.14);border-radius:11px;color:#F5EFE7;box-shadow:0 14px 34px -8px rgba(0,0,0,.65);padding:11px 12px;font-family:var(--font-body)}.leaflet-tooltip.lf-tip:before{display:none}';
    document.head.appendChild(st);

    // heat layers rendered into a blurred, low-opacity pane → soft readable heatmap
    map.createPane('heatPane');
    const hp = map.getPane('heatPane');
    hp.style.zIndex = 350; hp.style.pointerEvents = 'none'; hp.style.filter = 'blur(20px)'; hp.style.opacity = '0.7'; hp.style.mixBlendMode = 'screen';
    const heatRenderer = L.svg({ pane:'heatPane' });
    MAP_LAYERS.forEach(l=>{
      const g = L.layerGroup();
      ESTATES.forEach(e=>{
        const b = blobFor(e, l.id); if(!b || b.v<0.12) return;
        L.circle(ESTATE_LL[e.id], { radius: 900 + b.v*2700, stroke:false, fillColor:`rgb(${b.color})`, fillOpacity: 0.4, interactive:false, pane:'heatPane', renderer:heatRenderer }).addTo(g);
      });
      heat.current[l.id] = g;
    });

    // estate markers
    ESTATES.forEach(e=>{
      const camp = pr.current.campaignAreas.has(e.name);
      const m = L.marker(ESTATE_LL[e.id], { icon: buildEstateIcon(e,{ selected:false, camp, dim:false }), riseOnHover:true, zIndexOffset: 400 });
      m.bindTooltip(estateTooltip(e), { direction:'top', offset:[0,-22], opacity:1, className:'lf-tip', sticky:false });
      m.on('click', ()=>{ pr.current.setArea(null); pr.current.app.setSelected(e.id); });
      m.addTo(map); est.current[e.id] = m;
    });

    // HQ + delivery radius
    L.circle(HQ_LL, { radius: 5200, color:'#F28C1B', weight:1.5, dashArray:'5 5', fillColor:'#F28C1B', fillOpacity:0.06, interactive:false }).addTo(map);
    const hqIcon = L.divIcon({ className:'lf-icon', iconSize:[0,0], iconAnchor:[0,0], html:
      `<div style="position:absolute;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;">`+
      `<div class="lf-hq-dot"><svg width="24" height="24" viewBox="0 0 24 24" fill="#fff" stroke="#fff" stroke-width="2" stroke-linejoin="round"><path d="M12 2c1 5-3 5-3 9a3 3 0 0 0 6 0c0-1-1-2-1-3 2 1 4 3 4 6a6 6 0 1 1-12 0c0-4 4-6 6-12z"/></svg></div>`+
      `<div class="lf-hq-label">Fire &amp; Dough</div></div>` });
    L.marker(HQ_LL, { icon:hqIcon, zIndexOffset:600 }).bindTooltip(
      `<div style="min-width:150px"><b style="font-family:var(--font-display);font-size:13px">Fire &amp; Dough</b><div style="font-size:10px;color:#A89B8E;margin-bottom:7px">${HQ.sub}</div><div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">`+
      `<div><div style="font-family:var(--font-display);font-weight:900;font-size:14px;color:#F8A640">${HQ.ordersToday}</div><div style="font-size:9px;color:#A89B8E">orders</div></div>`+
      `<div><div style="font-family:var(--font-display);font-weight:900;font-size:14px;color:#F8A640">${HQ.driversOut}</div><div style="font-size:9px;color:#A89B8E">drivers</div></div>`+
      `<div><div style="font-family:var(--font-display);font-weight:900;font-size:14px;color:#F8A640">${HQ.avgDeliver}</div><div style="font-size:9px;color:#A89B8E">avg</div></div></div></div>`,
      { direction:'top', offset:[0,-30], opacity:1, className:'lf-tip' }).addTo(map);

    // rivals
    const rg = L.layerGroup();
    RIVALS.forEach(rv=>{
      const tone = rv.pressure==='High'?'#E0492B':rv.pressure==='Medium'?'#C9722A':'#7C675B';
      const ic = L.divIcon({ className:'lf-icon', iconSize:[0,0], iconAnchor:[0,0], html:
        `<div style="position:absolute;transform:translate(-50%,-50%);"><div class="lf-rival" style="border:2px solid ${tone}"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="${tone}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 14.5 21 21l-1 1-3 .5-.5-3-6.5-6.5M3 3l5 .5L8.5 8 14 13.5M3 3l.5 5L8 8.5"/><path d="m17.5 5.5 3-.5 1 1-6.5 6.5"/></svg></div></div>` });
      L.marker(RIVAL_LL[rv.id], { icon:ic, zIndexOffset:200 }).bindTooltip(
        `<div style="min-width:160px"><b style="font-family:var(--font-display);font-size:13px">${rv.name}</b><div style="font-size:10px;color:#A89B8E">${rv.cuisine}</div><div style="margin-top:7px;font-size:10px;color:#CDBEAE;line-height:1.4">${rv.note}</div></div>`,
        { direction:'top', offset:[0,-16], opacity:1, className:'lf-tip' }).addTo(rg);
    });
    rivalG.current = rg;

    // drivers
    const dg = L.layerGroup();
    const hubs = [...ESTATES.map(e=>ESTATE_LL[e.id]), HQ_LL];
    const rnd=(a,b)=>a+Math.random()*(b-a);
    const drivers = [];
    for(let i=0;i<11;i++){
      const ic = L.divIcon({ className:'lf-icon', iconSize:[0,0], iconAnchor:[0,0], html:'<div style="position:absolute;transform:translate(-50%,-50%);"><div class="lf-driver"></div></div>' });
      const m = L.marker(HQ_LL,{ icon:ic, interactive:false, zIndexOffset:100 }); m.addTo(dg);
      const route=[]; const stops=3+(i%3);
      for(let j=0;j<stops;j++){ if(i%2===0&&j%2===0) route.push(HQ_LL); else { const h=hubs[Math.floor(Math.random()*hubs.length)]; route.push([h[0]+rnd(-0.004,0.004), h[1]+rnd(-0.006,0.006)]); } }
      drivers.push({ m, route, seg:Math.floor(Math.random()*route.length), t:Math.random(), speed:rnd(0.10,0.22) });
    }
    driverG.current = dg;
    let last=performance.now();
    const tick=(now)=>{ const dt=Math.min((now-last)/1000,0.05); last=now;
      for(const d of drivers){ const a=d.route[d.seg], b=d.route[(d.seg+1)%d.route.length];
        const dist=Math.max(0.004, Math.hypot(b[0]-a[0], b[1]-a[1])); d.t+=(d.speed*0.05/dist)*dt;
        if(d.t>=1){ d.t=0; d.seg=(d.seg+1)%d.route.length; }
        const e2=d.t<0.5?2*d.t*d.t:1-Math.pow(-2*d.t+2,2)/2;
        d.m.setLatLng([a[0]+(b[0]-a[0])*e2, a[1]+(b[1]-a[1])*e2]); }
      rafRef.current=requestAnimationFrame(tick); };
    rafRef.current=requestAnimationFrame(tick);

    map.on('click', ()=>{ pr.current.app.setSelected(null); pr.current.setArea(null); });
    // cinematic motion blur on any pan/zoom ramp
    let rushT;
    const rushOn=()=>{ if(tourFlight.current && elRef.current) elRef.current.classList.add('lf-rush'); clearTimeout(rushT); };
    const rushOff=()=>{ clearTimeout(rushT); rushT=setTimeout(()=>{ if(elRef.current) elRef.current.classList.remove('lf-rush'); tourFlight.current=false; }, 150); };
    map.on('zoomstart movestart', rushOn);
    map.on('zoomend moveend', rushOff);
    setTimeout(()=>map.invalidateSize(), 60);
    return ()=>{ cancelAnimationFrame(rafRef.current); map.remove(); mapRef.current=null; };
  }, []);

  // refresh estate icons on selection / campaign changes
  const selKey = app.selected || '';
  const campKey = [...campaignAreas].sort().join(',') + '|' + campaignOnly;
  React.useEffect(()=>{
    Object.entries(est.current).forEach(([id,m])=>{
      const e = ESTATES.find(x=>x.id===id); if(!e) return;
      const camp = campaignAreas.has(e.name);
      m.setIcon(buildEstateIcon(e, { selected: app.selected===id, camp, dim: campaignOnly && !camp }));
    });
    if (app.selected && mapRef.current && ESTATE_LL[app.selected]) {
      mapRef.current.flyTo(ESTATE_LL[app.selected], 14.2, { duration:1.05, easeLinearity:0.16 });
      if(tourFlight.current){ const fl=flashRef.current; if(fl){ fl.classList.remove('on'); void fl.offsetWidth; fl.classList.add('on'); } }
    }
  }, [selKey, campKey]);

  // toggle heat layers
  React.useEffect(()=>{
    const map = mapRef.current; if(!map) return;
    MAP_LAYERS.forEach(l=>{ const g=heat.current[l.id]; if(!g) return; if(layers[l.id]){ if(!map.hasLayer(g)) g.addTo(map); } else if(map.hasLayer(g)) map.removeLayer(g); });
  }, [layers]);

  // toggle drivers / rivals
  React.useEffect(()=>{ const map=mapRef.current, g=driverG.current; if(!map||!g) return; if(showDrivers){ if(!map.hasLayer(g)) g.addTo(map); } else if(map.hasLayer(g)) map.removeLayer(g); }, [showDrivers]);
  React.useEffect(()=>{ const map=mapRef.current, g=rivalG.current; if(!map||!g) return; if(showRivals){ if(!map.hasLayer(g)) g.addTo(map); } else if(map.hasLayer(g)) map.removeLayer(g); }, [showRivals]);

  // tours are manual steppers — each click shifts to the next estate / quadrant
  const QUAD = [ {id:'N',name:'North MK',ll:[52.085,-0.755]}, {id:'E',name:'East MK',ll:[52.035,-0.690]}, {id:'S',name:'South MK',ll:[51.996,-0.745]}, {id:'W',name:'West MK',ll:[52.035,-0.828]} ];
  const stepEstate=()=>{ const order=[...ESTATES].sort((a,b)=>b.opportunity-a.opportunity); estIdx.current=(estIdx.current+1)%order.length; tourFlight.current=true; setArea(null); app.setSelected(order[estIdx.current].id); };
  const stepCompass=()=>{ quadIdx.current=(quadIdx.current+1)%QUAD.length; const q=QUAD[quadIdx.current]; tourFlight.current=true; app.setSelected(null); setArea(q.id); if(mapRef.current) mapRef.current.flyTo(q.ll, 13, { duration:1.2, easeLinearity:0.16 }); };
  const resetMap=()=>{ tourFlight.current=true; app.setSelected(null); setArea(null); estIdx.current=-1; quadIdx.current=-1; if(mapRef.current) mapRef.current.flyTo(MK_CENTER, 12.4, { duration:0.9 }); };

  // campaign-areas mode: reframe the map onto live campaigns + recede everything else
  React.useEffect(()=>{
    const map=mapRef.current; if(!map) return;
    const hp=map.getPane('heatPane'); if(hp) hp.style.opacity = campaignOnly ? '0.18' : '0.7';
    if(campaignOnly){
      const lls = ESTATES.filter(e=>campaignAreas.has(e.name)).map(e=>ESTATE_LL[e.id]);
      if(lls.length) map.flyToBounds(L.latLngBounds(lls).pad(0.6), { duration:1.1, easeLinearity:0.16, maxZoom:13.5 });
    } else if(!app.selected){
      map.flyTo(MK_CENTER, 12.4, { duration:0.9 });
    }
  }, [campaignOnly]);

  return (
    <div className={active?undefined:'lf-bg'} style={{ position:'absolute', inset:0, zIndex:0 }}>
      <div ref={elRef} style={{ position:'absolute', inset:0, overflow:'hidden', '--navpad': navOffset+'px' }}/>
      <div ref={flashRef} className="lf-flash"/>
      {active && <>
      <div className="lf-mapctl" style={{ top:'auto', right:'auto', left:navOffset+52, bottom:16, flexDirection:'row', alignItems:'center', flexWrap:'wrap' }}>
        <button className="lf-mapbtn" title="Recenter the map on Milton Keynes" onClick={resetMap}><I.target c="#F5EFE7" s={15}/> Recenter</button>
        <button className="lf-mapbtn" title="Step to the next estate" onClick={stepEstate}><I.pin c="#F5EFE7" s={15}/> Tour estates ▸</button>
        <button className={'lf-mapbtn'+(area?' on':'')} title="Step through N·E·S·W" onClick={stepCompass}><I.compass c={area?'#fff':'#F5EFE7'} s={15}/> Tour N·E·S·W ▸</button>
        <span style={{ width:1, height:22, background:'rgba(255,255,255,0.2)', margin:'0 1px' }}/>
        <button className={'lf-mapbtn'+(fullscreen?' on':'')} title={fullscreen?'Show intelligence panel':'Expand map to full screen'} onClick={onToggleFullscreen}>{fullscreen ? <I.minimize c={fullscreen?'#fff':'#F2A35B'} s={15}/> : <I.maximize c="#F2A35B" s={15}/>} {fullscreen?'Show panel':'Full screen'}</button>
      </div>
      {area && (
        <div style={{ position:'absolute', bottom:64, left:'50%', transform:'translateX(-50%)', zIndex:500, padding:'8px 15px', background:'rgba(16,11,8,0.86)', backdropFilter:'blur(6px)', borderRadius:11, boxShadow:'var(--shadow-md)', display:'flex', alignItems:'center', gap:8 }}>
          <I.compass c="var(--sun-orange)" s={16}/>
          <span style={{ fontFamily:'var(--font-display)', fontWeight:800, fontSize:13, color:'#F5EFE7', letterSpacing:'0.02em' }}>{({N:'North MK',E:'East MK',S:'South MK',W:'West MK'})[area]} · area report →</span>
        </div>
      )}
      <MapLegend right={rightInset}/>
      {campaignOnly && (
        <div style={{ position:'absolute', bottom:104, left:'50%', transform:'translateX(-50%)', zIndex:500, display:'flex', alignItems:'center', gap:9, padding:'9px 13px', background:'rgba(98,67,173,0.94)', backdropFilter:'blur(6px)', borderRadius:11, boxShadow:'var(--shadow-md)' }}>
          <I.megaphone c="#fff" s={16}/>
          <span style={{ fontFamily:'var(--font-display)', fontWeight:800, fontSize:12.5, color:'#fff' }}>{liveCount} campaign{liveCount!==1?'s':''} live · {campaignAreas.size} {campaignAreas.size===1?'area':'areas'} spotlighted</span>
        </div>
      )}
      </>}
    </div>
  );
}

// ─── Area report (N·E·S·W quadrant) ──────────────────────────────────
function estateQuad(e){ const ll=ESTATE_LL[e.id]; const dlat=ll[0]-MK_CENTER[0], dlng=ll[1]-MK_CENTER[1]; return Math.abs(dlat)>=Math.abs(dlng) ? (dlat>=0?'N':'S') : (dlng>=0?'E':'W'); }

function AreaReport({ app, quadId, setArea, campaignAreas }) {
  const name = ({N:'North MK',E:'East MK',S:'South MK',W:'West MK'})[quadId];
  const inq = ESTATES.filter(e=>estateQuad(e)===quadId).sort((a,b)=>b.opportunity-a.opportunity);
  const customers = inq.reduce((s,e)=>s+e.customers,0);
  const interest = inq.reduce((s,e)=>s+e.interest,0);
  const avgOpp = Math.round(inq.reduce((s,e)=>s+e.opportunity,0)/(inq.length||1));
  const avgInf = Math.round(inq.reduce((s,e)=>s+e.influence,0)/(inq.length||1));
  const camps = ACTIVE_CAMPAIGNS.filter(c=>(c.status==='live'||c.status==='ending') && inq.some(e=>e.name===c.area));
  const campRev = camps.reduce((s,c)=>s+c.revenue,0), campSpend = camps.reduce((s,c)=>s+c.spend,0), campOrders = camps.reduce((s,c)=>s+c.orders,0);
  const blendRoas = campSpend ? (campRev/campSpend).toFixed(1) : '—';
  const topOpp = inq[0];

  return (
    <div style={{ width:362, flexShrink:0, height:'100%', overflowY:'auto', background:'rgba(27,18,13,0.62)', backdropFilter:'blur(22px) saturate(140%)', WebkitBackdropFilter:'blur(22px) saturate(140%)', border:'1px solid rgba(255,255,255,0.12)', borderRadius:22, boxShadow:'0 24px 60px -18px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.08)' }}>
      <div style={{ padding:'18px 20px 16px', borderBottom:'1px solid rgba(255,255,255,0.1)', position:'sticky', top:0, background:'rgba(20,14,11,0.6)', backdropFilter:'blur(14px)', WebkitBackdropFilter:'blur(14px)', zIndex:2, borderTopLeftRadius:22, borderTopRightRadius:22 }}>
        <div style={{ display:'flex', alignItems:'flex-start', justifyContent:'space-between' }}>
          <div>
            <Eyebrow color="#E9B97A" style={{ marginBottom:6 }}><I.compass c="var(--sun-orange)" s={12}/> &nbsp;Area report</Eyebrow>
            <h2 style={{ margin:0, fontFamily:'var(--font-display)', fontWeight:900, fontSize:22, letterSpacing:'-0.01em', color:'var(--dust-cream)' }}>{name}</h2>
          </div>
          <button onClick={()=>setArea(null)} style={{ width:30, height:30, borderRadius:9, border:'1px solid rgba(255,255,255,0.18)', background:'rgba(255,255,255,0.08)', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer' }}><I.x c="#B6A595" s={15}/></button>
        </div>
        <div style={{ fontFamily:'var(--font-body)', fontSize:12.5, color:'#B6A595', marginTop:8 }}>{inq.length} estates · {customers.toLocaleString()} customers in this quadrant</div>
      </div>

      <div style={{ padding:'16px 20px' }}>
        {/* aggregate KPIs */}
        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginBottom:16 }}>
          {[['Customers',customers.toLocaleString()],['Want your food',interest.toLocaleString()],['Avg opportunity',avgOpp],['Avg influence',avgInf+'%']].map(([l,v])=>(
            <div key={l} style={{ background:'var(--bg-app)', border:'1px solid var(--soft-line)', borderRadius:12, padding:'12px 13px' }}>
              <div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:20, color:'var(--fg-primary)', lineHeight:1 }}>{v}</div>
              <div style={{ fontFamily:'var(--font-body)', fontSize:11, color:'var(--warm-stone)', marginTop:4 }}>{l}</div>
            </div>
          ))}
        </div>

        {/* campaign performance in this area */}
        <div style={{ background:'var(--bg-app)', border:'1px solid var(--soft-line)', borderRadius:13, padding:'14px 15px', marginBottom:16 }}>
          <Eyebrow style={{ marginBottom:11 }}><I.megaphone c="var(--brand-orange)" s={12}/> &nbsp;Campaign performance</Eyebrow>
          {camps.length===0 ? (
            <div>
              <div style={{ fontFamily:'var(--font-body)', fontSize:12.5, color:'var(--warm-stone)', lineHeight:1.45, marginBottom:11 }}>No live campaigns in {name} yet — with {avgOpp} avg opportunity this is open territory.</div>
              <Btn kind="accent" full size="sm" icon={<I.rocket c="#fff" s={15}/>} onClick={()=>app.openBuilder(topOpp.id)}>Launch in {name}</Btn>
            </div>
          ) : (
            <>
              <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:10, marginBottom:12 }}>
                {[['Revenue','£'+campRev.toLocaleString()],['Blended ROAS',blendRoas+'×'],['Orders',campOrders]].map(([l,v])=>(
                  <div key={l}><div style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:16, color:'var(--fg-primary)' }}>{v}</div><div style={{ fontFamily:'var(--font-body)', fontSize:10, color:'var(--warm-stone)', marginTop:2 }}>{l}</div></div>
                ))}
              </div>
              {camps.map(c=>(
                <div key={c.id} onClick={()=>app.openCampaign(c.id)} style={{ display:'flex', alignItems:'center', gap:10, padding:'9px 0', borderTop:'1px solid var(--soft-line)', cursor:'pointer' }}>
                  <div style={{ flex:1, minWidth:0 }}>
                    <div style={{ fontFamily:'var(--font-display)', fontWeight:800, fontSize:12.5, color:'var(--fg-primary)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{c.name}</div>
                    <div style={{ fontFamily:'var(--font-body)', fontSize:10.5, color:'var(--warm-stone)' }}>{c.area} · {c.orders} orders</div>
                  </div>
                  <Pill tone={c.roi>=5?'green':c.roi>=3.5?'amber':'red'} style={{ fontSize:9.5, padding:'2px 7px' }}>{(c.revenue/c.spend).toFixed(1)}×</Pill>
                  <I.chevR c="var(--warm-stone)" s={15}/>
                </div>
              ))}
            </>
          )}
        </div>

        {/* estates in this area */}
        <div style={{ background:'rgba(250,246,240,0.95)', border:'1px solid rgba(255,255,255,0.5)', borderRadius:16, padding:'13px 15px 4px', boxShadow:'0 10px 26px -14px rgba(0,0,0,0.5)' }}>
        <Eyebrow style={{ marginBottom:10 }}>Estates · tap for full report</Eyebrow>
        {inq.map(e=>{
          const s = NODE_STATE[e.state]; const camp = campaignAreas.has(e.name);
          return (
            <div key={e.id} onClick={()=>{ setArea(null); app.setSelected(e.id); }} style={{ display:'flex', alignItems:'center', gap:11, padding:'10px 0', borderBottom:'1px solid var(--soft-line)', cursor:'pointer' }}>
              <span style={{ width:10, height:10, borderRadius:'50%', background:s.fill, flexShrink:0 }}/>
              <div style={{ flex:1, minWidth:0 }}>
                <div style={{ display:'flex', alignItems:'center', gap:6 }}>
                  <span style={{ fontFamily:'var(--font-display)', fontWeight:700, fontSize:13, color:'var(--fg-primary)' }}>{e.name}</span>
                  {camp && <Pill tone="purple" style={{ fontSize:9, padding:'1px 6px' }}>Live</Pill>}
                </div>
                <div style={{ fontFamily:'var(--font-body)', fontSize:11, color:'var(--warm-stone)' }}>{e.customers.toLocaleString()} customers · {e.influence}% influence</div>
              </div>
              <div style={{ width:40 }}><Bar pct={e.opportunity} color={s.fill} h={6}/></div>
              <span style={{ fontFamily:'var(--font-display)', fontWeight:900, fontSize:14, color:'var(--fg-primary)', width:24, textAlign:'right' }}>{e.opportunity}</span>
            </div>
          );
        })}
        </div>
      </div>
    </div>
  );
}

// ─── Map screen (persistent map; interactive only when active) ────────
function MapScreen({ app, navOffset=260, active=true }) {
  const [layers, setLayers] = React.useState({ heatmap:false, density:false, cuisine:false, competitor:false, influence:false }); // default: ALL layers off — vendor opts in deliberately
  const [campaignOnly, setCampaignOnly] = React.useState(false);
  const [showDrivers, setShowDrivers] = React.useState(false);
  const [showRivals, setShowRivals] = React.useState(false);
  const [area, setArea] = React.useState(null);
  const [fullscreen, setFullscreen] = React.useState(false);
  const [layersOpen, setLayersOpen] = React.useState(true); // toolbar open so layer toggles are visible
  const campaignAreas = new Set(ACTIVE_CAMPAIGNS.filter(c=>c.status==='live'||c.status==='ending').map(c=>c.area));
  const liveCount = ACTIVE_CAMPAIGNS.filter(c=>c.status==='live'||c.status==='ending').length;
  const e = ESTATES.find(x=>x.id===app.selected);
  const toggle = (id)=> setLayers(l=>({ ...l, [id]:!l[id] }));

  const TOP=16, EDGE=16, BAR_TOP=88, PANEL_W=362;
  const rightInset = fullscreen ? EDGE : (EDGE + PANEL_W + 12);

  return (
    <div data-map-campaigns="" data-map-layer-state={JSON.stringify({ ...layers, drivers:showDrivers, rivals:showRivals })} style={{ position:'absolute', inset:0, zIndex: active?20:0, pointerEvents: active?'auto':'none' }}>
      {/* Full-bleed live map */}
      <LeafletMap app={app} layers={layers} campaignAreas={campaignAreas} campaignOnly={campaignOnly}
        showDrivers={showDrivers} showRivals={showRivals} liveCount={liveCount} area={area} setArea={setArea}
        navOffset={navOffset} rightInset={rightInset} fullscreen={fullscreen} onToggleFullscreen={()=>setFullscreen(f=>!f)} active={active}/>

      {/* Dim the map when it is just a backdrop behind another tab */}
      {!active && <div style={{ position:'absolute', inset:0, zIndex:5, pointerEvents:'none', background:'rgba(20,14,11,0.32)' }}/>}

      {active && <>
      {/* Floating topbar */}
      <div style={{ position:'absolute', top:TOP, left:navOffset, right:EDGE, zIndex:55 }}>
        <Topbar app={app} floating/>
      </div>

      {/* Floating frosted-glass map-layers toolbar — collapsible */}
      {!layersOpen ? (
        <button onClick={()=>setLayersOpen(true)} style={{ position:'absolute', top:BAR_TOP, left:navOffset, zIndex:50,
          display:'inline-flex', alignItems:'center', gap:8, padding:'10px 15px', borderRadius:14, cursor:'pointer',
          background:'rgba(255,255,255,0.5)', backdropFilter:'blur(22px) saturate(150%)', WebkitBackdropFilter:'blur(22px) saturate(150%)',
          border:'1px solid rgba(255,255,255,0.5)', boxShadow:'0 16px 40px -16px rgba(27,18,13,0.42)',
          fontFamily:'var(--font-display)', fontWeight:800, fontSize:12.5, color:'var(--fg-primary)' }}>
          <I.layers c="var(--brand-orange)" s={16}/> Map layers
          {(()=>{ const n=Object.values(layers).filter(Boolean).length; return n>0 ? <span style={{ fontFamily:'var(--font-mono)', fontSize:10.5, padding:'1px 7px', borderRadius:7, background:'rgba(216,90,20,0.14)', color:'#c0500f' }}>{n} on</span> : null; })()}
          <I.chevD c="var(--warm-stone)" s={15}/>
        </button>
      ) : (
      <div data-map-toolbar="" style={{ position:'absolute', top:BAR_TOP, left:navOffset, right:rightInset, zIndex:50,
        display:'flex', alignItems:'center', gap:9, flexWrap:'wrap', padding:'10px 13px', borderRadius:16,
        background:'rgba(255,255,255,0.5)', backdropFilter:'blur(22px) saturate(150%)', WebkitBackdropFilter:'blur(22px) saturate(150%)',
        border:'1px solid rgba(255,255,255,0.5)', boxShadow:'0 16px 40px -16px rgba(27,18,13,0.42)', transition:'right 520ms var(--ease-out)' }}>
        <Eyebrow style={{ marginRight:2 }}>Map layers</Eyebrow>
        {MAP_LAYERS.map(l=>{
          const on = layers[l.id];
          return (
            <button key={l.id} data-map-layer-toggle={l.id} {...(l.id==='heatmap'?{'data-map-heat-toggle':''}:{})} onClick={()=>toggle(l.id)} style={{
              display:'inline-flex', alignItems:'center', gap:7, padding:'7px 12px', borderRadius:10, cursor:'pointer',
              background: on ? 'var(--dark-chocolate)' : 'rgba(255,255,255,0.5)', color: on ? 'var(--dust-cream)' : 'var(--fg-primary)',
              border:'1px solid', borderColor: on ? 'var(--dark-chocolate)' : 'rgba(255,255,255,0.5)',
              fontFamily:'var(--font-display)', fontWeight:700, fontSize:12.5, boxShadow:'var(--shadow-sm)', transition:'all 150ms',
            }}>
              <l.icon c={on?'var(--sun-orange)':'var(--warm-stone)'} s={15}/> {l.label}
            </button>
          );
        })}
        <div style={{ width:1, height:26, background:'rgba(27,18,13,0.14)', margin:'0 2px' }}/>
        <ChipToggle on={showDrivers} onClick={()=>setShowDrivers(v=>!v)} color="#D85A14" icon={I.truck}>Live drivers</ChipToggle>
        <ChipToggle on={showRivals} onClick={()=>setShowRivals(v=>!v)} color="#E0492B" icon={I.swords}>Rivals</ChipToggle>
        <div onClick={()=>setCampaignOnly(v=>!v)} role="button" style={{
          display:'inline-flex', alignItems:'center', gap:8, padding:'7px 12px', borderRadius:10, cursor:'pointer',
          background: campaignOnly ? 'rgba(122,87,201,0.16)' : 'rgba(255,255,255,0.5)', color: campaignOnly ? '#6243ad' : 'var(--fg-primary)',
          border:'1px solid', borderColor: campaignOnly ? '#7A57C9' : 'rgba(255,255,255,0.5)',
          fontFamily:'var(--font-display)', fontWeight:700, fontSize:12.5, boxShadow:'var(--shadow-sm)',
        }}>
          <Toggle on={campaignOnly} onChange={()=>setCampaignOnly(v=>!v)} color="#7A57C9"/> Campaign areas{campaignAreas.size?` · ${campaignAreas.size}`:''}
        </div>
        <button onClick={()=>setLayersOpen(false)} title="Collapse layers toolbar" style={{ marginLeft:'auto', width:32, height:32, borderRadius:10, border:'1px solid rgba(255,255,255,0.5)', background:'rgba(255,255,255,0.5)', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', flexShrink:0 }}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--warm-stone)" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="m18 15-6-6-6 6"/></svg>
        </button>
      </div>
      )}

      {/* Right intelligence panel — glides off screen on full-screen */}
      <div style={{ position:'absolute', top:BAR_TOP, right:EDGE, bottom:EDGE, width:PANEL_W, zIndex:52,
        transform: fullscreen ? `translateX(calc(100% + ${EDGE+24}px))` : 'translateX(0)',
        opacity: fullscreen ? 0 : 1, pointerEvents: fullscreen ? 'none' : 'auto',
        transition:'transform 520ms var(--ease-out), opacity 300ms ease' }}>
        {area ? <AreaReport app={app} quadId={area} setArea={setArea} campaignAreas={campaignAreas}/> : e ? <IntelPanel app={app} e={e}/> : <IntelEmpty/>}
      </div>
      </>}
    </div>
  );
}

function ChipToggle({ on, onClick, color, icon:Icon, children }) {
  const layerHook = children === 'Live drivers' ? 'drivers' : children === 'Rivals' ? 'rivals' : undefined;
  return (
    <button {...(layerHook ? {'data-map-layer-toggle': layerHook} : {})} onClick={onClick} style={{
      display:'inline-flex', alignItems:'center', gap:7, padding:'7px 12px', borderRadius:10, cursor:'pointer',
      background: on ? color : 'rgba(255,255,255,0.5)', color: on ? '#fff' : 'var(--fg-primary)',
      border:'1px solid', borderColor: on ? color : 'rgba(255,255,255,0.5)',
      fontFamily:'var(--font-display)', fontWeight:700, fontSize:12.5, boxShadow:'var(--shadow-sm)', transition:'all 150ms',
    }}>
      <Icon c={on?'#fff':'var(--warm-stone)'} s={15}/> {children}
    </button>
  );
}

Object.assign(window, { MapScreen });
