// World Map — pan/zoom + pins
const PIN_TYPES = {
  city:        { icon: '🏰', color: '#c9a227' },
  dungeon:     { icon: '💀', color: '#c53030' },
  landmark:    { icon: '⛰',  color: '#9a9793' },
  seal:        { icon: '🔮', color: '#7c3aed' },
  neutral:     { icon: '📍', color: '#6b6966' },
};

const SEVERITY_COLORS = {
  catastrophic: '#991b1b',
  danger:       '#c53030',
  major:        '#b45309',
  active:       '#1a5c7a',
  explored:     '#1e6b3c',
  neutral:      '#4a4a52',
};

const PIN_TYPE_LABELS = ['city','dungeon','landmark','seal','neutral'];

const WorldMapView = ({ user, setNav }) => {
  const isAdmin = user.isAdmin;

  const [campaigns, setCampaigns] = React.useState([]);
  // Derived from campaigns once they load. Defaults to admin-only until loaded.
  const isDMorAdmin = isAdmin || isAnyDm(campaigns);
  const [pins, setPins] = React.useState([]);
  const [worldEvents, setWorldEvents] = React.useState([]);
  const [npcs, setNpcs] = React.useState([]); // flattened across visible campaigns, used by the editor's NPC chips

  // Load campaigns + pins + world events from API in parallel; backend already scopes campaigns by role.
  React.useEffect(() => {
    let cancelled = false;
    Promise.all([api.campaigns.list(), api.mapPins.list(), api.worldEvents.list()]).then(async ([camps, ps, evs]) => {
      if (cancelled) return;
      setCampaigns(camps || []);
      setPins(ps || []);
      setWorldEvents(evs || []);
      // Pull NPCs across all accessible campaigns so the pin editor can link them.
      try {
        const lists = await Promise.all((camps || []).map(c => api.npcs.list(c.id).catch(() => [])));
        if (!cancelled) setNpcs(lists.flat());
      } catch (err) { console.error('[WorldMapView] NPC fan-out failed:', err); }
    }).catch(err => console.error('[WorldMapView] load failed:', err));
    return () => { cancelled = true; };
  }, []);

  const myCampaignIds = React.useMemo(() => campaigns.map(c => c.id), [campaigns]);
  const [transform, setTransform] = React.useState({ x: 0, y: 0, scale: 1 });
  const [dragging, setDragging] = React.useState(false);
  const [dragStart, setDragStart] = React.useState(null);
  const [selectedPin, setSelectedPin] = React.useState(null);
  const [placingPin, setPlacingPin] = React.useState(false);
  const [editingPin, setEditingPin] = React.useState(null); // null | 'new' | pin id
  const [newPinPos, setNewPinPos] = React.useState(null);
  const [filterType, setFilterType] = React.useState('all');
  const [filterCampaign, setFilterCampaign] = React.useState('all');

  const [sidebarOpen, setSidebarOpen] = React.useState(true);

  const containerRef = React.useRef(null);
  const imgRef = React.useRef(null);

  // Visible pins — filter by campaign membership and filters
  const visiblePins = React.useMemo(() => pins.filter(p => {
    const campaignMatch = p.campaignIds.length === 0 ||
      p.campaignIds.some(id => myCampaignIds.includes(id));
    const typeMatch = filterType === 'all' || p.type === filterType;
    const campFilter = filterCampaign === 'all' ||
      p.campaignIds.includes(filterCampaign);
    return campaignMatch && typeMatch && campFilter;
  }), [pins, myCampaignIds, filterType, filterCampaign]);

  // ── Pan & zoom ─────────────────────────────────────────────────────────────
  const handleWheel = (e) => {
    e.preventDefault();
    const rect = containerRef.current.getBoundingClientRect();
    const mx = e.clientX - rect.left;
    const my = e.clientY - rect.top;
    const delta = e.deltaY > 0 ? 0.85 : 1.18;
    setTransform(prev => {
      const newScale = Math.max(0.3, Math.min(5, prev.scale * delta));
      const scaleRatio = newScale / prev.scale;
      return {
        scale: newScale,
        x: mx - scaleRatio * (mx - prev.x),
        y: my - scaleRatio * (my - prev.y),
      };
    });
  };

  const handleMouseDown = (e) => {
    if (e.button !== 0) return;
    if (placingPin) return;
    setDragging(true);
    setDragStart({ x: e.clientX - transform.x, y: e.clientY - transform.y });
  };

  const handleMouseMove = (e) => {
    if (!dragging || !dragStart) return;
    setTransform(prev => ({
      ...prev,
      x: e.clientX - dragStart.x,
      y: e.clientY - dragStart.y,
    }));
  };

  const handleMouseUp = () => { setDragging(false); setDragStart(null); };

  // ── Place pin on click ─────────────────────────────────────────────────────
  const handleMapClick = (e) => {
    if (!placingPin || !imgRef.current) return;
    const imgRect = imgRef.current.getBoundingClientRect();
    const xPct = ((e.clientX - imgRect.left) / imgRect.width) * 100;
    const yPct = ((e.clientY - imgRect.top) / imgRect.height) * 100;
    setNewPinPos({ x: parseFloat(xPct.toFixed(2)), y: parseFloat(yPct.toFixed(2)) });
    setEditingPin('new');
    setPlacingPin(false);
  };

  // ── Focus on pin ───────────────────────────────────────────────────────────
  const focusPin = (pin) => {
    if (!containerRef.current) return;
    const rect = containerRef.current.getBoundingClientRect();
    const scale = 2.2;
    const pinPixelX = (pin.x / 100) * 820;
    const pinPixelY = (pin.y / 100) * 600;
    setTransform({ scale, x: rect.width / 2 - pinPixelX * scale, y: rect.height / 2 - pinPixelY * scale });
    setSelectedPin(pin);
    setEditingPin(null);
  };

  // ── Fit to view ────────────────────────────────────────────────────────────
  const fitToView = () => {
    if (!containerRef.current) return;
    const rect = containerRef.current.getBoundingClientRect();
    const scaleX = rect.width / 820;
    const scaleY = rect.height / 600;
    const scale = Math.min(scaleX, scaleY, 1);
    setTransform({ x: (rect.width - 820 * scale) / 2, y: (rect.height - 600 * scale) / 2, scale });
  };

  React.useEffect(() => { fitToView(); }, []);

  // ── Save / delete pin (API-backed) ─────────────────────────────────────────
  const refreshPins = async () => {
    try { setPins((await api.mapPins.list()) || []); }
    catch (err) { console.error('[WorldMapView] refresh failed:', err); }
  };

  const savePin = async (pin) => {
    try {
      // City-only fields (size, region, plotPrice) are only meaningful when
      // type === 'city'. The backend ignores them otherwise; sending them
      // unconditionally keeps create/update payloads symmetric and avoids
      // forgetting one path when adding fields.
      const cityFields = pin.type === 'city' ? {
        citySize:      pin.citySize      || null,
        cityRegion:    pin.cityRegion    || null,
        cityPlotPrice: typeof pin.cityPlotPrice === 'number' ? pin.cityPlotPrice : null,
      } : { citySize: null, cityRegion: null, cityPlotPrice: null };

      if (editingPin === 'new') {
        await api.mapPins.create({
          name: pin.name, type: pin.type || 'landmark', severity: pin.severity || 'neutral',
          x: newPinPos.x, y: newPinPos.y,
          description: pin.description || '', cityId: pin.cityId || null,
          campaignIds: pin.campaignIds || [],
          linkedNpcIds: pin.linkedNpcIds || [],
          linkedWorldEventIds: pin.linkedWorldEventIds || [],
          ...cityFields,
        });
      } else {
        await api.mapPins.update(editingPin, {
          name: pin.name, type: pin.type, severity: pin.severity,
          description: pin.description, cityId: pin.cityId || null,
          campaignIds: pin.campaignIds || [],
          linkedNpcIds: pin.linkedNpcIds || [],
          linkedWorldEventIds: pin.linkedWorldEventIds || [],
          ...cityFields,
        });
      }
      await refreshPins();
      // Any city-typed pin save may have created or updated a City row. Tell
      // the rest of the app to refetch cities so the guild base picker and
      // similar consumers see it without a full page reload.
      if (pin.type === 'city') window.dispatchEvent(new Event('kamia:cities-changed'));
    } catch (err) {
      console.error('[WorldMapView] savePin failed:', err);
      window.dialog.alert('Failed to save pin: ' + (err.message || 'unknown error'));
    }
    setEditingPin(null);
    setNewPinPos(null);
  };

  const deletePin = async (id) => {
    if (!(await window.dialog.confirm({ title:'Delete pin?', message:'Delete this pin?', danger:true, confirmLabel:'Delete' }))) return;
    try {
      await api.mapPins.delete(id);
      setPins(prev => prev.filter(p => p.id !== id));
    } catch (err) {
      console.error('[WorldMapView] deletePin failed:', err);
      window.dialog.alert('Failed to delete pin: ' + (err.message || 'unknown error'));
    }
    setSelectedPin(null);
  };

  return (
    <div style={mapStyles.root}>
      {/* Toolbar */}
      <div style={mapStyles.toolbar}>
        <div style={{display:'flex',gap:8,alignItems:'center',flexWrap:'wrap'}}>
          <span style={mapStyles.toolbarTitle}>🗺 World Map</span>
          <select style={mapStyles.filterSelect} value={filterType} onChange={e=>setFilterType(e.target.value)}>
            <option value="all">All types</option>
            {PIN_TYPE_LABELS.map(t=><option key={t} value={t}>{t}</option>)}
          </select>
          <select style={mapStyles.filterSelect} value={filterCampaign} onChange={e=>setFilterCampaign(e.target.value)}>
            <option value="all">All campaigns</option>
            {campaigns.map(c=><option key={c.id} value={c.id}>{c.name}</option>)}
          </select>
        </div>
        <div style={{display:'flex',gap:8,alignItems:'center'}}>
          <button style={{...mapStyles.toolBtn,...(sidebarOpen?mapStyles.toolBtnActive:{})}} onClick={()=>setSidebarOpen(o=>!o)} title="Toggle pin list">☰ Pins</button>
          <button style={mapStyles.toolBtn} onClick={fitToView} title="Fit to view">⊡ Fit</button>
          <button style={mapStyles.toolBtn} onClick={()=>setTransform(p=>({...p,scale:Math.min(5,p.scale*1.3)}))} title="Zoom in">+</button>
          <button style={mapStyles.toolBtn} onClick={()=>setTransform(p=>({...p,scale:Math.max(0.3,p.scale*0.77)}))} title="Zoom out">−</button>
          {isAdmin && (
            <button style={{...mapStyles.toolBtn,...(placingPin?mapStyles.toolBtnActive:{})}}
              onClick={()=>{ setPlacingPin(!placingPin); setSelectedPin(null); }}>
              {placingPin ? '✕ Cancel' : '📍 Place Pin'}
            </button>
          )}
        </div>
      </div>

      {placingPin && (
        <div style={mapStyles.placingBanner}>
          Click anywhere on the map to place a new pin
        </div>
      )}

      {/* Body: sidebar + map */}
      <div style={{ display:'flex', flex:1, overflow:'hidden', position:'relative' }}>

      {/* Pin List Sidebar */}
      <div style={{ ...mapStyles.pinSidebar, width: sidebarOpen ? 220 : 0, minWidth: sidebarOpen ? 220 : 0 }}>
        {sidebarOpen && (
          <>
            <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 12px 8px', borderBottom:'1px solid #2a2a32', flexShrink:0 }}>
              <span style={{ fontSize:11, fontWeight:800, color:'#6b6966', textTransform:'uppercase', letterSpacing:'0.12em' }}>Pins</span>
              <button style={{ background:'none', border:'none', color:'#4a4a52', cursor:'pointer', fontSize:14, lineHeight:1, padding:0 }} onClick={()=>setSidebarOpen(false)}>✕</button>
            </div>
            <div style={{ overflowY:'auto', flex:1 }}>
              {PIN_TYPE_LABELS.map(type => {
                const group = visiblePins.filter(p => p.type === type);
                if (!group.length) return null;
                const typeInfo = PIN_TYPES[type];
                return (
                  <div key={type}>
                    <div style={{ padding:'8px 12px 4px', fontSize:10, fontWeight:800, color:'#6b6966', textTransform:'uppercase', letterSpacing:'0.12em', display:'flex', alignItems:'center', gap:5 }}>
                      <span>{typeInfo.icon}</span>{type}
                    </div>
                    {group.map(pin => {
                      const sevColor = SEVERITY_COLORS[pin.severity] || SEVERITY_COLORS.neutral;
                      const isActive = selectedPin?.id === pin.id;
                      return (
                        <button key={pin.id}
                          style={{ display:'flex', alignItems:'center', gap:8, width:'100%', background: isActive ? 'rgba(201,162,39,0.08)' : 'none', border:'none', borderLeft: isActive ? `2px solid ${sevColor}` : '2px solid transparent', padding:'7px 12px', cursor:'pointer', textAlign:'left', fontFamily:"'Nunito',sans-serif", transition:'background 0.12s' }}
                          onClick={() => focusPin(pin)}>
                          <span style={{ width:8, height:8, borderRadius:'50%', background:sevColor, flexShrink:0, display:'inline-block' }}></span>
                          <span style={{ fontSize:12, color: isActive ? '#e8e6e3' : '#9a9793', fontWeight: isActive ? 700 : 400, flex:1, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{pin.name}</span>
                        </button>
                      );
                    })}
                  </div>
                );
              })}
              {visiblePins.length === 0 && (
                <div style={{ fontSize:12, color:'#4a4a52', padding:'20px 12px', textAlign:'center' }}>No pins match filters</div>
              )}
            </div>
          </>
        )}
      </div>

      {/* Map container */}
      <div
        ref={containerRef}
        style={{...mapStyles.mapContainer, cursor: placingPin ? 'crosshair' : dragging ? 'grabbing' : 'grab'}}
        onWheel={handleWheel}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
        onMouseLeave={handleMouseUp}
        onClick={handleMapClick}
      >
        <div style={{
          position:'absolute',
          transform:`translate(${transform.x}px,${transform.y}px) scale(${transform.scale})`,
          transformOrigin:'0 0',
          width: 820, height: 600,
          userSelect:'none',
        }}>
          <img
            ref={imgRef}
            src="map.jpg"
            alt="World Map"
            style={{width:820,height:600,display:'block',borderRadius:4,imageRendering:'auto'}}
            draggable={false}
          />

          {/* Pins */}
          {visiblePins.map(pin => {
            const typeInfo = PIN_TYPES[pin.type] || PIN_TYPES.neutral;
            const sevColor = SEVERITY_COLORS[pin.severity] || SEVERITY_COLORS.neutral;
            const isSelected = selectedPin?.id === pin.id;
            return (
              <div
                key={pin.id}
                style={{
                  position:'absolute',
                  left:`${pin.x}%`, top:`${pin.y}%`,
                  transform:'translate(-50%,-100%)',
                  cursor:'pointer',
                  zIndex: isSelected ? 20 : 10,
                  filter: isSelected ? 'drop-shadow(0 0 6px rgba(255,255,255,0.6))' : 'drop-shadow(0 2px 4px rgba(0,0,0,0.8))',
                  transition:'filter 0.15s',
                }}
                onClick={e => { e.stopPropagation(); setSelectedPin(isSelected ? null : pin); setEditingPin(null); }}
              >
                {/* Pin body */}
                <div style={{
                  background: sevColor,
                  border: `2px solid rgba(255,255,255,0.3)`,
                  borderRadius: '50% 50% 50% 0',
                  transform: 'rotate(-45deg)',
                  width: 28, height: 28,
                  display:'flex', alignItems:'center', justifyContent:'center',
                  boxShadow: isSelected ? `0 0 0 3px rgba(255,255,255,0.4)` : 'none',
                }}>
                  <span style={{transform:'rotate(45deg)', fontSize:13, lineHeight:1}}>{typeInfo.icon}</span>
                </div>
                {/* Label on hover/select */}
                {isSelected && (
                  <div style={{
                    position:'absolute', bottom:'calc(100% + 6px)', left:'50%',
                    transform:'translateX(-50%)',
                    background:'#1c1c22', border:'1px solid #3a3a42',
                    borderRadius:5, padding:'3px 8px', fontSize:11,
                    color:'#e8e6e3', fontWeight:700, whiteSpace:'nowrap',
                    fontFamily:"'Nunito',sans-serif",
                  }}>{pin.name}</div>
                )}
              </div>
            );
          })}
        </div>
      </div>

      {/* Pin detail panel */}
      {selectedPin && editingPin === null && (
        <PinDetailPanel
          pin={selectedPin}
          campaigns={campaigns}
          worldEvents={worldEvents}
          isAdmin={isAdmin}
          onClose={() => setSelectedPin(null)}
          onEdit={() => setEditingPin(selectedPin.id)}
          onDelete={() => deletePin(selectedPin.id)}
          setNav={setNav}
        />
      )}

      {/* Pin editor */}
      {editingPin !== null && (
        <PinEditor
          campaigns={campaigns}
          npcs={npcs}
          worldEvents={worldEvents}
          pin={editingPin === 'new' ? null : pins.find(p=>p.id===editingPin)}
          onSave={savePin}
          onCancel={() => { setEditingPin(null); setNewPinPos(null); }}
        />
      )}
      </div>{/* end body flex */}
    </div>
  );
};

// ── Pin Detail Panel ──────────────────────────────────────────────────────────
const PinDetailPanel = ({ pin, campaigns = [], worldEvents = [], isAdmin, onClose, onEdit, onDelete, setNav }) => {
  const typeInfo = PIN_TYPES[pin.type] || PIN_TYPES.neutral;
  const sevColor = SEVERITY_COLORS[pin.severity] || SEVERITY_COLORS.neutral;
  const linkedCampaigns = campaigns.filter(c => (pin.campaignIds || []).includes(c.id));
  // NPC name resolution lifted to a future MapPin DTO enrichment — see todo.md.
  const linkedNpcs = [];
  const linkedEvents = worldEvents.filter(e => (pin.linkedWorldEventIds || []).includes(e.id));

  return (
    <div style={mapStyles.detailPanel}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',marginBottom:12}}>
        <div style={{display:'flex',gap:10,alignItems:'center'}}>
          <div style={{...mapStyles.pinIconLg, background: sevColor}}>{typeInfo.icon}</div>
          <div>
            <div style={{fontFamily:"'Cinzel',serif",fontSize:16,fontWeight:700,color:'#e8e6e3'}}>{pin.name}</div>
            <div style={{fontSize:11,color:'#9a9793',textTransform:'capitalize',marginTop:1}}>{pin.type} · <span style={{color:sevColor}}>{pin.severity}</span></div>
          </div>
        </div>
        <button style={mapStyles.closeBtn} onClick={onClose}>✕</button>
      </div>

      <p style={{fontSize:13,color:'#c5c3c0',lineHeight:1.6,margin:'0 0 12px'}}>{pin.description}</p>

      {linkedCampaigns.length > 0 && (
        <div style={mapStyles.detailSection}>
          <div style={mapStyles.detailSectionLabel}>Visible in</div>
          <div style={{display:'flex',flexWrap:'wrap',gap:4}}>
            {linkedCampaigns.map(c=>(
              <span key={c.id} style={{...mapStyles.tag, borderLeft:`2px solid ${c.bannerColor}`}}>{c.name}</span>
            ))}
          </div>
        </div>
      )}

      {linkedNpcs.length > 0 && (
        <div style={mapStyles.detailSection}>
          <div style={mapStyles.detailSectionLabel}>Related NPCs</div>
          {linkedNpcs.map(n=>(
            <button key={n.id} style={mapStyles.linkRow}
              onClick={()=>setNav({view:'npc-chat',npcId:n.id,campaignId:n.campaignId})}>
              <span style={mapStyles.npcAvatarSm}>{n.avatar}</span>
              <span>{n.name}</span>
              <span style={{marginLeft:'auto',fontSize:11,color:'#6b6966'}}>→</span>
            </button>
          ))}
        </div>
      )}

      {linkedEvents.length > 0 && (
        <div style={mapStyles.detailSection}>
          <div style={mapStyles.detailSectionLabel}>World Events</div>
          {linkedEvents.map(e=>(
            <div key={e.id} style={{...mapStyles.linkRow,cursor:'default',borderLeft:`3px solid ${severityColor(e.severity)}`}}>
              <span style={{fontSize:12,color:'#e8e6e3'}}>{e.title}</span>
            </div>
          ))}
        </div>
      )}

      {isAdmin && (
        <div style={{display:'flex',gap:8,marginTop:14}}>
          <button style={mapStyles.editBtn} onClick={onEdit}>✎ Edit Pin</button>
          <button style={{...mapStyles.editBtn,color:'#f87171',borderColor:'rgba(248,113,113,0.2)'}} onClick={onDelete}>Delete</button>
        </div>
      )}
    </div>
  );
};

// ── Pin Editor ────────────────────────────────────────────────────────────────
const CITY_SIZES = ['capital', 'city', 'town', 'outpost', 'hamlet'];
const PinEditor = ({ pin, onSave, onCancel, campaigns = [], npcs = [], worldEvents = [] }) => {
  const [form, setForm] = React.useState({
    name: pin?.name || '',
    type: pin?.type || 'city',
    severity: pin?.severity || 'neutral',
    description: pin?.description || '',
    campaignIds: pin?.campaignIds || [],
    linkedNpcIds: pin?.linkedNpcIds || [],
    linkedWorldEventIds: pin?.linkedWorldEventIds || [],
    // City-only fields. Only sent to the backend when the pin is a city. The
    // backend uses them to populate the auto-created or linked City row.
    // Pre-fill from the linked City when editing an existing city pin so
    // edits don't accidentally clear region / plot price.
    citySize:      pin?.citySize      || 'town',
    cityRegion:    pin?.cityRegion    || '',
    cityPlotPrice: pin?.cityPlotPrice ?? 0,
  });

  const set = (k, v) => setForm(p => ({...p,[k]:v}));

  const toggleArr = (k, val) => setForm(p => ({
    ...p,
    [k]: p[k].includes(val) ? p[k].filter(x=>x!==val) : [...p[k],val],
  }));

  return (
    <div style={mapStyles.editorPanel}>
      <div style={{fontSize:13,fontWeight:800,color:'#c9a227',textTransform:'uppercase',letterSpacing:'0.1em',marginBottom:14}}>
        {pin ? 'Edit Pin' : 'New Pin'}
      </div>

      <div style={mapStyles.formRow}>
        <label style={mapStyles.formLabel}>Name<Req/></label>
        <input style={mapStyles.formInput} value={form.name} onChange={e=>set('name',e.target.value)} placeholder="Location name"/>
      </div>

      <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,marginBottom:10}}>
        <div>
          <label style={mapStyles.formLabel}>Type</label>
          <select style={mapStyles.formSelect} value={form.type} onChange={e=>set('type',e.target.value)}>
            {PIN_TYPE_LABELS.map(t=><option key={t} value={t}>{t}</option>)}
          </select>
        </div>
        <div>
          <label style={mapStyles.formLabel}>Severity</label>
          <select style={mapStyles.formSelect} value={form.severity} onChange={e=>set('severity',e.target.value)}>
            {Object.keys(SEVERITY_COLORS).map(s=><option key={s} value={s}>{s}</option>)}
          </select>
        </div>
      </div>

      {/* City-only details. The backend keeps a City row in sync with these
          fields (creating one on the first save of a city-typed pin). Guild
          base-of-operations pickers read those City rows. */}
      {form.type === 'city' && (
        <div style={{ ...mapStyles.formRow, background:'rgba(201,162,39,0.04)', border:'1px solid rgba(201,162,39,0.18)', borderRadius:8, padding:'10px 12px' }}>
          <div style={{ fontSize:11, color:'#c9a227', fontWeight:800, textTransform:'uppercase', letterSpacing:'0.1em', marginBottom:10 }}>
            City details
          </div>
          <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:10, marginBottom:8 }}>
            <div>
              <label style={mapStyles.formLabel}>Size</label>
              <select style={mapStyles.formSelect} value={form.citySize} onChange={e=>set('citySize',e.target.value)}>
                {CITY_SIZES.map(s => <option key={s} value={s}>{s}</option>)}
              </select>
            </div>
            <div>
              <label style={mapStyles.formLabel}>Plot price (gp)</label>
              <input type="number" min={0} step={100} style={mapStyles.formInput}
                value={form.cityPlotPrice}
                onChange={e=>set('cityPlotPrice', Math.max(0, parseInt(e.target.value)||0))} />
            </div>
          </div>
          <div>
            <label style={mapStyles.formLabel}>Region</label>
            <input style={mapStyles.formInput} value={form.cityRegion}
              onChange={e=>set('cityRegion', e.target.value)}
              placeholder="e.g. Aldenmoor (south)" />
          </div>
          <div style={{ fontSize:11, color:'#6b6966', marginTop:8 }}>
            A matching city row is created on save and shown in guild "Purchase Plot" pickers.
          </div>
        </div>
      )}

      <div style={mapStyles.formRow}>
        <label style={mapStyles.formLabel}>Description</label>
        <textarea style={{...mapStyles.formInput,resize:'vertical'}} rows={3}
          value={form.description} onChange={e=>set('description',e.target.value)}/>
      </div>

      <div style={mapStyles.formRow}>
        <label style={mapStyles.formLabel}>Visible in campaigns</label>
        <div style={{display:'flex',flexWrap:'wrap',gap:6}}>
          {campaigns.map(c=>(
            <button key={c.id}
              style={{...mapStyles.checkChip,...(form.campaignIds.includes(c.id)?{background:'rgba(197,48,48,0.2)',borderColor:'#c53030',color:'#e8e6e3'}:{})}}
              onClick={()=>toggleArr('campaignIds',c.id)}>
              {c.name}
            </button>
          ))}
        </div>
      </div>

      <div style={mapStyles.formRow}>
        <label style={mapStyles.formLabel}>Linked NPCs</label>
        <div style={{display:'flex',flexWrap:'wrap',gap:6}}>
          {npcs.map(n=>(
            <button key={n.id}
              style={{...mapStyles.checkChip,...(form.linkedNpcIds.includes(n.id)?{background:'rgba(201,162,39,0.15)',borderColor:'#c9a227',color:'#e8e6e3'}:{})}}
              onClick={()=>toggleArr('linkedNpcIds',n.id)}>
              {n.name}
            </button>
          ))}
        </div>
      </div>

      <div style={mapStyles.formRow}>
        <label style={mapStyles.formLabel}>Linked World Events</label>
        <div style={{display:'flex',flexWrap:'wrap',gap:6}}>
          {worldEvents.map(e=>(
            <button key={e.id}
              style={{...mapStyles.checkChip,...(form.linkedWorldEventIds.includes(e.id)?{background:'rgba(107,26,26,0.3)',borderColor:'#c53030',color:'#e8e6e3'}:{})}}
              onClick={()=>toggleArr('linkedWorldEventIds',e.id)}>
              {e.title.substring(0,30)}…
            </button>
          ))}
        </div>
      </div>

      <div style={{display:'flex',gap:8,marginTop:14}}>
        <button style={mapStyles.editBtn} onClick={()=>onSave(form)} disabled={!form.name.trim()}>
          {pin ? 'Save Changes' : 'Create Pin'}
        </button>
        <button style={{...mapStyles.editBtn,background:'none',borderColor:'#3a3a42',color:'#9a9793'}} onClick={onCancel}>Cancel</button>
      </div>
    </div>
  );
};

// ── Styles ────────────────────────────────────────────────────────────────────
const mapStyles = {
  root: { display:'flex', flexDirection:'column', height:'100%', overflow:'hidden', position:'relative', fontFamily:"'Nunito',sans-serif" },
  pinSidebar: { display:'flex', flexDirection:'column', background:'#13131a', borderRight:'1px solid #2a2a32', flexShrink:0, overflow:'hidden', transition:'width 0.2s ease, min-width 0.2s ease' },
  toolbar: { display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 16px', background:'#16161b', borderBottom:'1px solid #2a2a32', flexShrink:0, flexWrap:'wrap', gap:8 },
  toolbarTitle: { fontFamily:"'Cinzel',serif", fontSize:15, fontWeight:700, color:'#c9a227', marginRight:8 },
  filterSelect: { background:'#111116', border:'1px solid #3a3a42', borderRadius:6, color:'#e8e6e3', padding:'5px 10px', fontSize:12, cursor:'pointer', fontFamily:"'Nunito',sans-serif" },
  toolBtn: { background:'#1c1c22', border:'1px solid #3a3a42', color:'#9a9793', borderRadius:6, padding:'5px 12px', cursor:'pointer', fontSize:13, fontWeight:700, fontFamily:"'Nunito',sans-serif" },
  toolBtnActive: { background:'rgba(197,48,48,0.2)', borderColor:'#c53030', color:'#e8e6e3' },
  placingBanner: { background:'rgba(201,162,39,0.12)', border:'none', borderBottom:'1px solid rgba(201,162,39,0.3)', padding:'7px 16px', fontSize:13, color:'#c9a227', fontWeight:700, textAlign:'center', flexShrink:0 },
  mapContainer: { flex:1, position:'relative', overflow:'hidden', background:'#0d0d10' },
  detailPanel: {
    position:'absolute', top:16, right:16, width:300,
    background:'#1c1c22', border:'1px solid #3a3a42', borderRadius:10,
    padding:'16px 18px', boxShadow:'0 8px 32px rgba(0,0,0,0.6)',
    zIndex:100, maxHeight:'calc(100% - 32px)', overflowY:'auto',
  },
  editorPanel: {
    position:'absolute', top:16, right:16, width:320,
    background:'#1c1c22', border:'1px solid #c9a22744', borderRadius:10,
    padding:'16px 18px', boxShadow:'0 8px 32px rgba(0,0,0,0.6)',
    zIndex:100, maxHeight:'calc(100% - 32px)', overflowY:'auto',
  },
  closeBtn: { background:'none', border:'none', color:'#6b6966', cursor:'pointer', fontSize:16, padding:0, lineHeight:1 },
  pinIconLg: { width:36, height:36, borderRadius:8, display:'flex', alignItems:'center', justifyContent:'center', fontSize:18, flexShrink:0 },
  detailSection: { borderTop:'1px solid #2a2a32', paddingTop:10, marginTop:10 },
  detailSectionLabel: { fontSize:10, fontWeight:800, color:'#6b6966', textTransform:'uppercase', letterSpacing:'0.1em', marginBottom:6 },
  tag: { fontSize:11, background:'#2a2a32', color:'#9a9793', padding:'2px 8px', borderRadius:4, paddingLeft:6 },
  linkRow: { display:'flex', alignItems:'center', gap:8, background:'none', border:'1px solid #2a2a32', borderRadius:6, padding:'7px 10px', cursor:'pointer', color:'#e8e6e3', fontSize:13, fontFamily:"'Nunito',sans-serif", marginBottom:4, width:'100%', textAlign:'left' },
  npcAvatarSm: { width:24, height:24, borderRadius:4, background:'#2a2a32', display:'flex', alignItems:'center', justifyContent:'center', fontSize:10, fontWeight:700, color:'#c9a227', flexShrink:0 },
  editBtn: { background:'rgba(197,48,48,0.15)', border:'1px solid #c53030', color:'#e8e6e3', borderRadius:6, padding:'6px 14px', cursor:'pointer', fontSize:13, fontWeight:700, fontFamily:"'Nunito',sans-serif" },
  formRow: { marginBottom:10 },
  formLabel: { fontSize:10, fontWeight:800, color:'#6b6966', textTransform:'uppercase', letterSpacing:'0.08em', display:'block', marginBottom:4 },
  formInput: { width:'100%', background:'#111116', border:'1px solid #3a3a42', borderRadius:6, color:'#e8e6e3', padding:'7px 10px', fontSize:13, outline:'none', fontFamily:"'Nunito',sans-serif", boxSizing:'border-box' },
  formSelect: { width:'100%', background:'#111116', border:'1px solid #3a3a42', borderRadius:6, color:'#e8e6e3', padding:'7px 10px', fontSize:13, cursor:'pointer', fontFamily:"'Nunito',sans-serif", boxSizing:'border-box' },
  checkChip: { background:'none', border:'1px solid #2a2a32', color:'#6b6966', borderRadius:6, padding:'4px 10px', cursor:'pointer', fontSize:12, fontFamily:"'Nunito',sans-serif" },
};

Object.assign(window, { WorldMapView, mapStyles });
