// Right-side inspector — shows different fields per selection type
// (node / edge / group / note).
function Inspector({
  selection, setSelection,
  nodes, edges, groups, notes,
  defaultCurrency,
  updateNode, updateEdge, updateNote,
  deleteNode, deleteEdge, deleteNote,
  duplicateNode, duplicateNote,
  addNode, addNote, clearAll,
  onLoadDemo, onAutoGroupByDate,
  createGroupFromNode, updateGroup, deleteGroup,
  composingGroupId, setComposingGroupId,
  notePickMode, startNoteAnchorPick,
  edgePickMode, startEdgeRetargetPick,
}){
  groups = groups || [];
  notes = notes || [];
  if (!selection){
    return (
      <div className="side">
        <div className="card">
          <h3 style={{marginTop:0}}>Getting started</h3>
          <div style={{fontSize:12, lineHeight:1.7, color:'var(--ink-soft)'}}>
            • Use the toolbar's <b style={{color:'var(--green-deep)'}}>+ Place</b> to add a place<br/>
            • <b style={{color:'var(--green-deep)'}}>+ Place ▾</b> picks from stations, airports, hotels, etc.<br/>
            • Drag the <b style={{color:'var(--green-deep)'}}>＋</b> handle on a place to another place to connect them<br/>
            • Click any arrow / place / note to edit it
          </div>
        </div>
        <div style={{display:'flex',gap:6, flexDirection:'column'}}>
          <button className="btn primary" onClick={()=>addNode({type:'pin'})}>+ Add place</button>
          <button className="btn" onClick={()=>addNote()}>
            <window.TourIcon name="sticky-note" size={14}/>
            Add note
          </button>
          {/* Auto-grouping by date — duplicates the toolbar's ・・・ menu
              entry, surfaced here for discoverability since users typically
              come back to the sidebar after entering all datetimes. */}
          {onAutoGroupByDate && (
            <button className="btn" onClick={()=>onAutoGroupByDate()}
                    title="Generate Day 1 / Day 2 / … groups from each node's arrival / departure dates">
              <window.TourIcon name="calendar" size={14}/>
              Auto-group by date
            </button>
          )}
          <button className="btn" onClick={()=>onLoadDemo('travel')}>
            <window.TourIcon name="sparkles" size={14}/> Sample itinerary
          </button>
          <button className="btn" onClick={()=>onLoadDemo('business')}>
            <window.TourIcon name="briefcase" size={14}/> Sample business trip
          </button>
          <button className="btn ghost" onClick={clearAll}>
            <window.TourIcon name="eraser" size={14}/> Clear all
          </button>
        </div>
        <div className="empty-hint">
          Nothing selected.<br/>Click any place, arrow, or note on the canvas.
        </div>
      </div>
    );
  }

  if (selection.type === 'node'){
    const node = nodes.find(n=>n.id===selection.id);
    if (!node) return null;
    const nodeGroups = groups.filter(g => (g.nodeIds || []).includes(node.id));
    return (
      <div className="side">
        <NodeInspector node={node} allNodes={nodes} allEdges={edges}
                       defaultCurrency={defaultCurrency}
                       updateNode={updateNode} deleteNode={deleteNode}
                       duplicateNode={duplicateNode}
                       nodeGroups={nodeGroups}
                       createGroupFromNode={createGroupFromNode}
                       updateGroup={updateGroup} deleteGroup={deleteGroup}
                       setComposingGroupId={setComposingGroupId}
                       setSelection={setSelection}/>
      </div>
    );
  }

  if (selection.type === 'edge'){
    const edge = edges.find(e=>e.id===selection.id);
    if (!edge) return null;
    return (
      <div className="side">
        <EdgeInspector edge={edge} totalEdges={edges.length} edges={edges}
                       nodes={nodes} defaultCurrency={defaultCurrency}
                       updateEdge={updateEdge} deleteEdge={deleteEdge}
                       edgePickMode={edgePickMode}
                       startEdgeRetargetPick={startEdgeRetargetPick}/>
      </div>
    );
  }

  if (selection.type === 'group'){
    const group = groups.find(g=>g.id===selection.id);
    if (!group) return null;
    return (
      <div className="side">
        <GroupInspector group={group} nodes={nodes}
                        updateGroup={updateGroup} deleteGroup={deleteGroup}
                        setComposingGroupId={setComposingGroupId}
                        setSelection={setSelection}/>
      </div>
    );
  }

  if (selection.type === 'note'){
    const note = notes.find(n=>n.id===selection.id);
    if (!note) return null;
    return (
      <div className="side">
        <NoteInspector note={note} nodes={nodes} edges={edges}
                       updateNote={updateNote} deleteNote={deleteNote}
                       duplicateNote={duplicateNote}
                       notePickMode={notePickMode}
                       startNoteAnchorPick={startNoteAnchorPick}/>
      </div>
    );
  }

  return null;
}

// ── DateTimeField — HTML5 datetime-local with quick-fill chips ─
// `quickActions` is an array of { label, value } where value is the
// datetime ISO to set on click (or '' to clear). Quick actions disable
// when their `value` is empty / unavailable.
// `min` is the earliest selectable datetime — used to block picking times
// that would travel BACKWARD through the edge graph (e.g. setting a
// destination's arrival to a date earlier than the origin's departure).
// HTML5 datetime-local respects `min` in the picker UI; we also show an
// explicit error if a typed value violates it.
function DateTimeField({ value, onChange, quickActions, error, hint, min }){
  const id = React.useMemo(()=>'dt_' + Math.random().toString(36).slice(2,8), []);
  return (
    <div>
      <input id={id} type="datetime-local"
             value={value || ''}
             min={min || undefined}
             onChange={e=>onChange(e.target.value)}
             style={{
               border:'1px solid var(--line)', background:'var(--paper)',
               borderRadius:8, padding:'7px 10px', fontSize:13, fontFamily:'inherit',
               width:'100%', outline:'none',
               borderColor: error ? 'var(--danger)' : undefined,
             }}/>
      {(quickActions || []).length > 0 && (
        <div style={{display:'flex', flexWrap:'wrap', gap:4, marginTop:4}}>
          {quickActions.map((qa, i) => (
            <button key={i}
                    className="btn sm ghost"
                    disabled={qa.value === undefined || qa.value === null}
                    onClick={()=>onChange(qa.value || '')}
                    style={{padding:'3px 8px', fontSize:11}}
                    title={qa.title || ''}>
              {qa.label}
            </button>
          ))}
        </div>
      )}
      {error && (
        <div style={{fontSize:11, color:'var(--danger)', marginTop:3, fontWeight:600, display:'flex', alignItems:'center', gap:4}}>
          <window.TourIcon name="circle-alert" size={12} stroke="currentColor"/> {error.replace(/^⚠\s*/, '')}
        </div>
      )}
      {hint && !error && (
        <div style={{fontSize:11, color:'var(--ink-soft)', marginTop:3}}>{hint}</div>
      )}
    </div>
  );
}

// Find the "previous" node — i.e. the from-node of the lowest-seq incoming edge.
// Used to derive sensible quick-fill defaults like "same as prev departure".
function findPrevNode(nodeId, nodes, edges){
  const incoming = edges.filter(e => e.to === nodeId)
                        .sort((a,b)=>(a.seq||999)-(b.seq||999));
  if (!incoming.length) return null;
  return nodes.find(n => n.id === incoming[0].from) || null;
}

// Latest "must-be-after" boundary time for a node's ARRIVAL. Walks all
// incoming edges and returns the maximum departure (or arrival as fallback)
// among predecessor nodes. Returns null if there is no constraint — e.g.
// the node has no incoming edges, or no predecessor has any datetime set.
// The returned ISO string is suitable for the HTML5 datetime-local `min`
// attribute and our internal validation messages.
function getEarliestArrivalBoundary(nodeId, nodes, edges){
  let latest = null;
  edges.filter(e => e.to === nodeId).forEach(e => {
    const fromNode = nodes.find(n => n.id === e.from);
    if (!fromNode) return;
    // Prefer the predecessor's departure (real "moment they left"); fall
    // back to its arrival if departure isn't recorded.
    const candidate = fromNode.departure || fromNode.arrival;
    if (!candidate) return;
    if (!latest || candidate > latest) latest = candidate;
  });
  return latest;
}

// (SeqDirectInput removed — sequence numbers are auto-derived from each
// node's arrival/departure times, so manual entry is no longer needed.)

// ── Validated input components ─────────────────────────────────
// Validated cost input. Accepts ¥420, 420円, 1,200, $8.50 etc. Just checks
// that there's a number in there.
//
// Convenience: when the user types digits with no currency symbol
// (e.g. "420" or "1,200"), we auto-prefix the trip's defaultCurrency on
// blur. Only kicks in when no symbol is present AND the input is purely
// numeric — leaves "$8.50" / "420円" etc. untouched. Triggered on blur
// (not onChange) so typing isn't disrupted.
function CostInput({ value, onChange, placeholder, defaultCurrency }){
  const [v, setV] = React.useState(value || '');
  React.useEffect(()=>setV(value || ''), [value]);
  const cleaned = (v || '').replace(/[,¥$€£\s円元₩฿₫]/g, '').replace(/^A\$/,'').replace(/^CHF/i,'').trim();
  const num = Number(cleaned);
  const looksValid = !v || (cleaned !== '' && isFinite(num) && !isNaN(num));
  const symbol = defaultCurrency || '$';

  const onBlur = () => {
    const t = (v || '').trim();
    if (!t) return;
    // Has any currency symbol (¥/$/€/£/円/元/₩/฿/₫/A$/CHF)? Leave it alone.
    if (/[¥$€£円元₩฿₫]/.test(t) || /A\$/.test(t) || /CHF/i.test(t)) return;
    // Pure number (digits, commas, optional single decimal point)?
    if (/^\d{1,3}(,\d{3})*(\.\d+)?$|^\d+(\.\d+)?$/.test(t)){
      const next = symbol + t;
      setV(next);
      onChange(next);
    }
  };

  return (
    <div>
      <input type="text" value={v} maxLength={20}
             onChange={e=>{ setV(e.target.value); onChange(e.target.value); }}
             onBlur={onBlur}
             placeholder={placeholder || (symbol + '420')}
             style={{borderColor: !looksValid ? 'var(--danger)' : undefined, width:'100%'}}/>
      {!looksValid && (
        <div style={{fontSize:11, color:'var(--danger)', marginTop:3, fontWeight:600, display:'flex', alignItems:'center', gap:4}}>
          <window.TourIcon name="circle-alert" size={12} stroke="currentColor"/> No number found. e.g. <code>{symbol}420</code> / <code>1,200</code> / <code>500</code>
        </div>
      )}
    </div>
  );
}

// ── Node inspector ─────────────────────────────────────────────
function NodeInspector({node, allNodes, allEdges, defaultCurrency,
                        updateNode, deleteNode, duplicateNode, nodeGroups,
                        createGroupFromNode, updateGroup, deleteGroup,
                        setComposingGroupId, setSelection}){
  nodeGroups = nodeGroups || [];
  const fileRef = React.useRef(null);
  const onFile = async (e) => {
    const f = e.target.files?.[0];
    if (!f) return;
    try {
      const dataUrl = await compressImage(f);
      updateNode(node.id, { image: dataUrl });
    } catch(err) {
      console.error('Image compression failed', err);
    }
    e.target.value = '';
  };

  return (
    <>
      <h3>Edit place</h3>
      <div className="card">
        <div className="field">
          <label>Name</label>
          <input type="text" value={node.name||''} maxLength={40}
                 onChange={e=>updateNode(node.id,{name:e.target.value})}
                 placeholder="Station / sights name, etc."/>
        </div>
        <div className="field">
          <label>Subtitle (shown small below the name)</label>
          <input type="text" value={node.subtitle||''} maxLength={60}
                 onChange={e=>updateNode(node.id,{subtitle:e.target.value})}
                 placeholder="e.g. Transfer / 1 night / famous ○○"/>
        </div>
      </div>

      <div className="card warn">
        <h3 style={{marginTop:0}}>Arrival · Departure · Cost</h3>

        {(() => {
          const prev = findPrevNode(node.id, allNodes, allEdges);
          const arr = node.arrival || '';
          const dep = node.departure || '';
          const dt = window.tourDateTime;

          // Lower bounds for the date pickers — block picking times that
          // would travel backward through the edge graph (a frequent typo
          // when manually entering datetimes).
          //   - arrival's min = the latest predecessor's departure (across
          //     ALL incoming edges, not just the lowest-seq one)
          //   - departure's min = max(this node's arrival, arrival's min)
          const minArr = getEarliestArrivalBoundary(node.id, allNodes, allEdges);
          const minDep = arr || minArr || null;

          // Validation (still surfaces errors for typed-in values that bypass
          // the picker's `min` enforcement, especially on older browsers).
          let arrErr = null, depErr = null;
          if (arr && dep && dt){
            const m = dt.minutesBetween(arr, dep);
            if (m != null && m < 0) depErr = '⚠ Departure is earlier than arrival';
          }
          if (minArr && arr && dt){
            const m = dt.minutesBetween(minArr, arr);
            if (m != null && m < 0) arrErr = `⚠ Arrival is earlier than the previous step's departure (${dt.formatTime(minArr)})`;
          }

          // Quick-fill action sets
          const arrQuick = [
            { label: prev?.name ? `Same as "${prev.name}" departure` : 'Same as previous departure',
              value: prev?.departure || null,
              title: prev?.departure ? dt?.formatTime(prev.departure) : '' },
            arr ? { label: 'Clear', value: '' } : null,
          ].filter(Boolean);

          const depQuick = arr ? [
            { label: 'Arr +15m',  value: dt?.addMinutes(arr, 15) },
            { label: '+30m',     value: dt?.addMinutes(arr, 30) },
            { label: '+1h',      value: dt?.addMinutes(arr, 60) },
            { label: '+2h',      value: dt?.addMinutes(arr, 120) },
            dep ? { label: 'Clear', value: '' } : null,
          ].filter(Boolean) : (dep ? [{ label:'Clear', value:'' }] : []);

          // Compose hints — surface the constraint reason so the user
          // understands why earlier dates are unavailable.
          const arrHintBoundary = minArr && dt
            ? `Only ${dt.formatTime(minArr)} (previous departure) and later`
            : null;
          const depHintBoundary = arr && dt
            ? `Only ${dt.formatTime(arr)} (arrival) and later`
            : (minArr && dt ? `Only ${dt.formatTime(minArr)} (previous departure) and later` : null);

          return (
            <>
              <div className="field">
                <label>
                  <span className="field-icon arrival" aria-hidden="true">
                    <window.TourIcon name="flag" size={11} stroke="currentColor"/>
                  </span>
                  Arrival
                </label>
                <DateTimeField value={arr}
                               onChange={(v)=>updateNode(node.id, {arrival:v})}
                               quickActions={arrQuick}
                               error={arrErr}
                               min={minArr}
                               hint={!arr ? (arrHintBoundary || 'Leave empty if this is the starting point') : arrHintBoundary}/>
              </div>

              <div className="field">
                <label>
                  <span className="field-icon departure" aria-hidden="true">
                    <window.TourIcon name="flag-triangle-right" size={11} stroke="currentColor"/>
                  </span>
                  Departure
                </label>
                <DateTimeField value={dep}
                               onChange={(v)=>updateNode(node.id, {departure:v})}
                               quickActions={depQuick}
                               error={depErr}
                               min={minDep}
                               hint={!dep ? (depHintBoundary || 'Leave empty if this is the final stop') : depHintBoundary}/>
              </div>

              {arr && dep && !depErr && dt && (() => {
                const m = dt.minutesBetween(arr, dep);
                if (m == null || m <= 0) return null;
                return (
                  <div style={{fontSize:12, color:'var(--ink-soft)', lineHeight:1.5,
                               background:'var(--bg-mint)', padding:'6px 10px',
                               borderRadius:6, marginTop:-2, marginBottom:10,
                               border:'1px solid var(--green-light)'}}>
                    <window.TourIcon name="clock" size={12} stroke="currentColor"/> Stay at this place: <b style={{color:'var(--green-deep)'}}>{dt.formatDuration(m)}</b>
                  </div>
                );
              })()}
            </>
          );
        })()}

        <div className="field">
          {(() => {
            // Auto-detected category from icon (bed/tent → Lodging, else → Misc).
            // node.costCategory is an optional explicit override that supports
            // 4 expense buckets: Lodging / Misc / Meeting / Entertainment.
            const auto = (node.icon === 'bed' || node.icon === 'tent') ? 'lodging' : 'misc';
            const current = node.costCategory || auto;
            const isOverride = node.costCategory && node.costCategory !== auto;
            const labelOf = (k) => ({lodging:'Lodging', misc:'Misc', meeting:'Meeting', entertainment:'Entertainment'}[k] || 'Misc');
            const colorOf = (k) => ({
              lodging: { bg:'#F5F3FF', fg:'#7C3AED' },
              misc:    { bg:'#FFFBEB', fg:'#B45309' },
              meeting: { bg:'#EFF6FF', fg:'#2563EB' },
              entertainment: { bg:'#FDF2F8', fg:'#DB2777' },
            }[k] || { bg:'#FFFBEB', fg:'#B45309' });
            const c = colorOf(current);
            // Toggle helper: clicking the auto-derived category clears the
            // explicit override (returns to auto-mode).
            const setCat = (k) => updateNode(node.id, { costCategory: k === auto ? undefined : k });
            return (
              <>
                <label style={{display:'flex', alignItems:'center', gap:8, justifyContent:'space-between'}}>
                  <span style={{display:'inline-flex', alignItems:'center', gap:0}}>
                    <span className="field-icon cost" aria-hidden="true">
                      <window.TourIcon name="wallet" size={11} stroke="currentColor"/>
                    </span>
                    Cost
                  </span>
                  <span style={{
                    fontSize:10, fontWeight:700, padding:'2px 7px', borderRadius:4,
                    background: c.bg, color: c.fg,
                    letterSpacing:'.04em',
                  }}>
                    {labelOf(current)}
                    {!isOverride && <span style={{opacity:.7, fontWeight:500, marginLeft:4}}>(auto)</span>}
                  </span>
                </label>
                <CostInput value={node.cost || ''}
                           onChange={(v)=>updateNode(node.id, {cost:v})}
                           defaultCurrency={defaultCurrency}
                           placeholder={(defaultCurrency || '$') + '500'}/>
                <div className="seg" style={{marginTop:6, flexWrap:'wrap'}}>
                  <button className={current==='lodging'?'on':''}
                          onClick={()=>setCat('lodging')}
                          title="Set as Lodging">
                    <window.TourIcon name="bed" size={12}/> Lodging
                  </button>
                  <button className={current==='misc'?'on':''}
                          onClick={()=>setCat('misc')}
                          title="Set as Misc">
                    <window.TourIcon name="wallet" size={12}/> Misc
                  </button>
                  <button className={current==='meeting'?'on':''}
                          onClick={()=>setCat('meeting')}
                          title="Set as Meeting (business)">
                    <window.TourIcon name="briefcase" size={12}/> Meeting
                  </button>
                  <button className={current==='entertainment'?'on':''}
                          onClick={()=>setCat('entertainment')}
                          title="Set as Entertainment (meals, etc.)">
                    <window.TourIcon name="utensils" size={12}/> Entertainment
                  </button>
                </div>
                <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.5, marginTop:4}}>
                  Default: <b>Hotel</b> / <b>Tent</b> icons → Lodging, otherwise Misc. Override to Meeting / Entertainment manually.
                  {isOverride && <span style={{color:'var(--accent-blue)', marginLeft:4}}>(manually overridden)</span>}
                </div>
              </>
            );
          })()}
        </div>

        <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.5, marginTop:-4}}>
          Edge transit time is auto-derived from the previous node's <b>departure</b> and the next node's <b>arrival</b>.<br/>
          Chips on the canvas can be dragged freely.
        </div>
      </div>

      <div className="card mint">
        <h3 style={{marginTop:0}}>Icon</h3>
          <div style={{display:'flex',gap:6,marginBottom:10, flexWrap:'wrap'}}>
            <button className="btn sm" onClick={()=>fileRef.current.click()}>
              <window.TourIcon name="camera" size={14}/> Upload image
            </button>
            {node.image && <button className="btn sm ghost"
                                   onClick={()=>updateNode(node.id,{image:null})}>Clear image</button>}
            <input ref={fileRef} className="file-hidden" type="file" accept="image/*" onChange={onFile}/>
          </div>
          <IconPicker
            selected={node.icon}
            onSelect={(name)=>updateNode(node.id,{icon:name, image:null, emoji:null})}
            onClear={()=>updateNode(node.id,{icon:null})}
          />
          <div className="field" style={{marginTop:12}}>
            <label style={{display:'inline-flex', alignItems:'center', gap:6}}>
              <window.TourIcon name="star-fill" size={11}
                stroke={node.highlight || 'currentColor'}
                fill={node.highlight || 'transparent'}/>
              Highlight (important marker)
            </label>
            <div className="swatches">
              <button style={{background:'#fff', position:'relative'}}
                      className={!node.highlight?'on':''}
                      title="No highlight"
                      onClick={()=>updateNode(node.id,{highlight:null})}>
                <span style={{position:'absolute', inset:0, display:'grid', placeItems:'center', color:'var(--ink-muted)', fontSize:14}}>—</span>
              </button>
              {['#EAB308','#EF4444','#F97316','#10A37F','#3B82F6','#7C3AED','#EC4899','#1F2937'].map(c=>(
                <button key={c} style={{background:c}}
                        className={node.highlight===c?'on':''}
                        title={`Highlight ${c}`}
                        onClick={()=>updateNode(node.id,{highlight:c})}/>
              ))}
              {/* Rainbow custom picker — same pattern as background color */}
              <label className={'rainbow-swatch' + (
                       node.highlight && !['#EAB308','#EF4444','#F97316','#10A37F','#3B82F6','#7C3AED','#EC4899','#1F2937'].includes(node.highlight)
                         ? ' on' : ''
                     )}
                     title="Custom color"
                     style={{
                       width:26, height:26, borderRadius:6,
                       background:'conic-gradient(from 0deg, #EAB308, #EF4444, #F97316, #10A37F, #3B82F6, #7C3AED, #EC4899, #EAB308)',
                       border:'1px solid var(--line)',
                       cursor:'pointer', position:'relative', display:'inline-block', flexShrink:0,
                     }}>
                <input type="color" value={node.highlight || '#10A37F'}
                       onChange={e=>updateNode(node.id,{highlight:e.target.value},{skipHistory:true})}
                       onBlur={e=>updateNode(node.id,{highlight:e.target.value})}
                       style={{position:'absolute', inset:0, width:'100%', height:'100%', opacity:0, cursor:'pointer', border:'none', padding:0}}/>
              </label>
            </div>
            <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.5, marginTop:4, marginBottom:8}}>
              Mark special places with a colored ring — "trip highlight", "must-do meeting", etc. Schedule view picks this up too.
            </div>
          </div>
          <div className="field" style={{marginTop:0}}>
            <label>Background color</label>
            <div className="swatches">
              {NODE_COLORS.map(c=>(
                <button key={c} style={{background:c}}
                        className={node.color===c?'on':''}
                        onClick={()=>updateNode(node.id,{color:c})}/>
              ))}
              <label className={'rainbow-swatch' + (NODE_COLORS.indexOf(node.color)<0 ? ' on' : '')}
                     title="Custom color"
                     style={{
                       width:26, height:26, borderRadius:6,
                       background:'conic-gradient(from 0deg, #10A37F, #3B82F6, #7C3AED, #F59E0B, #EF4444, #10A37F)',
                       border:'1px solid var(--line)',
                       cursor:'pointer', position:'relative', display:'inline-block', flexShrink:0,
                     }}>
                <input type="color" value={node.color || '#FFFFFF'}
                       onChange={e=>updateNode(node.id,{color:e.target.value},{skipHistory:true})}
                       onBlur={e=>updateNode(node.id,{color:e.target.value})}
                       style={{position:'absolute', inset:0, width:'100%', height:'100%', opacity:0, cursor:'pointer', border:'none', padding:0}}/>
              </label>
            </div>
          </div>
          <div className="field">
            <label>Shape</label>
            <div className="seg">
              {[['circle','Circle'],['square','Square'],['bubble','Bubble']].map(([v,l])=>(
                <button key={v} className={node.shape===v || (!node.shape && v==='circle')?'on':''}
                        onClick={()=>updateNode(node.id,{shape:v})}>{l}</button>
              ))}
            </div>
          </div>
          <div className="field">
            <label>Name position</label>
            <div className="seg">
              {[['top','Top'], ['left','Left'], ['bottom','Bottom'], ['right','Right']].map(([v,l])=>(
                <button key={v} className={(node.labelPos || 'bottom')===v?'on':''}
                        onClick={()=>updateNode(node.id,{labelPos:v})}>{l}</button>
              ))}
            </div>
          </div>
        </div>

      <div style={{display:'flex', gap:6, marginBottom:8}}>
        {duplicateNode && (
          <button className="btn" onClick={()=>duplicateNode(node.id)}
                  title="Duplicate this place (copy with same icon, color, and times)">
            <window.TourIcon name="circle-dot" size={14}/> Duplicate
          </button>
        )}
        <button className="btn danger" onClick={()=>deleteNode(node.id)} style={{flex:1}}>
          <window.TourIcon name="trash-2" size={14}/> Delete place
        </button>
      </div>

      {/* Group membership */}
      {nodeGroups.length > 0 ? (
        <div className="card" style={{background:'var(--bg-soft)', border:'1px dashed var(--line)'}}>
          <div style={{fontSize:11, fontWeight:700, color:'var(--ink-soft)', marginBottom:8, textTransform:'uppercase', letterSpacing:'.04em'}}>
            Member of ({nodeGroups.length})
          </div>
          {nodeGroups.map(ng => (
            <div key={ng.id} style={{display:'flex', alignItems:'center', gap:8, marginBottom:6, background:'var(--paper)', padding:'5px 9px', borderRadius:7, border:'1px solid var(--line)'}}>
              <span style={{width:10, height:10, borderRadius:'50%', background:ng.color, display:'inline-block', flexShrink:0}}/>
              <span style={{flex:1, fontWeight:700, fontSize:12, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{ng.name || '(no name)'}</span>
              <button className="btn sm ghost" title="Edit"
                      style={{padding:'2px 6px', fontSize:11}}
                      onClick={()=>setSelection && setSelection({type:'group', id:ng.id})}>✎</button>
              <button className="btn sm ghost" title="Remove from this group"
                      style={{padding:'2px 6px', fontSize:11, color:'var(--danger)'}}
                      onClick={()=>{
                        const nextIds = (ng.nodeIds||[]).filter(x => x !== node.id);
                        if (nextIds.length === 0) { deleteGroup && deleteGroup(ng.id); }
                        else { updateGroup && updateGroup(ng.id, {nodeIds: nextIds}); }
                      }}>×</button>
            </div>
          ))}
          <button className="btn sm block" style={{marginTop:6}}
                  onClick={()=>{
                    const gid = createGroupFromNode && createGroupFromNode(node.id);
                    if (gid) { setComposingGroupId && setComposingGroupId(gid); }
                  }}>
            ＋ Add to another group
          </button>
        </div>
      ) : (
        <button className="btn"
                onClick={()=>{
                  const gid = createGroupFromNode && createGroupFromNode(node.id);
                  if (gid) { setComposingGroupId && setComposingGroupId(gid); }
                }}>
          <window.TourIcon name="calendar" size={14}/> Add to a Day group
        </button>
      )}
    </>
  );
}

// ── Icon picker (Lucide) ───────────────────────────────────────
function IconPicker({ selected, onSelect, onClear }){
  return (
    <div className="field">
      <label>Icon (Lucide)</label>
      <div style={{maxHeight:240, overflowY:'auto', padding:'4px 2px', border:'1px solid var(--line)', borderRadius:8, background:'var(--paper)'}}>
        <button className="btn sm ghost" onClick={onClear}
                style={{margin:'4px', padding:'4px 8px'}}>— No icon</button>
        {window.TOUR_ICON_GROUPS?.map(group => (
          <div key={group.id} style={{padding:'0 4px 4px'}}>
            <div className="icon-group-label">{group.label}</div>
            <div className="icon-grid">
              {group.icons.map(name => (
                <button key={name}
                        className={selected===name?'on':''}
                        title={name}
                        onClick={()=>onSelect(name)}>
                  <window.TourIcon name={name} size={18}/>
                </button>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ── Edge inspector ─────────────────────────────────────────────
// Reverse-direction button — looks grayed-out (not strictly disabled) when
// reversal would produce a backward-in-time edge. Clicking the grayed
// version reveals the warning instead of always showing it inline.
function ReverseEdgeButton({ edge, from, to, updateEdge }){
  const dt = window.tourDateTime;
  const newFromTime = to?.departure   || to?.arrival;
  const newToTime   = from?.arrival   || from?.departure;
  let blocked = false, blockReason = '';
  if (dt && newFromTime && newToTime){
    const m = dt.minutesBetween(newFromTime, newToTime);
    if (m != null && m < 0){
      blocked = true;
      blockReason = `Reversing would make ${to?.name || 'destination'} (${dt.formatTime(newFromTime)}) → ${from?.name || 'origin'} (${dt.formatTime(newToTime)}) — that goes backward in time`;
    }
  }
  const [showReason, setShowReason] = React.useState(false);
  // Hide the warning if blocked-state changes (e.g. user fixed the times).
  React.useEffect(() => { if (!blocked) setShowReason(false); }, [blocked]);
  return (
    <>
      <div style={{display:'flex', gap:6, marginTop:6}}>
        <button className="btn sm ghost"
                title={blocked ? 'Click to see why' : 'Swap origin and destination'}
                style={blocked ? {opacity:0.45, cursor:'help'} : null}
                onClick={()=>{
                  if (blocked) { setShowReason(true); return; }
                  updateEdge(edge.id, {from:edge.to, to:edge.from});
                }}>
          <window.TourIcon name="arrow-left-right" size={12}/> Reverse direction
        </button>
        <div style={{flex:1}}/>
      </div>
      {blocked && showReason && (
        <div style={{fontSize:11, color:'var(--danger)', lineHeight:1.5, marginTop:4, fontWeight:600,
                     display:'flex', alignItems:'flex-start', gap:4}}>
          <window.TourIcon name="circle-alert" size={12} stroke="currentColor"/>
          <span>{blockReason}</span>
        </div>
      )}
    </>
  );
}

function EdgeInspector({edge, totalEdges, edges, nodes, defaultCurrency, updateEdge, deleteEdge, edgePickMode, startEdgeRetargetPick}){
  const from = nodes.find(n=>n.id===edge.from);
  const to = nodes.find(n=>n.id===edge.to);
  const rel = RELATIONS.find(r=>r.id===edge.relation) || RELATIONS[0];
  const isOrth = edge.style === 'orth-h' || edge.style === 'orth-v';
  // Auto-derived transit minutes from the from-node's departure to the
  // to-node's arrival. Read-only — the user fills these times on the nodes.
  const transitMins = (window.tourDateTime && from?.departure && to?.arrival)
    ? window.tourDateTime.minutesBetween(from.departure, to.arrival)
    : null;

  return (
    <>
      <h3>Edit edge</h3>
      <div className="card info">
        {/* Endpoint editing — click-to-pick a new from/to node, or swap. */}
        <div className="field" style={{marginBottom:10}}>
          <label>Edge endpoints</label>
          {/* min-width:0 + display:flex on the button is required for the
              inner span to ellipsis-truncate inside a 1fr grid cell — without
              them, a long node name expands the cell and breaks the layout. */}
          <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:6, minWidth:0}}>
            <button className={`btn sm${edgePickMode?.edgeId===edge.id && edgePickMode.endpoint==='from' ? ' primary' : ''}`}
                    onClick={()=>startEdgeRetargetPick && startEdgeRetargetPick(edge.id, 'from')}
                    title="Click, then tap a new origin place on the canvas"
                    style={{padding:'8px', display:'flex', flexDirection:'column', alignItems:'flex-start', gap:2, height:'auto', textAlign:'left', minWidth:0, overflow:'hidden'}}>
              <span style={{fontSize:10, color:'var(--ink-muted)', fontWeight:700, letterSpacing:'.04em'}}>FROM ✎</span>
              <span style={{fontSize:12, fontWeight:800, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:'100%', alignSelf:'stretch'}}>
                {from?.name || '?'}
              </span>
            </button>
            <button className={`btn sm${edgePickMode?.edgeId===edge.id && edgePickMode.endpoint==='to' ? ' primary' : ''}`}
                    onClick={()=>startEdgeRetargetPick && startEdgeRetargetPick(edge.id, 'to')}
                    title="Click, then tap a new destination place on the canvas"
                    style={{padding:'8px', display:'flex', flexDirection:'column', alignItems:'flex-start', gap:2, height:'auto', textAlign:'left', minWidth:0, overflow:'hidden'}}>
              <span style={{fontSize:10, color:'var(--ink-muted)', fontWeight:700, letterSpacing:'.04em'}}>TO ✎</span>
              <span style={{fontSize:12, fontWeight:800, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:'100%', alignSelf:'stretch'}}>
                {to?.name || '?'}
              </span>
            </button>
          </div>
          <ReverseEdgeButton edge={edge} from={from} to={to} updateEdge={updateEdge}/>
          <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.5, marginTop:4}}>
            Click ✎ then tap a new place on the map to re-route this edge — handy for plan changes or insertions.
          </div>
        </div>


        {/* (Manual sequence-number UI removed — order is auto-derived from
            each node's arrival/departure times. Numbers shuffle when you
            edit times.) */}

        <div className="field">
          <label>Transport mode</label>
          <div className="seg">
            {RELATIONS.map(r=>(
              <button key={r.id}
                      className={edge.relation===r.id?'on':''}
                      title={r.label}
                      onClick={()=>updateEdge(edge.id,{
                        relation:r.id, color:r.color, label:r.label,
                        stamp:r.stamp, style: r.defaultStyle || edge.style,
                      })}>
                {r.stamp && window.TourIcon && <window.TourIcon name={r.stamp} size={12}/>}
                {r.label}
              </button>
            ))}
          </div>
        </div>

        <div className="field">
          <label>Label (free input)</label>
          <input type="text" value={edge.label ?? rel.label} maxLength={40}
                 onChange={e=>updateEdge(edge.id,{label:e.target.value})}
                 placeholder="e.g. Yamanote Line / Kodama / Walk"/>
        </div>

        <div className="field">
          <label>Stamp icon (shown at the start of the label)</label>
          <div style={{display:'flex', flexWrap:'wrap', gap:4}}>
            <button className={`btn sm${!edge.stamp?' primary':''}`}
                    style={{padding:'5px 10px'}}
                    onClick={()=>updateEdge(edge.id,{stamp:''})}>None</button>
            {STAMPS.map(name=>(
              <button key={name} className="btn sm"
                      style={{
                        padding:6, width:30, height:30, justifyContent:'center',
                        background: edge.stamp===name ? 'var(--green)' : 'var(--paper)',
                        color: edge.stamp===name ? '#fff' : 'var(--ink)',
                        borderColor: edge.stamp===name ? 'var(--green)' : 'var(--line)',
                      }}
                      title={name}
                      onClick={()=>updateEdge(edge.id,{stamp:name})}>
                {window.TourIcon && <window.TourIcon name={name} size={16}/>}
              </button>
            ))}
          </div>
        </div>
      </div>

      {/* Cost only — transit time is auto-derived from connected nodes' arrival/departure */}
      <div className="card warn">
        <h3 style={{marginTop:0}}>Travel cost / duration</h3>

        <div className="field">
          <label>Travel cost / Fare</label>
          <CostInput value={edge.cost || ''}
                     onChange={(v)=>updateEdge(edge.id,{cost:v})}
                     defaultCurrency={defaultCurrency}
                     placeholder={(defaultCurrency || '$') + '420 / 1,200'}/>
        </div>

        {/* Auto-derived transit time — read-only readout */}
        <div className="field" style={{marginBottom:0}}>
          <label>Transit duration (auto-derived)</label>
          <div style={{
            background:'var(--paper)', border:'1px solid var(--line)',
            borderRadius:8, padding:'7px 10px', fontSize:13,
            color: transitMins != null && transitMins > 0 ? 'var(--ink)' : 'var(--ink-muted)',
          }}>
            {transitMins == null
              ? <span>Set the departure of "{from?.name||'?'}" and the arrival of "{to?.name||'?'}" to auto-calculate</span>
              : transitMins <= 0
                ? <span style={{color:'var(--danger)', display:'inline-flex', alignItems:'center', gap:4}}>
                  <window.TourIcon name="circle-alert" size={12} stroke="currentColor"/> Departure is later than arrival
                </span>
                : <span><b>{window.tourDateTime?.formatDuration(transitMins)}</b>
                    <span style={{color:'var(--ink-soft)', marginLeft:8, fontSize:11}}>
                      ({window.tourDateTime?.formatTime(from.departure)} → {window.tourDateTime?.formatTime(to.arrival)})
                    </span>
                  </span>}
          </div>
        </div>

        <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.5, marginTop:8}}>
          Chips can be dragged freely (a dashed link appears when detached).
        </div>
        {edge.annoOffset && (Math.abs(edge.annoOffset.dx)>2 || Math.abs(edge.annoOffset.dy)>2) && (
          <button className="btn sm" style={{marginTop:6}}
                  onClick={()=>updateEdge(edge.id,{annoOffset: undefined})}>
            ↺ Reset chip position
          </button>
        )}
      </div>

      <div className="card">
        <h3 style={{marginTop:0}}>Arrow style</h3>
        <div className="seg">
          {/* twoway (Two-way ⇄) is deprecated — rendering still works for
              legacy data, but the picker hides it so new edges can't use it. */}
          {ARROW_STYLES.filter(s => s.id !== 'twoway').map(s=>(
            <button key={s.id} className={edge.style===s.id?'on':''}
                    onClick={()=>updateEdge(edge.id,{style:s.id})}>{s.label}</button>
          ))}
        </div>
        {isOrth && (
          <div className="field" style={{marginTop:10, marginBottom:0}}>
            <label>L-corner radius (also adjustable by drag handle)</label>
            <div style={{display:'flex', gap:8, alignItems:'center'}}>
              <input type="range" min={0} max={80} step={1}
                     value={typeof edge.cornerR === 'number' ? edge.cornerR : 8}
                     onChange={e=>updateEdge(edge.id,{cornerR: Number(e.target.value)},{skipHistory:true})}
                     onMouseUp={e=>updateEdge(edge.id,{cornerR: Number(e.target.value)})}
                     onTouchEnd={e=>updateEdge(edge.id,{cornerR: Number(e.target.value)})}
                     style={{flex:1}}/>
              <span style={{fontSize:12, fontWeight:700, color:'var(--ink-soft)', minWidth:38, textAlign:'right'}}>
                {Math.round(typeof edge.cornerR === 'number' ? edge.cornerR : 8)}
              </span>
            </div>
            <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.4, marginTop:4}}>
              0 = right angle; larger = smoother corner.<br/>
              Or drag the <b style={{color:'var(--green-deep)'}}>○ corner handle</b> on the canvas inward.
            </div>
          </div>
        )}
        <div className="field" style={{marginTop:10}}>
          <label>Line color</label>
          <div className="swatches">
            {EDGE_COLOR_PRESETS.map(p=>(
              <button key={p.color} style={{background:p.color}}
                      className={edge.color===p.color?'on':''}
                      title={p.label}
                      onClick={()=>updateEdge(edge.id,{color:p.color})}/>
            ))}
            <label className={'rainbow-swatch' + (EDGE_COLOR_PRESETS.findIndex(p => p.color === edge.color) < 0 ? ' on' : '')}
                   title="Custom color"
                   style={{
                     width:26, height:26, borderRadius:6,
                     background:'conic-gradient(from 0deg, #10A37F, #3B82F6, #7C3AED, #F59E0B, #EF4444, #10A37F)',
                     border:'1px solid var(--line)',
                     cursor:'pointer', position:'relative', display:'inline-block', flexShrink:0,
                   }}>
              <input type="color" value={edge.color || '#10A37F'}
                     onChange={e=>updateEdge(edge.id,{color:e.target.value},{skipHistory:true})}
                     onBlur={e=>updateEdge(edge.id,{color:e.target.value})}
                     style={{position:'absolute', inset:0, width:'100%', height:'100%', opacity:0, cursor:'pointer', border:'none', padding:0}}/>
            </label>
          </div>
        </div>
        {!isOrth && edge.customOffset !== undefined && edge.customOffset !== 0 && (
          <button className="btn sm" style={{marginTop:8}}
                  onClick={()=>updateEdge(edge.id,{customOffset:undefined})}>↺ Reset bend</button>
        )}
      </div>

      {/* Arrow-delete button removed — arrows are auto-managed from each
          node's times. To remove a leg, change the connected nodes' times
          (or delete a node), and the arrow disappears automatically. */}
      <div style={{fontSize:11, color:'var(--ink-soft)', textAlign:'center', marginTop:2, lineHeight:1.6,
                   background:'var(--bg-mint-soft)', padding:'8px 10px', borderRadius:8,
                   border:'1px solid var(--green-light)'}}>
        <window.TourIcon name="zap" size={11} stroke="currentColor"/>{' '}
        Arrows are <b>auto-derived from each stop's times</b>. Individual arrows can't be deleted directly.<br/>
        To remove a leg, change the connected times or delete a stop.
      </div>

      <div style={{fontSize:11, color:'var(--ink-soft)', textAlign:'center', marginTop:8, lineHeight:1.6}}>
        💡 <b>Shift+click</b> another arrow to copy its settings (transit / color / fare / label …) onto this one
      </div>
    </>
  );
}

// ── Group inspector ────────────────────────────────────────────
function GroupInspector({group, nodes, updateGroup, deleteGroup, setComposingGroupId, setSelection}){
  const members = (group.nodeIds || [])
    .map(nid => nodes.find(n => n.id === nid))
    .filter(Boolean);
  const presets = ['#10A37F','#3B82F6','#F59E0B','#7C3AED','#EF4444','#0891B2','#84CC16','#1F2937'];

  return (
    <>
      <h3><window.TourIcon name="calendar" size={14} stroke="currentColor"/> Edit group</h3>
      <div className="card">
        <div className="field">
          <label>Group name</label>
          <input type="text" value={group.name||''} maxLength={40}
                 onChange={e=>updateGroup(group.id, {name:e.target.value})}
                 placeholder="Day 1 / Morning / etc."/>
        </div>
        <label className="pop-check" style={{borderTop:'none', padding:0}}>
          <input type="checkbox" checked={group.showName !== false}
                 onChange={e=>updateGroup(group.id, {showName:e.target.checked})}/>
          Show name on canvas
        </label>
      </div>

      <div className="card mint">
        <h3 style={{marginTop:0}}>Color</h3>
        <div className="swatches">
          {presets.map(c=>(
            <button key={c} style={{background:c}}
                    className={group.color===c?'on':''}
                    onClick={()=>updateGroup(group.id,{color:c})}/>
          ))}
          <label className={'rainbow-swatch' + (presets.indexOf(group.color)<0 ? ' on' : '')}
                 title="Custom color"
                 style={{
                   width:26, height:26, borderRadius:6,
                   background:'conic-gradient(from 0deg, #10A37F, #3B82F6, #7C3AED, #F59E0B, #EF4444, #10A37F)',
                   border:'1px solid var(--line)',
                   cursor:'pointer', position:'relative', display:'inline-block', flexShrink:0,
                 }}>
            <input type="color" value={group.color || '#10A37F'}
                   onChange={e=>updateGroup(group.id,{color:e.target.value},{skipHistory:true})}
                   onBlur={e=>updateGroup(group.id,{color:e.target.value})}
                   style={{position:'absolute', inset:0, width:'100%', height:'100%', opacity:0, cursor:'pointer', border:'none', padding:0}}/>
          </label>
        </div>
      </div>

      <div className="card info">
        <h3 style={{marginTop:0}}>Members ({members.length})</h3>
        <div style={{display:'flex', flexDirection:'column', gap:5}}>
          {members.map(n => (
            <div key={n.id} style={{display:'flex', alignItems:'center', gap:8, background:'var(--paper)', padding:'5px 9px', borderRadius:7, border:'1px solid var(--line)'}}>
              <span style={{width:18, height:18, borderRadius:5, background:n.color, display:'grid', placeItems:'center', flexShrink:0, border:'1px solid var(--line)'}}>
                {n.icon ? <window.TourIcon name={n.icon} size={12} stroke="var(--ink)"/> : (n.emoji || (n.name||'?').charAt(0).toUpperCase())}
              </span>
              <span style={{flex:1, fontSize:12, fontWeight:600, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{n.name || '(no name)'}</span>
              <button className="btn sm ghost" title="Remove from group"
                      onClick={()=>{
                        const next = (group.nodeIds||[]).filter(x => x !== n.id);
                        if (next.length === 0) { deleteGroup(group.id); return; }
                        updateGroup(group.id, {nodeIds: next});
                      }}>×</button>
            </div>
          ))}
        </div>
        <button className="btn sm block" style={{marginTop:8}}
                onClick={()=>{ setComposingGroupId && setComposingGroupId(group.id); }}>
          ＋ Add / edit members
        </button>
      </div>

      <button className="btn danger" onClick={()=>deleteGroup(group.id)}>
        <window.TourIcon name="trash-2" size={14}/> Delete group
      </button>
    </>
  );
}

// ── Note inspector ─────────────────────────────────────────────
function NoteInspector({note, nodes, edges, updateNote, deleteNote, duplicateNote,
                        notePickMode, startNoteAnchorPick}){
  const colors = ['#FFFBEB','#F0FDF4','#F0F9FF','#FAF5FF','#FEF2F2','#FFF7ED','#F8FAFC'];
  const anchorType = note.anchor?.type || '';
  const anchorId   = note.anchor?.id || '';
  // Resolve the anchor target's display name for the summary card.
  let anchorName = '';
  if (anchorType === 'node'){
    anchorName = nodes.find(n => n.id === anchorId)?.name || '(deleted)';
  } else if (anchorType === 'edge'){
    const e = edges.find(x => x.id === anchorId);
    if (e){
      const f = nodes.find(n => n.id === e.from)?.name || '?';
      const t = nodes.find(n => n.id === e.to)?.name   || '?';
      anchorName = `#${e.seq ?? ''} ${e.label || ''} (${f} → ${t})`;
    } else anchorName = '(deleted)';
  }
  const isPickingThis = notePickMode && notePickMode.noteId === note.id;
  return (
    <>
      <h3><window.TourIcon name="sticky-note" size={14} stroke="currentColor"/> Edit note</h3>
      <div className="card warn">
        <div className="field">
          <label>Text</label>
          <textarea rows={6} value={note.text||''} maxLength={500}
                    onChange={e=>updateNote(note.id,{text:e.target.value})}
                    placeholder="Notes / cautions / booking memo&#10;&#10;Line breaks supported"/>
        </div>

        {/* Anchor: pick a node or edge by clicking it on the canvas. */}
        <div className="field">
          <label>Linked target (dashed connection)</label>

          {anchorType ? (
            // Currently anchored — show summary card + actions
            <div style={{
              background:'var(--paper)', border:'1px solid var(--line)',
              borderRadius:8, padding:'8px 10px', display:'flex',
              alignItems:'center', gap:8, fontSize:12,
            }}>
              <window.TourIcon
                name={anchorType === 'node' ? 'map-pin' : 'route'}
                size={14}
                stroke={anchorType === 'node' ? '#7C3AED' : 'var(--green-deep)'}/>
              <span style={{flex:1, fontWeight:600, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>
                {anchorType === 'node' ? 'Place: ' : 'Arrow: '}
                <b>{anchorName}</b>
              </span>
              <button className="btn sm ghost" title="Change link"
                      style={{padding:'2px 8px', fontSize:11}}
                      onClick={()=>startNoteAnchorPick && startNoteAnchorPick(note.id, anchorType)}>
                <window.TourIcon name="pencil" size={11}/> Change
              </button>
              <button className="btn sm ghost" title="Unlink"
                      style={{padding:'2px 8px', fontSize:11, color:'var(--danger)'}}
                      onClick={()=>updateNote(note.id, {anchor: null})}>
                <window.TourIcon name="x" size={11}/>
              </button>
            </div>
          ) : (
            // No anchor yet — click-to-pick buttons
            <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:6}}>
              <button className={`btn${isPickingThis && notePickMode?.type==='node' ? ' primary' : ''}`}
                      onClick={()=>startNoteAnchorPick && startNoteAnchorPick(note.id, 'node')}
                      style={{padding:'8px 10px', fontSize:12}}>
                <window.TourIcon name="map-pin" size={13}/> Link to a place
              </button>
              <button className={`btn${isPickingThis && notePickMode?.type==='edge' ? ' primary' : ''}`}
                      onClick={()=>startNoteAnchorPick && startNoteAnchorPick(note.id, 'edge')}
                      style={{padding:'8px 10px', fontSize:12}}>
                <window.TourIcon name="route" size={13}/> Link to an arrow
              </button>
            </div>
          )}

          <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.5, marginTop:6}}>
            Press the button, then click a <b>place</b> or <b>arrow</b> to link.
          </div>
        </div>
        <div className="field">
          <label>Background color</label>
          <div className="swatches">
            {colors.map(c=>(
              <button key={c} style={{background:c, border:'1px solid var(--line)'}}
                      className={note.color===c?'on':''}
                      onClick={()=>updateNote(note.id,{color:c})}/>
            ))}
          </div>
        </div>
      </div>
      <div style={{display:'flex', gap:6}}>
        {duplicateNote && (
          <button className="btn" onClick={()=>duplicateNote(note.id)}
                  title="Duplicate this note (copy text, color, and link)">
            <window.TourIcon name="sticky-note" size={14}/> Duplicate
          </button>
        )}
        <button className="btn danger" onClick={()=>deleteNote(note.id)} style={{flex:1}}>
          <window.TourIcon name="trash-2" size={14}/> Delete note
        </button>
      </div>
    </>
  );
}

window.Inspector = Inspector;
