// 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}}>はじめかた</h3>
          <div style={{fontSize:12, lineHeight:1.7, color:'var(--ink-soft)'}}>
            ・ツールバーの<b style={{color:'var(--green-deep)'}}>＋ノード</b>で地点を追加<br/>
            ・<b style={{color:'var(--green-deep)'}}>＋ノード ▾</b>で駅・空港・ホテルなどから選んで追加<br/>
            ・地点アイコン右上の<b style={{color:'var(--green-deep)'}}>＋</b>を別の地点へドラッグ → 矢印で接続<br/>
            ・矢印・ノード・メモをクリックで編集
          </div>
        </div>
        <div style={{display:'flex',gap:6, flexDirection:'column'}}>
          <button className="btn primary" onClick={()=>addNode({type:'pin'})}>＋ ノードを追加</button>
          <button className="btn" onClick={()=>addNote()}>
            <window.TourIcon name="sticky-note" size={14}/>
            メモを追加
          </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="各ノードの到着・出発日時から「1日目／2日目…」のグループを自動生成">
              <window.TourIcon name="calendar" size={14}/>
              日付でグループ自動生成
            </button>
          )}
          <button className="btn" onClick={()=>onLoadDemo('travel')}>
            <window.TourIcon name="sparkles" size={14}/> サンプル旅程
          </button>
          <button className="btn" onClick={()=>onLoadDemo('business')}>
            <window.TourIcon name="briefcase" size={14}/> サンプル出張
          </button>
          <button className="btn ghost" onClick={clearAll}>
            <window.TourIcon name="eraser" size={14}/> すべてクリア
          </button>
        </div>
        <div className="empty-hint">
          何も選択されていません。<br/>キャンバスのノード・矢印・メモをクリックしてください。
        </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 ─────────────────────────────────
// StayTimeInput: number field + unit picker (分 / 時間 / 日). Compose the
// text value as `${num}${unit}`. Free-form compound text like "1時間30分" is
// preserved as-is — we mark it valid since the SummaryBar parser handles it.
function StayTimeInput({ value, onChange }){
  const initial = parseStayTimeForInput(value);
  const [num, setNum] = React.useState(initial.num);
  const [unit, setUnit] = React.useState(initial.unit);
  const [freeForm, setFreeForm] = React.useState(initial.freeForm);
  const [err, setErr] = React.useState(false);

  // Re-sync from outside (e.g. when user switches between nodes)
  React.useEffect(()=>{
    const p = parseStayTimeForInput(value);
    setNum(p.num); setUnit(p.unit); setFreeForm(p.freeForm); setErr(false);
  }, [value]);

  const commit = (newNum, newUnit) => {
    if (newNum === '' || newNum == null){ onChange(''); setErr(false); return; }
    const n = Number(newNum);
    if (!isFinite(n) || isNaN(n)) { setErr(true); return; }
    setErr(false);
    onChange(`${n}${newUnit}`);
  };

  if (freeForm){
    // Compound entry like "1時間30分" — keep editable as plain text, no unit picker.
    return (
      <div>
        <input type="text" value={value} maxLength={20}
               onChange={e=>onChange(e.target.value)}
               style={{width:'100%'}}
               placeholder="30分"/>
        <div style={{fontSize:11, color:'var(--ink-soft)', marginTop:3}}>
          複合表記が検出されました。<button className="btn sm ghost" style={{padding:'2px 8px'}}
                  onClick={()=>{ setFreeForm(false); onChange(''); }}>単純入力に戻す</button>
        </div>
      </div>
    );
  }

  return (
    <div>
      <div style={{display:'flex', gap:6, alignItems:'center'}}>
        <input type="text" inputMode="decimal" value={num} maxLength={8}
               onChange={e=>{ setNum(e.target.value); commit(e.target.value, unit); }}
               style={{width:90, borderColor: err ? 'var(--danger)' : undefined}}
               placeholder="30"/>
        <div className="seg" style={{flex:1}}>
          {['分','時間','日'].map(u=>(
            <button key={u} className={unit===u?'on':''}
                    onClick={()=>{ setUnit(u); commit(num, u); }}>{u}</button>
          ))}
        </div>
      </div>
      {err && (
        <div style={{fontSize:11, color:'var(--danger)', marginTop:4, fontWeight:600, display:'flex', alignItems:'center', gap:4}}>
          <window.TourIcon name="circle-alert" size={12} stroke="currentColor"/> 数値で入力してください（例: 30 / 1.5 / 2）
        </div>
      )}
    </div>
  );
}

// Parse a stayTime string into { num, unit, freeForm }.
//   "30分"     → {num:'30',  unit:'分',    freeForm:false}
//   "1.5時間"  → {num:'1.5', unit:'時間',  freeForm:false}
//   "1時間30分" → {num:'',    unit:'分',    freeForm:true} ← compound form
//   ""         → {num:'',    unit:'分',    freeForm:false}
function parseStayTimeForInput(s){
  if (!s) return { num:'', unit:'分', freeForm:false };
  const txt = String(s).trim();
  // Compound form: contains 時間 AND a 分/分後 OR 時 AND 分
  if (/(時間|h).*(分|m)/.test(txt) || /(\d+).*(時間|h).*(\d+).*(分|m)/.test(txt)){
    return { num:'', unit:'分', freeForm:true };
  }
  const m = txt.match(/^(\d+(?:\.\d+)?)\s*(分|時間|日|h\b|m\b|d\b|min)?$/);
  if (m){
    let unit = m[2] || '分';
    if (unit === 'h') unit = '時間';
    else if (unit === 'm' || unit === 'min') unit = '分';
    else if (unit === 'd') unit = '日';
    return { num: m[1], unit, freeForm:false };
  }
  // Unparseable — but treat as free-form so the user sees their text.
  return { num:'', unit:'分', freeForm:true };
}

// Validated free-form duration input for edge.duration (transit time on a leg).
// Accepts compound forms like "1時間30分". Highlights the field if input
// can't be parsed at all.
function DurationInput({ value, onChange, placeholder }){
  const [v, setV] = React.useState(value || '');
  React.useEffect(()=>setV(value || ''), [value]);
  const parsed = parseDurationLocal(v);
  const looksValid = !v || parsed > 0;
  return (
    <div>
      <input type="text" value={v} maxLength={20}
             onChange={e=>{ setV(e.target.value); onChange(e.target.value); }}
             placeholder={placeholder || '45分 / 1時間30分 / 2h'}
             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"/> 認識できません。例: <code>45分</code> / <code>1時間30分</code> / <code>2h</code>
        </div>
      )}
    </div>
  );
}

// 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"/> 数値が見つかりません。例: <code>{symbol}420</code> / <code>1,200円</code> / <code>500</code>
        </div>
      )}
    </div>
  );
}

function parseDurationLocal(s){
  if (!s) return 0;
  const txt = String(s).trim().toLowerCase();
  const hm = txt.match(/(\d+(?:\.\d+)?)\s*h(?:ours?|r)?\s*(\d+(?:\.\d+)?)?/);
  if (hm) return parseFloat(hm[1])*60 + (hm[2] ? parseFloat(hm[2]) : 0);
  const h2 = txt.match(/(\d+(?:\.\d+)?)\s*(?:h\b|時間)/);
  if (h2) return parseFloat(h2[1]) * 60;
  const d2 = txt.match(/(\d+(?:\.\d+)?)\s*(?:d\b|day|日)/);
  if (d2) return parseFloat(d2[1]) * 1440;
  const m2 = txt.match(/(\d+(?:\.\d+)?)\s*(?:m(?:in)?\b|分)/);
  if (m2) return parseFloat(m2[1]);
  // Compound 1時間30分
  const cm = txt.match(/(\d+(?:\.\d+)?)\s*(?:時間|h)\s*(\d+(?:\.\d+)?)\s*(?:分|m)/);
  if (cm) return parseFloat(cm[1])*60 + parseFloat(cm[2]);
  const n = parseFloat(txt);
  return isFinite(n) ? n : 0;
}

// ── 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>地点（ノード）編集</h3>
      <div className="card">
        <div className="field">
          <label>名前</label>
          <input type="text" value={node.name||''} maxLength={40}
                 onChange={e=>updateNode(node.id,{name:e.target.value})}
                 placeholder="駅名・観光地名など"/>
        </div>
        <div className="field">
          <label>サブタイトル（名前の下に小さく表示）</label>
          <input type="text" value={node.subtitle||''} maxLength={60}
                 onChange={e=>updateNode(node.id,{subtitle:e.target.value})}
                 placeholder="例: 乗換 / 1泊 / 名物の○○"/>
        </div>
      </div>

      <div className="card warn">
        <h3 style={{marginTop:0}}>到着・出発・諸費用</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 = '⚠ 出発が到着より前になっています';
          }
          if (minArr && arr && dt){
            const m = dt.minutesBetween(minArr, arr);
            if (m != null && m < 0) arrErr = `⚠ 到着が前ノードの出発（${dt.formatTime(minArr)}）より前になっています`;
          }

          // Quick-fill action sets
          const arrQuick = [
            { label: prev?.name ? `「${prev.name}」の出発と同じ` : '前ノードの出発と同じ',
              value: prev?.departure || null,
              title: prev?.departure ? dt?.formatTime(prev.departure) : '' },
            arr ? { label: 'クリア', value: '' } : null,
          ].filter(Boolean);

          const depQuick = arr ? [
            { label: '到着+15分',  value: dt?.addMinutes(arr, 15) },
            { label: '+30分',     value: dt?.addMinutes(arr, 30) },
            { label: '+1時間',    value: dt?.addMinutes(arr, 60) },
            { label: '+2時間',    value: dt?.addMinutes(arr, 120) },
            dep ? { label: 'クリア', value: '' } : null,
          ].filter(Boolean) : (dep ? [{ label:'クリア', value:'' }] : []);

          // Compose hints — surface the constraint reason so the user
          // understands why earlier dates are unavailable.
          const arrHintBoundary = minArr && dt
            ? `前の地点の出発（${dt.formatTime(minArr)}）以降のみ選択可`
            : null;
          const depHintBoundary = arr && dt
            ? `到着（${dt.formatTime(arr)}）以降のみ選択可`
            : (minArr && dt ? `前の地点の出発（${dt.formatTime(minArr)}）以降のみ選択可` : null);

          return (
            <>
              <div className="field">
                <label>
                  <span className="field-icon arrival" aria-hidden="true">
                    <window.TourIcon name="flag" size={11} stroke="currentColor"/>
                  </span>
                  到着日時
                </label>
                <DateTimeField value={arr}
                               onChange={(v)=>updateNode(node.id, {arrival:v})}
                               quickActions={arrQuick}
                               error={arrErr}
                               min={minArr}
                               hint={!arr ? (arrHintBoundary || '出発地点なら空のままでOK') : 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>
                  出発日時
                </label>
                <DateTimeField value={dep}
                               onChange={(v)=>updateNode(node.id, {departure:v})}
                               quickActions={depQuick}
                               error={depErr}
                               min={minDep}
                               hint={!dep ? (depHintBoundary || '到着地点（最終地）なら空のままでOK') : 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"/> この地点での所要時間: <b style={{color:'var(--green-deep)'}}>{dt.formatDuration(m)}</b>
                  </div>
                );
              })()}
            </>
          );
        })()}

        <div className="field">
          {(() => {
            // Auto-detected category from icon (bed/tent → 宿泊費, else → 雑費).
            // node.costCategory is an optional explicit override that supports
            // 4 expense buckets: 宿泊費 / 雑費 / 会議費 / 交際費.
            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:'宿泊費', misc:'雑費', meeting:'会議費', entertainment:'交際費'}[k] || '雑費');
            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>
                    諸費用
                  </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}}>(自動)</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="この費用を宿泊費に分類">
                    <window.TourIcon name="bed" size={12}/> 宿泊費
                  </button>
                  <button className={current==='misc'?'on':''}
                          onClick={()=>setCat('misc')}
                          title="この費用を雑費に分類">
                    <window.TourIcon name="wallet" size={12}/> 雑費
                  </button>
                  <button className={current==='meeting'?'on':''}
                          onClick={()=>setCat('meeting')}
                          title="商談・打ち合わせ等の会議費に分類">
                    <window.TourIcon name="briefcase" size={12}/> 会議費
                  </button>
                  <button className={current==='entertainment'?'on':''}
                          onClick={()=>setCat('entertainment')}
                          title="接待・会食等の交際費に分類">
                    <window.TourIcon name="utensils" size={12}/> 交際費
                  </button>
                </div>
                <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.5, marginTop:4}}>
                  デフォルトは アイコンが <b>ホテル</b> / <b>テント</b> なら「宿泊費」、それ以外は「雑費」。会議費・交際費にしたい場合は手動で切替できます。
                  {isOverride && <span style={{color:'var(--accent-blue)', marginLeft:4}}>(手動指定中)</span>}
                </div>
              </>
            );
          })()}
        </div>

        <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.5, marginTop:-4}}>
          矢印（区間）の<b>移動時間</b>は、前ノードの<b>出発</b>と次ノードの<b>到着</b>から自動算出されます。<br/>
          キャンバスのチップはドラッグで自由に移動できます。
        </div>
      </div>

      <div className="card mint">
        <h3 style={{marginTop:0}}>アイコン</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}/> 画像アップ
            </button>
            {node.image && <button className="btn sm ghost"
                                   onClick={()=>updateNode(node.id,{image:null})}>画像クリア</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'}/>
              ハイライト（重要マーク）
            </label>
            <div className="swatches">
              <button style={{background:'#fff', position:'relative'}}
                      className={!node.highlight?'on':''}
                      title="ハイライトなし"
                      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={`ハイライト ${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="カスタム色を選ぶ"
                     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}}>
              「今回の目玉スポット」「外せない商談相手」など、強調したい地点に色枠を付けられます。工程表にも反映されます。
            </div>
          </div>
          <div className="field" style={{marginTop:0}}>
            <label>背景色</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="カスタム色を選ぶ"
                     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>形</label>
            <div className="seg">
              {[['circle','まる'],['square','四角'],['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>名前の位置</label>
            <div className="seg">
              {[['top','上'], ['left','左'], ['bottom','下'], ['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="このノードを複製（同じアイコン・色・日時で新しい地点を作成）">
            <window.TourIcon name="circle-dot" size={14}/> 複製
          </button>
        )}
        <button className="btn danger" onClick={()=>deleteNode(node.id)} style={{flex:1}}>
          <window.TourIcon name="trash-2" size={14}/> ノードを削除
        </button>
      </div>

      {/* Day 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'}}>
            所属グループ ({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 || '(無名)'}</span>
              <button className="btn sm ghost" title="編集"
                      style={{padding:'2px 6px', fontSize:11}}
                      onClick={()=>setSelection && setSelection({type:'group', id:ng.id})}>✎</button>
              <button className="btn sm ghost" title="このグループから外す"
                      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); }
                  }}>
            ＋ 別のグループにも入れる
          </button>
        </div>
      ) : (
        <button className="btn"
                onClick={()=>{
                  const gid = createGroupFromNode && createGroupFromNode(node.id);
                  if (gid) { setComposingGroupId && setComposingGroupId(gid); }
                }}>
          <window.TourIcon name="calendar" size={14}/> Day グループに入れる
        </button>
      )}
    </>
  );
}

// ── Icon picker (Lucide) ───────────────────────────────────────
function IconPicker({ selected, onSelect, onClear }){
  return (
    <div className="field">
      <label>アイコン（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'}}>— アイコンなし</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 = `反転すると ${to?.name || '行き先'}（${dt.formatTime(newFromTime)}）→ ${from?.name || '出発地点'}（${dt.formatTime(newToTime)}）になり、時間が逆行します`;
    }
  }
  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 ? 'クリックで理由を表示' : '出発と行き先を入替'}
                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}/> 向きを反転
        </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>矢印（区間）編集</h3>
      <div className="card info">
        {/* Endpoint editing — click-to-pick a new from/to node, or swap. */}
        <div className="field" style={{marginBottom:10}}>
          <label>区間の出発・到着</label>
          {/* Grid cells default to min-width=auto, so a long node name was
              expanding the cell and pushing the layout. min-width:0 on the
              button (grid item) lets the inner span ellipsis-truncate.
              display:flex makes flex-direction:column actually take effect
              (without it, the spans fall back to inline-block 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="クリック後、地図上の新しい出発地点をクリック"
                    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="クリック後、地図上の新しい行き先をクリック"
                    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}}>
            ✎ ボタンを押してから、地図上の新しい地点をクリックすると経路が変更されます。予定変更や挿入に便利です。
          </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>交通手段</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>
          <input type="text" value={edge.label ?? rel.label} maxLength={40}
                 onChange={e=>updateEdge(edge.id,{label:e.target.value})}
                 placeholder="例: 山手線・こだま号・徒歩"/>
        </div>

        <div className="field">
          <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:''})}>なし</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}}>旅費・所要時間</h3>

        <div className="field">
          <label>旅費・運賃</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>移動時間（自動算出）</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>「{from?.name||'?'}」の出発と「{to?.name||'?'}」の到着を入力すると自動計算されます</span>
              : transitMins <= 0
                ? <span style={{color:'var(--danger)', display:'inline-flex', alignItems:'center', gap:4}}>
                  <window.TourIcon name="circle-alert" size={12} stroke="currentColor"/> 出発が到着より後ろになっています
                </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}}>
          チップはドラッグで自由に動かせます（離れると点線でリンク）。
        </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})}>
            ↺ チップ位置をリセット
          </button>
        )}
      </div>

      <div className="card">
        <h3 style={{marginTop:0}}>矢印のスタイル</h3>
        <div className="seg">
          {/* twoway (両方向 ⇄) は廃止 — 既存データのために描画は残すが、
              ピッカーには出さない（新規作成は不可）。 */}
          {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字の角の丸み（コーナーRをドラッグ操作でも調整可）</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 で直角、大きくするほどなめらかに丸まります。<br/>
              キャンバスの<b style={{color:'var(--green-deep)'}}>角の○ハンドル</b>を内側にドラッグしても調整できます。
            </div>
          </div>
        )}
        <div className="field" style={{marginTop:10}}>
          <label>線の色</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="カスタム色を選ぶ"
                   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})}>↺ 曲げをリセット</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"/>{' '}
        矢印は<b>地点の時刻から自動生成</b>されるため、矢印自体は削除できません。<br/>
        消したい場合は前後の地点の時刻を変えるか、地点を削除してください。
      </div>

      <div style={{fontSize:11, color:'var(--ink-soft)', textAlign:'center', marginTop:8, lineHeight:1.6}}>
        💡 別の矢印を <b>Shift+クリック</b> でその矢印の設定（交通手段・色・運賃・ラベル等）をこちらにコピー
      </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"/> グループ編集</h3>
      <div className="card">
        <div className="field">
          <label>グループ名</label>
          <input type="text" value={group.name||''} maxLength={40}
                 onChange={e=>updateGroup(group.id, {name:e.target.value})}
                 placeholder="Day 1 / 1日目 / 朝の部 など"/>
        </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})}/>
          名前をキャンバスに表示
        </label>
      </div>

      <div className="card mint">
        <h3 style={{marginTop:0}}>色</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="カスタム色を選ぶ"
                 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.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 || '(無名)'}</span>
              <button className="btn sm ghost" title="グループから外す"
                      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); }}>
          ＋ メンバーを追加・編集
        </button>
      </div>

      <button className="btn danger" onClick={()=>deleteGroup(group.id)}>
        <window.TourIcon name="trash-2" size={14}/> グループを削除
      </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 || '(削除済み)';
  } 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 = '(削除済み)';
  }
  const isPickingThis = notePickMode && notePickMode.noteId === note.id;
  return (
    <>
      <h3><window.TourIcon name="sticky-note" size={14} stroke="currentColor"/> メモ編集</h3>
      <div className="card warn">
        <div className="field">
          <label>本文</label>
          <textarea rows={6} value={note.text||''} maxLength={500}
                    onChange={e=>updateNote(note.id,{text:e.target.value})}
                    placeholder="補足・注意点・予約メモなど&#10;&#10;改行も使えます"/>
        </div>

        {/* Anchor: pick a node or edge by clicking it on the canvas. */}
        <div className="field">
          <label>紐づけ先（点線で接続）</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' ? '地点: ' : '矢印: '}
                <b>{anchorName}</b>
              </span>
              <button className="btn sm ghost" title="紐づけを変更"
                      style={{padding:'2px 8px', fontSize:11}}
                      onClick={()=>startNoteAnchorPick && startNoteAnchorPick(note.id, anchorType)}>
                <window.TourIcon name="pencil" size={11}/> 変更
              </button>
              <button className="btn sm ghost" title="紐づけを解除"
                      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}/> 地点に紐づける
              </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}/> 矢印に紐づける
              </button>
            </div>
          )}

          <div style={{fontSize:11, color:'var(--ink-soft)', lineHeight:1.5, marginTop:6}}>
            ボタンを押して、対象の<b>地点</b>または<b>矢印</b>をクリックすると紐づきます。
          </div>
        </div>
        <div className="field">
          <label>背景色</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="このメモを複製（テキスト・色・紐づけそのままで新しい付箋を作成）">
            <window.TourIcon name="sticky-note" size={14}/> 複製
          </button>
        )}
        <button className="btn danger" onClick={()=>deleteNote(note.id)} style={{flex:1}}>
          <window.TourIcon name="trash-2" size={14}/> このメモを削除
        </button>
      </div>
    </>
  );
}

window.Inspector = Inspector;
