/*__IIFE_WRAPPED__*/
;(function(){
// screens-event.jsx — Secret-Input(本音) / Plan-Pending / Plan-Reveal / Surprise-Hint
const { useState: useS, useEffect: useE, useRef: useR } = React;
const {
  useApp, Icon, Btn, Chip, Card, Avatar, AiAvatar, ImgSlot, Eyebrow, LottieBox,
  TopBar, Body, Field, Segmented, PrivacyBadge, SAFE_TOP, Amulet,
  Weather, WeatherBadge,
  MonthGrid, TimeBand, parseISODate, parseTimeRange, hhmm, WD,
} = window;
const PARTNER_FALLBACK = 'パートナー';

/* ── 音声入力ボタン（Web Speech API）──
   タップして話すと、確定した認識結果を onText(text) で親の入力欄へ渡す。
   ブラウザ/端末が未対応なら何も描画しない（従来どおりキーボード入力）。 */
function MicButton({ onText, lang = 'ja-JP' }) {
  const [supported] = useS(() => !!(window.SpeechRecognition || window.webkitSpeechRecognition));
  const [listening, setListening] = useS(false);
  const recRef = useR(null);
  const onTextRef = useR(onText);
  onTextRef.current = onText;

  useE(() => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) return;
    const rec = new SR();
    rec.lang = lang;
    rec.interimResults = false;
    rec.continuous = false;
    rec.onresult = (e) => {
      let fin = '';
      for (let i = e.resultIndex; i < e.results.length; i++) {
        if (e.results[i].isFinal) fin += e.results[i][0].transcript;
      }
      fin = fin.trim();
      if (fin && onTextRef.current) onTextRef.current(fin);
    };
    rec.onend = () => setListening(false);
    rec.onerror = () => setListening(false);
    recRef.current = rec;
    return () => { try { rec.abort(); } catch (_) {} };
  }, []);

  if (!supported) return null;

  const toggle = () => {
    const rec = recRef.current;
    if (!rec) return;
    if (listening) { try { rec.stop(); } catch (_) {} setListening(false); return; }
    try { rec.start(); setListening(true); } catch (_) { /* 二重start は無視 */ }
  };

  return (
    <button type="button" onClick={toggle} aria-label="音声入力" title="音声入力" style={{
      width: 34, height: 34, borderRadius: '50%', border: 'none', flexShrink: 0,
      background: listening ? 'var(--amethyst)' : 'var(--surface-2)',
      color: listening ? '#fff' : 'var(--ink-soft)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
      transition: 'background .2s',
      boxShadow: listening ? '0 0 0 4px color-mix(in oklch, var(--amethyst) 22%, transparent)' : 'none',
    }}><Icon name="mic" size={18} /></button>
  );
}

/* 音声認識テキストを既存入力へ追記する（末尾に半角スペースを挟む） */
const appendVoice = (setText) => (t) => setText(prev => (prev && prev.trim() ? prev.replace(/\s+$/, '') + ' ' : '') + t);

const FIRST_PROMPT = { who: 'ai', text: 'どんな気分ですか？ 行きたい場所、いまの正直な体力、なんでも聞かせてください。ここはあなただけのひみつの場所です。' };

const delay = ms => new Promise(r => setTimeout(r, ms));
const spotToProposal = s => ({
  name: s.name, kind: s.category || 'スポット', blurb: s.note || '',
  img: (s.category || 'SPOT').slice(0, 6), lat: s.lat, lng: s.lng, category: s.category,
  photo: s.photoUrl, placeId: s.placeId, address: s.address, rating: s.rating, mapsUri: s.mapsUri,
});

/* ─────────── 体力バッテリー（スワイプで充電） ─────────── */
function batteryLabel(v) { return v < 25 ? 'クタクタ' : v < 55 ? 'ほどほど' : v < 80 ? '元気' : '元気満タン'; }
function batteryColor(v) { return v < 25 ? 'var(--me)' : v < 55 ? 'var(--gold)' : 'var(--partner)'; }

function BatterySwipe({ value, onChange }) {
  const ref = useR(null);
  const drag = useR(false);
  useE(() => {
    const setFrom = cx => {
      const r = ref.current.getBoundingClientRect();
      const p = Math.round(((cx - r.left) / r.width) * 100);
      onChange(Math.max(0, Math.min(100, p)));
    };
    const move = e => { if (drag.current) { e.preventDefault(); setFrom(e.clientX); } };
    const up = () => { drag.current = false; };
    window.addEventListener('pointermove', move, { passive: false });
    window.addEventListener('pointerup', up);
    ref.current.__setFrom = setFrom;
    return () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); };
  }, [onChange]);
  const col = batteryColor(value);
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 11 }}>
        <Icon name="battery" size={18} style={{ color: col }} />
        <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--ink)' }}>今日の体力</span>
        <span style={{ marginLeft: 'auto', fontFamily: 'var(--font-serif)', fontSize: 17, fontWeight: 600, color: col }}>{batteryLabel(value)}</span>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
        <div ref={ref}
          onPointerDown={e => { drag.current = true; ref.current.__setFrom(e.clientX); }}
          style={{
            position: 'relative', flex: 1, height: 56, borderRadius: 14, cursor: 'grab', touchAction: 'none',
            background: 'var(--surface-2)', border: '3px solid var(--ink)', padding: 4, overflow: 'hidden',
          }}>
          <div style={{ position: 'absolute', top: 4, bottom: 4, left: 4, width: `calc(${value}% - 8px)`, minWidth: 2,
            borderRadius: 8, background: `linear-gradient(90deg, color-mix(in oklch, ${col} 78%, #fff), ${col})`, transition: drag.current ? 'none' : 'width .12s ease' }} />
          <div style={{ position: 'absolute', inset: 4, display: 'flex', gap: 4, pointerEvents: 'none' }}>
            {[0, 1, 2, 3, 4].map(i => <div key={i} style={{ flex: 1, borderRight: i < 4 ? '1.5px solid color-mix(in srgb, var(--paper) 55%, transparent)' : 'none' }} />)}
          </div>
          <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, pointerEvents: 'none' }}>
            <span style={{ fontFamily: 'var(--font-serif)', fontSize: 22, fontWeight: 700, color: value > 48 ? '#fff' : 'var(--ink)', textShadow: value > 48 ? '0 1px 3px rgba(0,0,0,.25)' : 'none' }}>{value}%</span>
          </div>
        </div>
        <div style={{ width: 7, height: 22, borderRadius: 3, background: 'var(--ink)' }} />
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8, fontSize: 11, color: 'var(--ink-faint)' }}>
        <span>クタクタ</span><span>スワイプして充電 →</span><span>元気満タン</span>
      </div>
    </div>
  );
}

/* ─────────── チャット共通: 吹き出し・タイピング・提案 ─────────── */
function Bubble({ m }) {
  const me = m.who === 'me';
  return (
    <div style={{ alignSelf: me ? 'flex-end' : 'flex-start', maxWidth: '84%', display: 'flex', gap: 8, flexDirection: me ? 'row-reverse' : 'row' }}>
      {!me && <AiAvatar style={{ marginTop: 2 }} />}
      <div style={{
        background: me ? 'var(--me)' : 'var(--surface)',
        color: me ? '#fff' : 'var(--ink)',
        border: me ? 'none' : '1px solid var(--line)',
        borderRadius: me ? '18px 18px 6px 18px' : '18px 18px 18px 6px',
        padding: '11px 15px', fontSize: 14.5, lineHeight: 1.65, fontWeight: me ? 500 : 400,
        whiteSpace: 'pre-wrap',
      }}>{m.text}</div>
    </div>
  );
}

/* 配列をシャッフルした新しい配列を返す（毎回おなじ順で出ると「作り物」に見えるため） */
const shuffled = (arr) => {
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; }
  return a;
};

/* 応答待ちの“間”を埋める、意味をなさないひとりごと。
   ドットだけだと止まって見えるので、言葉が動き続けることで「生きている」ことを伝える。 */
const WHIMSY_LINES = [
  'こねこね中', 'ひらめき待ち', 'ふむふむ', 'ことばを探し中', 'ぐるぐる思案中',
  'そわそわ', 'まぜまぜ', 'ひとりごと中', 'メモ取り中', 'たくらみ中',
  'うんうん唸り中', 'にやにや中', 'そっと考え中', 'ぽかぽか思考中', 'とんとん拍子中',
  'えんやこら', 'ひらり思いつき中', 'もぐもぐ咀嚼中', 'ぱたぱた探し中', 'しんみり中',
];

function TypingBubble({ lines = WHIMSY_LINES }) {
  const seq = useR(null);
  if (!seq.current) seq.current = shuffled(lines);
  const [i, setI] = useS(0);
  const [sec, setSec] = useS(0);
  useE(() => {
    const t = setInterval(() => setI(v => (v + 1) % seq.current.length), 1900);
    const s = setInterval(() => setSec(v => v + 1), 1000);
    return () => { clearInterval(t); clearInterval(s); };
  }, []);
  return (
    <div style={{ alignSelf: 'flex-start', display: 'flex', gap: 8 }}>
      <AiAvatar style={{ marginTop: 2 }} />
      <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: '18px 18px 18px 6px', padding: '12px 15px', display: 'flex', gap: 9, alignItems: 'center' }}>
        <div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
          {[0, 1, 2].map(d => (
            <span key={d} style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--ink-faint)',
              animation: 'glowpulse 1.2s ease-in-out infinite', animationDelay: `${d * 0.18}s` }} />
          ))}
        </div>
        <span key={i} style={{ fontSize: 12.5, color: 'var(--ink-soft)', whiteSpace: 'nowrap', animation: 'risefade .45s ease' }}>{seq.current[i]}…</span>
        {sec >= 5 && (
          <span style={{ fontSize: 11, color: 'var(--ink-faint)', fontVariantNumeric: 'tabular-nums' }}>{sec}s</span>
        )}
      </div>
    </div>
  );
}

function ProposalBlock({ spots, onPick }) {
  const [picked, setPicked] = useS(null);
  const list = (spots && spots.length ? spots : []);
  return (
    <div style={{ alignSelf: 'flex-start', maxWidth: '94%', display: 'flex', gap: 8 }}>
      <AiAvatar style={{ marginTop: 2 }} />
      <div style={{ flex: 1 }}>
        <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: '18px 18px 18px 6px', padding: '11px 15px', fontSize: 14.5, lineHeight: 1.6, color: 'var(--ink)' }}>
          このあたり、<b>どこが気になる？</b><br />
          <span style={{ fontSize: 13, color: 'var(--ink-soft)' }}>気になる場所を選ぶと、Amulet Map に追加されます。</span>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 9, marginTop: 10 }}>
          {list.map(p => {
            const on = picked === p.name;
            return (
              <button key={p.name} disabled={picked} onClick={() => { setPicked(p.name); onPick(p); }} style={{
                display: 'flex', gap: 12, alignItems: 'center', textAlign: 'left', cursor: picked ? 'default' : 'pointer',
                background: on ? 'color-mix(in oklch, var(--amethyst) 12%, var(--paper))' : 'var(--surface)',
                border: on ? '1.5px solid var(--amethyst)' : '1px solid var(--line)',
                opacity: picked && !on ? 0.5 : 1, borderRadius: 16, padding: 9, transition: 'all .18s',
              }}>
                <ImgSlot label={p.img} src={p.photo} h={58} style={{ width: 58, flexShrink: 0 }} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 11, color: 'var(--amethyst)', fontWeight: 700 }}>{p.kind}</div>
                  <div style={{ fontWeight: 700, fontSize: 14.5, color: 'var(--ink)', margin: '1px 0 2px' }}>{p.name}</div>
                  <div style={{ fontSize: 11.5, color: 'var(--ink-soft)', lineHeight: 1.5 }}>{p.blurb}</div>
                </div>
                <div style={{ width: 24, height: 24, borderRadius: '50%', flexShrink: 0, border: on ? 'none' : '1.5px solid var(--line-2)', background: on ? 'var(--amethyst)' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  {on && <Icon name="check" size={14} stroke="#fff" sw={2.6} />}
                </div>
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

/* ─────────── プラン作り直しの承認カード（③ プラン相談） ───────────
   会話が溜まってAIが作り直し案を用意したら出る。押して初めてプランが作り直される
   （勝手な再生成を避けるための承認ゲート）。 */
function PlanProposalCard({ onApply }) {
  const { toast } = useApp();
  const [busy, setBusy] = useS(false);
  const [done, setDone] = useS(false);
  const apply = async () => {
    if (busy || done) return;
    setBusy(true);
    try {
      await onApply();
      setDone(true);
    } catch (e) {
      console.error(e);
      toast(Amulet.friendlyError(e));
    } finally { setBusy(false); }
  };
  return (
    <div style={{ alignSelf: 'flex-start', maxWidth: '94%', display: 'flex', gap: 8 }}>
      <AiAvatar style={{ marginTop: 2 }} />
      <div style={{ flex: 1 }}>
        <div style={{ background: 'color-mix(in oklch, var(--amethyst) 8%, var(--paper))', border: '1.5px solid color-mix(in oklch, var(--amethyst) 30%, transparent)', borderRadius: '18px 18px 18px 6px', padding: 13 }}>
          <div style={{ fontSize: 14.5, lineHeight: 1.6, color: 'var(--ink)' }}>
            {done ? <>プランを<b>作り直しました</b>。<br /><span style={{ fontSize: 13, color: 'var(--ink-soft)' }}>最新のプランに差し替わっています。</span></>
              : <>ここまでの相談で、<b>作り直しの案</b>がまとまったよ。<br /><span style={{ fontSize: 13, color: 'var(--ink-soft)' }}>反映してよければ、下のボタンで作り直せます。</span></>}
          </div>
          {!done && (
            <button onClick={apply} disabled={busy} style={{ marginTop: 11, width: '100%', height: 44, borderRadius: 13, border: 'none', cursor: busy ? 'default' : 'pointer', background: 'var(--amethyst)', color: '#fff', fontFamily: 'var(--font-ui)', fontSize: 14.5, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
              {busy ? '作り直しています…' : 'このプランで作り直す'}{!busy && <Icon name="arrow" size={18} stroke="#fff" />}
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

/* ─────────── デート作成サジェスト（秘密インプット専用の唯一のサジェスト） ───────────
   AIが「そろそろデートにする？」と判断したら出すカード。タップすると
   デート作成画面が、AIの提案したタイトル・エリア・日付で“埋まった状態”で開く。 */
function EventSuggestCard({ data }) {
  const { go } = useApp();
  const d = data || {};
  return (
    <div style={{ alignSelf: 'flex-start', maxWidth: '94%', display: 'flex', gap: 8 }}>
      <AiAvatar style={{ marginTop: 2 }} />
      <div style={{ flex: 1 }}>
        <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: '18px 18px 18px 6px', padding: '11px 15px', fontSize: 14.5, lineHeight: 1.6, color: 'var(--ink)' }}>
          気持ちが見えてきたよ。<b>そろそろデートにする？</b><br />
          <span style={{ fontSize: 13, color: 'var(--ink-soft)' }}>この内容で作成画面をひらけます。あとから直せます。</span>
        </div>
        <button onClick={() => go('event-create', { initTitle: d.title, initArea: d.area, initISO: d.dateISO, initTimeRange: d.timeRange })} style={{
          display: 'flex', gap: 12, alignItems: 'center', width: '100%', textAlign: 'left', cursor: 'pointer', marginTop: 10,
          background: 'color-mix(in oklch, var(--amethyst) 8%, var(--paper))', border: '1.5px solid color-mix(in oklch, var(--amethyst) 30%, transparent)',
          borderRadius: 16, padding: 13,
        }}>
          <div style={{ width: 44, height: 44, borderRadius: 13, flexShrink: 0, background: 'color-mix(in oklch, var(--amethyst) 16%, var(--paper))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <Icon name="cal" size={21} style={{ color: 'var(--amethyst)' }} />
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 11, color: 'var(--amethyst)', fontWeight: 700 }}>デートを作成</div>
            <div style={{ fontWeight: 700, fontSize: 14.5, color: 'var(--ink)', margin: '1px 0 2px' }}>{d.title || '新しいデート'}</div>
            <div style={{ fontSize: 11.5, color: 'var(--ink-soft)', lineHeight: 1.5 }}>{[d.dateLabel, d.timeRange && `${d.timeRange[0]}–${d.timeRange[1]}`, d.area].filter(Boolean).join(' · ') || 'タップして詳細を決める'}</div>
          </div>
          <Icon name="chevR" size={18} style={{ color: 'var(--amethyst)' }} />
        </button>
      </div>
    </div>
  );
}

/* ─────────── チャットの共通ロジック（実データ前提・実API） ─────────── */
function useAmuletChat(ev, live, toast, initialMsgs, mode = 'counsel') {
  const isPlan = mode === 'plan';
  const isLive = !!(ev && ev.live && live.coupleId);
  // 実データ前提。サインイン前（≒PC自動ログイン完了前）は最初の問いかけだけ出す。
  const [msgs, setMsgs] = useS(isLive ? [] : [FIRST_PROMPT]);
  const [loading, setLoading] = useS(isLive);
  const [typing, setTyping] = useS(false);

  useE(() => {
    if (!isLive) return;
    let dead = false;
    // ① counsel は inputs スレッド、③ plan は planChat スレッドから履歴を読む
    const loader = isPlan
      ? Amulet.getMyPlanChat(live.coupleId, ev.id)
      : Amulet.getMyInput(live.coupleId, ev.id);
    loader.then(doc => {
      if (dead) return;
      const hist = (doc?.messages || []).map(m => ({ who: m.role === 'user' ? 'me' : 'ai', text: m.text }));
      // live で履歴が空なら initialMsgs（intro）→ なければ FIRST_PROMPT
      const base = hist.length ? hist : (initialMsgs?.length ? initialMsgs : [FIRST_PROMPT]);
      // 前回の相談で承認待ちの案が残っていれば、末尾に承認カードを復元する
      if (isPlan && doc?.pendingPlan?.body) base.push({ kind: 'plan_proposal' });
      setMsgs(base);
    }).catch(e => {
      console.error(e);
      setMsgs([FIRST_PROMPT]);
    }).finally(() => { if (!dead) setLoading(false); });
    return () => { dead = true; };
  }, []);

  const send = async (text) => {
    if (!text.trim() || typing) return;
    setMsgs(m => [...m, { who: 'me', text }]);
    if (!isLive) return; // 実APIが無い状態では送信しない（サインイン後に有効）
    setTyping(true);
    try {
      if (isPlan) {
        // ③ プラン相談: まずは会話だけ。案が溜まったら承認カードを出し、
        //   本人が承認したときだけ applyPlanProposal でプランを作り直す（勝手に再生成しない）。
        const r = await Amulet.planChat({ eventId: ev.id, text });
        setMsgs(m => {
          const next = [...m, { who: 'ai', text: r.reply }];
          if (r.proposal) next.push({ kind: 'plan_proposal' });
          return next;
        });
      } else {
        // ① 本音・相談役
        const r = await Amulet.sendChat({ eventId: ev.id, text });
        setMsgs(m => {
          const next = [...m, { who: 'ai', text: r.reply }];
          const evSug = (r.actions || []).find(a => a.type === 'suggest_event');
          if (evSug) next.push({ kind: 'event_suggest', data: evSug });
          return next;
        });
      }
    } catch (e) {
      console.error(e);
      toast(Amulet.friendlyError(e));
    } finally { setTyping(false); }
  };

  // 承認カードの「このプランで作り直す」→ 溜まった案を実際に反映する
  const apply = async () => {
    if (!isLive) throw new Error('offline');
    await Amulet.applyPlanProposal(ev.id);
    // listenLatestPlan が新バージョンを拾って画面を差し替える
    toast(<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}><Icon name="refresh" size={15} />プランを作り直しました</span>);
  };

  const pick = async (p) => {
    setMsgs(m => [...m, { who: 'me', text: `「${p.name}」が気になるな。` }]);
    if (isLive && p.lat != null) {
      Amulet.addPin(live.coupleId, {
        name: p.name, category: p.category || p.kind || 'スポット',
        lat: p.lat, lng: p.lng, note: p.blurb || '',
        photoUrl: p.photo, placeId: p.placeId, address: p.address, rating: p.rating, mapsUri: p.mapsUri,
      }).catch(e => console.error('addPin:', e));
    }
    setTimeout(() => setMsgs(m => [...m, { who: 'ai', text: `いいですね。「${p.name}」、Amulet Map に追加してプランにも織り込んでおきますね。` }]), 500);
  };

  return { isLive, msgs, loading, typing, send, pick, apply };
}

/* ════════════════ HOME CHAT (① EventCreatorAgent · eventId なし) ════════════════
   ホームの「なんでも、ふたりのことを相談」から開く “なんでも屋” チャット。
   イベント不要。デートの相談・アプリへの不満・ふたりの関係の悩み…なんでも話せる。
   会話の中でデート作成の意図が出たら create_event(createDate) で「器」づくりに入り、
   タイトル（大まかなテーマ）が分かったらデート作成画面（EventCreateScreen）へ引き継ぐ。
   日付/時間帯/エリアの詳細確定はそちらの担当（ここでは作らない）。
   ※ 行き先・スポット等の詳細プランは「秘密インプット」以降の担当（ここでは作らない）。
   履歴は永続化しないので、毎回フロントから history を渡してマルチターンを成立させる。 */
const HOME_FIRST_PROMPT = { who: 'ai', text: 'こんにちは。悩み、デートの相談…なんでも聞かせてください。ちょっとした不満でも大丈夫です。' };

const WD_HOME = ['日', '月', '火', '水', '木', '金', '土'];
const isoToday = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; };
function parseISOLocal(iso) { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso || ''); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; }
function dowFromISO(iso) { const d = parseISOLocal(iso); return d ? WD_HOME[d.getDay()] : ''; }
function labelFromISO(iso) { const d = parseISOLocal(iso); return d ? `${d.getMonth() + 1}月${d.getDate()}日` : ''; }

/* デート作成ドラフト: 会話で集まった「器」を表示。揃ったら（complete）作成ボタンを出す。
   ここで作るのはデートの“器”だけ。行き先や時間割は次の「秘密インプット」で。 */
function EventDraftCard({ data }) {
  const { go } = useApp();
  const d = (data && data.draft) || {};
  const dateLabel = d.dateLabel || labelFromISO(d.dateISO);
  const dow = dowFromISO(d.dateISO);
  const rows = [
    ['cal',   '日付',     d.dateISO ? `${dateLabel}${dow ? `（${dow}）` : ''}` : '—'],
    ['clock', '時間帯',   (d.timeRange && d.timeRange.length === 2) ? `${d.timeRange[0]}–${d.timeRange[1]}` : '—'],
    ['pin',   'エリア',   d.area || '—'],
    ['edit',  'タイトル', d.title || '（なし）'],
  ];
  // ここでは作成せず、デート作成画面（EventCreateScreen）へ事前入力で渡す。
  // 日付・時間・エリアの詳細確定はそちらの担当。
  const goCreate = () => {
    go('event-create', {
      initTitle: d.title, initArea: d.area, initISO: d.dateISO, initTimeRange: d.timeRange, initSummary: d.summary,
    });
  };
  return (
    <div style={{ alignSelf: 'flex-start', maxWidth: '94%', display: 'flex', gap: 8 }}>
      <AiAvatar style={{ marginTop: 2 }} />
      <div style={{ flex: 1 }}>
        <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: '18px 18px 18px 6px', padding: '11px 15px', fontSize: 14.5, lineHeight: 1.6, color: 'var(--ink)' }}>
          この内容で<b>デートをつくる？</b><br /><span style={{ fontSize: 13, color: 'var(--ink-soft)' }}>次の画面で日付・時間・エリアを決められます。</span>
        </div>
        <div style={{ marginTop: 10, background: 'color-mix(in oklch, var(--amethyst) 8%, var(--paper))', border: '1.5px solid color-mix(in oklch, var(--amethyst) 30%, transparent)', borderRadius: 16, padding: 13 }}>
          {rows.map(([ic, k, v]) => (
            <div key={k} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '5px 0' }}>
              <Icon name={ic} size={16} style={{ color: 'var(--amethyst)', flexShrink: 0 }} />
              <span style={{ fontSize: 12, color: 'var(--ink-faint)', width: 56, flexShrink: 0 }}>{k}</span>
              <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}>{v}</span>
            </div>
          ))}
          <button onClick={goCreate} style={{ marginTop: 10, width: '100%', height: 44, borderRadius: 13, border: 'none', cursor: 'pointer', background: 'var(--amethyst)', color: '#fff', fontFamily: 'var(--font-ui)', fontSize: 14.5, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
            このデートをつくる<Icon name="arrow" size={18} stroke="#fff" />
          </button>
        </div>
      </div>
    </div>
  );
}

// これまでのターンで確定した「デートの器」に、今ターンの draft をマージする。
// 新しい非空フィールドが上書きし、空/未定義は既存を保持する（サーバ側 mergeDraft と同じ意味論）。
// これで、モデルが確定ターンで一部フィールドを出し忘れても器が消えず、確認カードが安定して出る。
function mergeHomeDraft(prev, next) {
  const p = prev || {}, n = next || {};
  const has = v => v !== undefined && v !== null
    && !(typeof v === 'string' && !v.trim())
    && !(Array.isArray(v) && v.length === 0);
  const pick = k => (has(n[k]) ? n[k] : p[k]);
  const merged = { title: pick('title'), dateISO: pick('dateISO'), dateLabel: pick('dateLabel'), area: pick('area'), timeRange: pick('timeRange'), summary: pick('summary') };
  // 日付が今ターンで変わったら、古い dateLabel は引き継がない（表示ズレ防止。無ければ ISO から補完される）
  if (has(n.dateISO) && n.dateISO !== p.dateISO) merged.dateLabel = has(n.dateLabel) ? n.dateLabel : undefined;
  const out = {};
  for (const k of Object.keys(merged)) if (merged[k] !== undefined) out[k] = merged[k];
  return out;
}
// タイトル（大まかなテーマ）が分かれば器として確認カードを出す。日付/エリア/時間帯は「プラン入力画面」で決める。
const homeDraftComplete = d => !!(d && d.title);

function HomeChatScreen({ params }) {
  const { go, back, live, toast } = useApp();
  const signedIn = !!live.user;          // ① は coupleId 不要・認証だけでよい
  const [msgs, setMsgs] = useS([HOME_FIRST_PROMPT]);
  const [typing, setTyping] = useS(false);
  const [text, setText] = useS('');
  const scroller = useR(null);
  const seeded = useR(false);
  const msgsRef = useR(msgs);            // 履歴を渡すため最新 msgs を参照
  const draftRef = useR({});             // これまで確定した「デートの器」を蓄積（サーバへ往復で渡す）
  useE(() => { msgsRef.current = msgs; }, [msgs]);

  useE(() => { const el = scroller.current; if (el) el.scrollTop = el.scrollHeight; }, [msgs, typing]);

  /* ── ホームからの shared-element 遷移 ──
     ホームの「なんでも、相談」ピル（params.hero の矩形）が、そのまま画面下の入力バーへ
     滑り降りるモーフ。キーボードの出現で入力バーの最終位置は毎フレーム動くため、
     目標矩形はアニメーション中に測り直して追従する。 */
  const entering = !!(params && params.hero);
  const [morphing, setMorphing] = useS(entering);
  const rootRef = useR(null);
  const pillRef = useR(null);       // 実物: 入力ピル（textarea を包む丸枠）
  const sendBtnRef = useR(null);    // 実物: 送信ボタン
  const clonePillRef = useR(null);  // モーフ用クローン
  const cloneSendRef = useR(null);
  const taRef = useR(null);

  /* キーボードを開いたまま遷移する: ホーム側でジェスチャ内にゴースト input へフォーカス済み。
     マウント直後（同一タスク内）に textarea へフォーカスを引き継ぐとキーボードは閉じない。 */
  React.useLayoutEffect(() => {
    if (params && params.focus && taRef.current) taRef.current.focus({ preventScroll: true });
  }, []);

  React.useLayoutEffect(() => {
    if (!morphing) return;
    const root = rootRef.current, pill = pillRef.current, send = sendBtnRef.current;
    const cp = clonePillRef.current, cs = cloneSendRef.current;
    if (!root || !pill || !send || !cp || !cs) { setMorphing(false); return; }
    // viewport座標 → root ローカル座標（PCのデバイスフレームは transform:scale されているので割り戻す）
    const loc = (r, rootRect, scale) => ({
      x: (r.left - rootRect.left) / scale, y: (r.top - rootRect.top) / scale,
      w: r.width / scale, h: r.height / scale,
    });
    const rr0 = root.getBoundingClientRect();
    const sc0 = rr0.width / root.offsetWidth || 1;
    const hr = params.hero;
    const p0 = loc({ left: hr.left, top: hr.top, width: hr.width, height: hr.height }, rr0, sc0);
    // 出発点の送信サークル（元ピルの右端内側にあるものを近似）
    const cSize = Math.max(26, Math.min(44, p0.h - 26));
    const s0 = { x: p0.x + p0.w - cSize - (p0.h - cSize) / 2, y: p0.y + (p0.h - cSize) / 2, w: cSize, h: cSize };
    const DUR = 420;
    const ease = t => 1 - Math.pow(1 - t, 3);
    const lerp = (a, b, k) => a + (b - a) * k;
    let raf, t0;
    const step = (now) => {
      if (t0 === undefined) t0 = now;
      const k = ease(Math.min(1, (now - t0) / DUR));
      const rr = root.getBoundingClientRect();
      const sc = rr.width / root.offsetWidth || 1;
      const p1 = loc(pill.getBoundingClientRect(), rr, sc);
      const s1 = loc(send.getBoundingClientRect(), rr, sc);
      const set = (el, a, b, radius) => {
        el.style.left = lerp(a.x, b.x, k) + 'px';
        el.style.top = lerp(a.y, b.y, k) + 'px';
        el.style.width = lerp(a.w, b.w, k) + 'px';
        el.style.height = lerp(a.h, b.h, k) + 'px';
        el.style.borderRadius = radius;
        el.style.opacity = 1;
      };
      set(cp, p0, p1, lerp(18, 22, k) + 'px');
      set(cs, s0, s1, '50%');
      if ((now - t0) < DUR) raf = requestAnimationFrame(step);
      else setMorphing(false);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [morphing]);

  /* キーボードの出入りで表示領域が変わったら、会話は最下部に貼りつけておく */
  useE(() => {
    const vv = window.visualViewport;
    if (!vv) return;
    const pin = () => { const el = scroller.current; if (el) el.scrollTop = el.scrollHeight; };
    vv.addEventListener('resize', pin);
    return () => vv.removeEventListener('resize', pin);
  }, []);

  const sendText = async (raw) => {
    const v = (raw || '').trim();
    if (!v || typing) return;
    setMsgs(m => [...m, { who: 'me', text: v }]);
    if (!signedIn) return;               // サインイン前は送らない（最初の問いかけだけ）
    setTyping(true);
    try {
      // これまでの会話を履歴として渡す（ホームチャットは永続化しないため）
      const history = msgsRef.current
        .filter(m => m.text && (m.who === 'me' || m.who === 'ai'))
        .map(m => ({ role: m.who === 'me' ? 'user' : 'model', text: m.text }));
      history.push({ role: 'user', text: v });
      // eventId を渡さない → ① EventCreatorAgent（なんでも屋）
      // これまで蓄積した器（draftRef）を同送し、サーバ側でもマージして complete を判定させる。
      const r = await Amulet.sendChat({ text: v, history, today: isoToday(), draft: draftRef.current });
      const acts = r.actions || [];
      const ce = acts.find(a => a.type === 'create_event');
      // 今ターンの draft を蓄積器へマージ。表示判定はマージ結果で行う（サーバの complete 単独に依存しない）。
      let mergedDraft = null;
      if (ce) { draftRef.current = mergeHomeDraft(draftRef.current, ce.draft || {}); mergedDraft = draftRef.current; }
      setMsgs(m => {
        const next = [...m, { who: 'ai', text: r.reply }];
        // 収集中はカードを出さず会話だけ。日付・時間帯・エリアが揃った確定段階だけ確認カードを出す。
        if (homeDraftComplete(mergedDraft)) {
          const lastCard = [...m].reverse().find(x => x.kind === 'event_draft');
          const dup = lastCard && JSON.stringify(lastCard.data?.draft) === JSON.stringify(mergedDraft);
          if (!dup) next.push({ kind: 'event_draft', data: { draft: mergedDraft } });
        } else if (!ce) {
          const evSug = acts.find(a => a.type === 'suggest_event'); // 旧エージェント互換
          if (evSug) next.push({ kind: 'event_suggest', data: evSug });
        }
        return next;
      });
    } catch (e) {
      console.error(e);
      toast(Amulet.friendlyError(e));
    } finally { setTyping(false); }
  };

  const send = () => { const v = text; if (!v.trim()) return; setText(''); sendText(v); };

  // ホームのサジェストチップから来たら、その文言で自動送信（1回だけ）
  useE(() => {
    if (seeded.current || !params?.seed) return;
    seeded.current = true;
    sendText(params.seed);
  }, []);

  return (
    <div ref={rootRef} style={{ height: '100%', background: 'var(--bg)', display: 'flex', flexDirection: 'column', position: 'relative' }}>
      {/* header — hero遷移のときは上からふわっと降りてくる */}
      <div style={{ paddingTop: SAFE_TOP, paddingBottom: 12, paddingLeft: 12, paddingRight: 14,
        background: 'color-mix(in srgb, var(--bg) 85%, transparent)', backdropFilter: 'blur(14px)', WebkitBackdropFilter: 'blur(14px)', zIndex: 20, borderBottom: '1px solid var(--line)',
        animation: entering ? 'dropin .32s ease both' : undefined }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <button onClick={back} style={{ width: 36, height: 36, borderRadius: 999, border: '1px solid var(--line-2)', background: 'var(--surface)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: 'var(--ink)', flexShrink: 0 }}><Icon name="back" size={19} /></button>
          <AiAvatar size={30} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontWeight: 700, fontSize: 15, color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>ふたりのことを相談</div>
            <div style={{ fontSize: 11, color: 'var(--ink-faint)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>悩み、デートのこと、なんでも</div>
          </div>
          <PrivacyBadge>相手には見えません</PrivacyBadge>
        </div>
      </div>

      {/* chat — hero遷移のときは下からふわっと */}
      <div ref={scroller} className="noscroll" style={{ flex: 1, overflowY: 'auto', padding: '16px 16px 12px', display: 'flex', flexDirection: 'column', gap: 14,
        animation: entering ? 'chatin .38s .06s ease both' : undefined }}>
        {msgs.map((m, i) =>
          m.kind === 'event_draft' ? <EventDraftCard key={i} data={m.data} />
          : m.kind === 'event_suggest' ? <EventSuggestCard key={i} data={m.data} />
          : <Bubble key={i} m={m} />)}
        {typing && <TypingBubble />}
        <div style={{ height: 4 }} />
      </div>

      {/* input — キーボードが出たら下パディングを詰める（--kbh は app.jsx が配布）。
           モーフ中は実物を透明にしてクローンに演じさせる */}
      <div style={{ padding: '10px 12px', paddingBottom: 'max(12px, calc(26px - var(--kbh, 0px)))',
        background: 'var(--paper)', borderTop: '1px solid var(--line)',
        opacity: morphing ? 0 : 1, transition: 'opacity .1s' }}>
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 8 }}>
          <div ref={pillRef} style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 8, background: 'var(--surface)', border: '1px solid var(--line-2)', borderRadius: 22, padding: '4px 6px 4px 16px' }}>
            <textarea ref={taRef} value={text} onChange={e => setText(e.target.value)}
              onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); send(); } }}
              placeholder="悩み、デートのこと…なんでも" style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', minHeight: 38, maxHeight: 120, paddingTop: 9, paddingBottom: 0, fontFamily: 'var(--font-ui)', fontSize: 15, color: 'var(--ink)', resize: 'none' }} />
            <MicButton onText={appendVoice(setText)} />
          </div>
          <button ref={sendBtnRef} onClick={send} style={{ width: 46, height: 46, borderRadius: '50%', border: 'none', background: 'var(--amethyst)', opacity: text.trim() && !typing ? 1 : .45, color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'opacity .2s', flexShrink: 0 }}><Icon name="send" size={20} /></button>
        </div>
      </div>

      {/* shared-element モーフ用クローン（ホームのピル → 入力バー）。初期 opacity 0 で
           最初のフレームの位置決めまで見せない。座標は effect が毎フレーム流し込む */}
      {morphing && (
        <div style={{ position: 'absolute', inset: 0, zIndex: 80, pointerEvents: 'none' }}>
          <div ref={clonePillRef} style={{ position: 'absolute', opacity: 0, background: 'var(--surface)', border: '1.5px solid var(--line-2)', borderRadius: 18, boxShadow: '0 10px 26px -20px rgba(61,54,44,.5)', display: 'flex', alignItems: 'center', paddingLeft: 16, overflow: 'hidden' }}>
            <span style={{ fontFamily: 'var(--font-ui)', fontSize: 14.5, color: 'var(--ink-faint)', whiteSpace: 'nowrap' }}>悩み、デートのこと…なんでも</span>
          </div>
          <div ref={cloneSendRef} style={{ position: 'absolute', opacity: 0, borderRadius: '50%', background: 'var(--amethyst)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <Icon name="send" size={18} stroke="#fff" />
          </div>
        </div>
      )}
    </div>
  );
}

function Sheet({ children, title, onClose }) {
  return (
    <div onClick={onClose} style={{ position: 'absolute', inset: 0, zIndex: 60, background: 'rgba(40,34,28,.32)', backdropFilter: 'blur(2px)', display: 'flex', alignItems: 'flex-end', animation: 'risefade .25s' }}>
      <div onClick={e => e.stopPropagation()} style={{ width: '100%', background: 'var(--paper)', borderRadius: '26px 26px 0 0', padding: '12px 22px 40px', boxShadow: '0 -10px 40px rgba(0,0,0,.2)', animation: 'floatup .3s ease' }}>
        <div style={{ width: 40, height: 4, borderRadius: 9, background: 'var(--line-2)', margin: '0 auto 16px' }} />
        {title && <div style={{ fontFamily: 'var(--font-serif)', fontSize: 20, color: 'var(--ink)', marginBottom: 18 }}>{title}</div>}
        {children}
      </div>
    </div>
  );
}

/* ════════════════ PLAN REVEAL (決定版プラン) ════════════════ */
function PlanRevealScreen({ params }) {
  const { go, back, live, toast } = useApp();
  const ev = params?.ev || {};
  const isLive = !!(ev.live && live.coupleId);
  const [planDoc, setPlanDoc] = useS(null);
  const [evDoc, setEvDoc] = useS(null);        // 基本情報（タイトル/日付）の編集を反映するため購読
  const [weather, setWeather] = useS('sun');
  const userSetWx = useR(false); // ユーザーが手動でタブを切り替えたか
  const [chatOpen, setChatOpen] = useS(false); // プランを相談 → その場で作り変えるチャット
  const [editOpen, setEditOpen] = useS(false); // 右上の編集（タイトル・日付のみ）

  useE(() => {
    if (!isLive) return;
    // 新: バージョン管理プランの最新版を購読（planChat の作り変えで自動差し替え）
    return Amulet.listenLatestPlan(live.coupleId, ev.id, setPlanDoc);
  }, []);

  useE(() => {
    if (!isLive) return;
    return Amulet.listenEvent(live.coupleId, ev.id, setEvDoc);
  }, []);

  /* ── 基本情報（タイトル・日付）。live購読(evDoc)を最優先、無ければ params にフォールバック ── */
  const raw = evDoc || ev.raw || {};
  const curTitle = raw.title || ev.title || 'ただの週末';
  const curDateLabel = raw.dateLabel || ev.date || '';
  const curDateISO = (raw.date && /^\d{4}-\d{1,2}-\d{1,2}/.test(raw.date)) ? raw.date : (ev.raw?.date || null);
  const curDow = raw.dow || ev.dow || '土';
  const curArea = raw.area || ev.area || '';

  const saveBasics = async (patch) => {
    if (!isLive) { toast('プレビューでは保存されません'); return; }
    await Amulet.updateEvent(live.coupleId, ev.id, patch);
    toast('変更を保存しました');
  };

  const deleteEvent = async () => {
    if (!isLive) { toast('プレビューでは削除されません'); setEditOpen(false); return; }
    await Amulet.deleteEvent(live.coupleId, ev.id);
    toast('🗑 デートを削除しました');
    go('home', {}, { replace: true });
  };

  /* 実際の天気を取得し、雨なら既定で「雨の日プラン」を表示（手動切替後は尊重）*/
  useE(() => {
    let dead = false;
    Weather.get(ev.raw?.date || ev.date, ev.area).then(k => {
      if (!dead && !userSetWx.current) setWeather(k);
    });
    return () => { dead = true; };
  }, []);
  const onWeather = (v) => { userSetWx.current = true; setWeather(v); };

  const items = (weather === 'sun' ? planDoc?.sunny : planDoc?.rainy) || [];
  const planTitle = planDoc?.title || '…';
  const blurb = planDoc?.blurb || '';
  const spots = items.map(it => ({ name: it.name, kind: it.kind, blurb: it.note, img: (it.tag || it.kind || 'SPOT').slice(0, 6), photo: it.photoUrl }));
  const route = items.map(it => ({ type: 'stop', time: it.time, name: it.name, kind: it.kind, note: it.note }));

  if (isLive && !planDoc) {
    return (
      <div style={{ height: '100%', background: 'var(--bg)', display: 'flex', flexDirection: 'column' }}>
        <TopBar onBack={back} right={<div style={{ width: 38 }} />} />
        <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 14 }}>
          <div style={{ width: 26, height: 26, borderRadius: '50%', border: '3px solid var(--amethyst)', borderTopColor: 'transparent', animation: 'spin 1s linear infinite' }} />
          <div style={{ fontSize: 13, color: 'var(--ink-soft)' }}>プランを読み込んでいます…</div>
        </div>
      </div>
    );
  }

  return (
    <div style={{ height: '100%', background: 'var(--bg)', display: 'flex', flexDirection: 'column', position: 'relative', overflow: 'hidden' }}>
      <TopBar onBack={back} right={
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          {isLive && (
            <button onClick={() => setEditOpen(true)} aria-label="タイトルや日付を編集" style={{
              width: 34, height: 34, borderRadius: 999, border: '1px solid var(--line-2)',
              background: 'var(--surface)', display: 'flex', alignItems: 'center', justifyContent: 'center',
              cursor: 'pointer', color: 'var(--ink-soft)', flexShrink: 0,
            }}>
              <Icon name="edit" size={16} sw={2.2} />
            </button>
          )}
          <WeatherBadge dateISO={curDateISO || ev.date} area={curArea} />
        </div>
      } />
      <Body>
        {/* title — バッジ→タイトル→日付の順にふわっと */}
        <div style={{ marginBottom: 18 }}>
          <h1 style={{ fontFamily: 'var(--font-serif)', fontWeight: 400, fontSize: 28, color: 'var(--ink)', margin: '10px 0 4px', textWrap: 'balance', animation: 'chatin .34s .06s ease both' }}>「{planTitle}」</h1>
          <div style={{ fontSize: 13, color: 'var(--ink-soft)', animation: 'chatin .34s .12s ease both' }}>{curDateLabel}（{curDow}） · {curArea}</div>
        </div>

        {/* ☀️/🌧 weather tabs (実プランのみ) */}
        {isLive && (
          <div style={{ marginBottom: 18 }}>
            <Segmented
              options={[
                { value: 'sun', label: <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><Icon name="sun" size={15} />晴れの日プラン</span> },
                { value: 'rain', label: <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}><Icon name="rain" size={15} />雨の日プラン</span> },
              ]}
              value={weather} onChange={onWeather} />
          </div>
        )}

        {/* ① main spots — 幅160で3枚目が右端から覗く＝横スクロールできると分かる。
             開いた瞬間に右から1枚ずつ滑り込む */}
        <div style={{ marginBottom: 8 }}><Eyebrow color="var(--amethyst)">主要スポット · {spots.length}</Eyebrow></div>
        <div className="noscroll" style={{ display: 'flex', gap: 12, overflowX: 'auto', margin: '10px -18px 22px', padding: '2px 18px 6px' }}>
          {spots.map((s, i) => (
            <div key={i} style={{ flexShrink: 0, width: 160, background: 'var(--surface)', borderRadius: 18, border: '1px solid var(--line)', overflow: 'hidden', boxShadow: '0 12px 26px -22px rgba(61,54,44,.5)', animation: `cardin .4s cubic-bezier(.22,1,.36,1) both`, animationDelay: `${0.12 + i * 0.08}s` }}>
              <ImgSlot label={s.img} src={s.photo} h={100} r={0} />
              <div style={{ padding: '12px 14px 14px' }}>
                <div style={{ fontSize: 10.5, fontWeight: 700, color: 'var(--amethyst)', letterSpacing: '.04em' }}>{s.kind}</div>
                <div style={{ fontWeight: 700, fontSize: 15, color: 'var(--ink)', margin: '3px 0 5px' }}>{s.name}</div>
                <div style={{ fontSize: 12, color: 'var(--ink-soft)', lineHeight: 1.6 }}>{s.blurb}</div>
              </div>
            </div>
          ))}
        </div>

        {/* ② route — 見えた瞬間に一筆書きで描かれる。天気タブ切替でも描き直す */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 12 }}>
          <Icon name="route" size={17} style={{ color: 'var(--ink-soft)' }} />
          <Eyebrow>当日のルート案内</Eyebrow>
        </div>
        <Card pad={16} style={{ marginBottom: 18 }}>
          <RouteList key={weather} route={route} draw />
        </Card>

        {/* ③ why this plan */}
        <div style={{ background: 'color-mix(in srgb, var(--amethyst) 8%, var(--paper))', border: '1px solid color-mix(in oklch, var(--amethyst) 22%, transparent)', borderRadius: 20, padding: 18, marginBottom: 18 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 9 }}>
            <Icon name="sparkle" size={17} style={{ color: 'var(--amethyst)' }} />
            <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--amethyst)' }}>どうしてこのプラン？</span>
          </div>
          <p style={{ fontSize: 13.5, lineHeight: 1.8, color: 'var(--ink-soft)', margin: 0 }}>{blurb}</p>
          <p style={{ fontSize: 11.5, lineHeight: 1.7, color: 'var(--ink-faint)', margin: '8px 0 0' }}>
            おたがいの本音の原文は、どちらにも表示されません。
          </p>
        </div>

        {/* ④ surprise hint entry — 仕掛ける側だけのひみつ。
             ふきだし（画像）をタップでこっそりメモへ。右端の犬はふきだしの話し手。 */}
        <div style={{ display: 'flex', alignItems: 'center', marginBottom: 14 }}>
          <button onClick={() => go('surprise-hint', { ev })} aria-label="こっそりメモを見る" style={{
            flex: 1, minWidth: 0, padding: 0, border: 'none', background: 'none', cursor: 'pointer',
            display: 'block', animation: 'floatup .4s ease both',
          }}>
            <img src="/assets/memo-hukidashi.png"
              alt="こっそりメモがあります — あなただけに見えるサプライズのヒント"
              style={{ width: '100%', display: 'block', filter: 'drop-shadow(0 10px 18px rgba(61,54,44,.20))' }} />
          </button>
          <LottieBox src="/assets/dog_blue.json" viewBox="560 120 640 820"
            style={{ width: 92, height: 118, flexShrink: 0, marginLeft: -4 }} />
        </div>

        {/* ⑤ プランを相談して、その場で作り変える案内（操作は右下のFABから） */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 11, background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 18, padding: 16, marginBottom: 14 }}>
          <div style={{ width: 42, height: 42, borderRadius: 13, flexShrink: 0, background: 'color-mix(in oklch, var(--amethyst) 14%, var(--paper))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <Icon name="chat" size={20} style={{ color: 'var(--amethyst)' }} />
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontWeight: 700, fontSize: 14.5, color: 'var(--ink)' }}>気になるところ、ありますか？</div>
            <div style={{ fontSize: 12, color: 'var(--ink-soft)', lineHeight: 1.55, marginTop: 2 }}>右下の「相談」から話すと、このプランをその場で作り変えます。</div>
          </div>
        </div>

        {/* spacer so FAB never overlaps content */}
        <div style={{ height: 56 }} />
      </Body>

      {/* FAB → プランを相談（その場で作り変える）。チャット中は隠す */}
      {!chatOpen && (
        <button onClick={() => setChatOpen(true)} style={{
          position: 'absolute', right: 18, bottom: 26, zIndex: 50,
          display: 'flex', alignItems: 'center', gap: 9, padding: '14px 20px 14px 17px',
          borderRadius: 999, border: 'none', cursor: 'pointer',
          background: 'var(--amethyst)', color: '#fff',
          fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 15,
          boxShadow: '0 14px 30px -10px color-mix(in oklch, var(--amethyst) 70%, transparent), 0 4px 10px rgba(40,34,28,.18)',
          animation: 'floatup .4s ease both',
        }}>
          <Icon name="chat" size={20} /> プランを相談
        </button>
      )}

      {chatOpen && (
        <PlanChatWindow ev={ev} onClose={() => setChatOpen(false)}
          title="このプランを相談"
          subtitle="話すと、その場で作り変えます"
          intro={[
            { who: 'ai', text: 'このプラン、気になるところはありますか？「もう少しのんびりにして」「夜は別のお店がいい」のように言っていただければ、その場で作り変えます。' },
          ]} />
      )}

      {editOpen && (
        <EventEditModal
          initial={{ title: curTitle, dateISO: curDateISO, area: curArea }}
          titleDateOnly
          onSave={saveBasics}
          onDelete={deleteEvent}
          onClose={() => setEditOpen(false)}
        />
      )}
    </div>
  );
}

/* ルート案内のタイムライン。draw を渡すと、表示された瞬間に縦線が上から下へ
   一筆書きのように伸びていき、停留点と内容が線の進行に合わせて順に現れる。 */
function RouteList({ route, draw }) {
  const hostRef = useR(null);
  const [started, setStarted] = useS(!draw); // draw なしなら最初から描画済み扱い
  useE(() => {
    if (!draw || started || !hostRef.current || !window.IntersectionObserver) { if (draw && !window.IntersectionObserver) setStarted(true); return; }
    const io = new IntersectionObserver(([e]) => {
      if (e.isIntersecting) { setStarted(true); io.disconnect(); }
    }, { threshold: 0 });
    io.observe(hostRef.current);
    return () => io.disconnect();
  }, [started]);

  const STEP = 0.18; // 1行ぶんの進行秒数
  // アニメーション種別ごとのスタイル（draw かつ started のときだけ付与。開始前は透明で待機）
  const anim = (name, delay, dur, extra) => {
    if (!draw) return extra || {};
    if (!started) return { ...(extra || {}), opacity: 0 };
    return { ...(extra || {}), animation: `${name} ${dur}s ease both`, animationDelay: `${Math.max(0, delay)}s` };
  };
  return (
    <div ref={hostRef} style={{ position: 'relative' }}>
      {route.map((it, i) => {
        const stop = it.type === 'stop';
        const isLast = i === route.length - 1;
        const base = i * STEP;
        return (
          <div key={i} style={{ display: 'flex', gap: 12, minHeight: stop ? 44 : 38 }}>
            {/* time col */}
            <div style={{ width: 42, flexShrink: 0, textAlign: 'right', paddingTop: 1 }}>
              {stop && <span style={{ fontFamily: 'var(--font-serif)', fontSize: 14, fontWeight: 600, color: 'var(--me)', display: 'inline-block', ...anim('chatin', base + 0.04, 0.3) }}>{it.time}</span>}
            </div>
            {/* rail — 上の継ぎ足し→点→下の線 の順で伸ばすと一筆書きに見える */}
            <div style={{ width: 24, flexShrink: 0, position: 'relative', display: 'flex', justifyContent: 'center' }}>
              {!isLast && <div style={{ position: 'absolute', top: stop ? 14 : 0, bottom: -2, width: 2, background: 'var(--line-2)', transformOrigin: 'top', ...anim('growY', base + 0.08, STEP - 0.06) }} />}
              {i > 0 && <div style={{ position: 'absolute', top: 0, height: stop ? 14 : 0, width: 2, background: 'var(--line-2)', transformOrigin: 'top', ...anim('growY', base, 0.06) }} />}
              {stop ? (
                <div style={{ width: 13, height: 13, borderRadius: '50%', background: 'var(--surface)', border: '3px solid var(--me)', marginTop: 4, zIndex: 1, ...anim('dotin', base + 0.05, 0.26) }} />
              ) : (
                <div style={{ width: 26, height: 26, borderRadius: '50%', background: 'var(--surface-2)', border: '2px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1, marginTop: 4, ...anim('dotin', base + 0.05, 0.26) }}>
                  <Icon name={it.mode === 'train' ? 'train' : 'walk'} size={15} style={{ color: 'var(--ink-soft)' }} />
                </div>
              )}
            </div>
            {/* content */}
            <div style={{ flex: 1, paddingBottom: 12, minWidth: 0, ...anim('chatin', base + 0.07, 0.32) }}>
              {stop ? (
                <div>
                  <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
                    <span style={{ fontWeight: 700, fontSize: 15, color: 'var(--ink)' }}>{it.name}</span>
                    <span style={{ fontSize: 11, color: 'var(--ink-faint)' }}>{it.kind}</span>
                  </div>
                  {it.note && <div style={{ fontSize: 12, color: 'var(--ink-soft)', marginTop: 2 }}>{it.note}</div>}
                </div>
              ) : (
                <div style={{ paddingTop: 5 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 7, flexWrap: 'wrap' }}>
                    {it.line && <span style={{ fontSize: 11.5, fontWeight: 700, color: 'var(--amethyst)', background: 'color-mix(in oklch, var(--amethyst) 12%, var(--paper))', borderRadius: 6, padding: '2px 8px' }}>{it.line}</span>}
                    <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink-soft)' }}>{it.text}</span>
                  </div>
                  {it.detail && <div style={{ fontSize: 11.5, color: 'var(--ink-faint)', marginTop: 3 }}>{it.detail}</div>}
                </div>
              )}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ════════════════ SURPRISE HINT ════════════════ */
function SurpriseHintScreen({ params }) {
  const { go, back, live } = useApp();
  const ev = params?.ev || {};
  const isLive = !!(ev.live && live.coupleId);
  const partnerName = live.partner?.name || PARTNER_FALLBACK;
  const [items, setItems] = useS(undefined); // undefined=読込中 / []=ヒントなし

  useE(() => {
    if (!isLive) { setItems([]); return; } // 実データ前提。未サインインはヒントなし表示
    let dead = false;
    Amulet.getSurprise(live.coupleId, ev.id)
      .then(d => { if (!dead) setItems(d?.items || []); })
      .catch(() => { if (!dead) setItems([]); });
    return () => { dead = true; };
  }, []);

  const liveHero = items && items.length ? items[0] : null;
  const liveRest = items && items.length > 1 ? items.slice(1) : [];

  return (
    <div style={{ height: '100%', background: 'var(--bg)', display: 'flex', flexDirection: 'column' }}>
      <TopBar onBack={back} />
      <Body>
        <div style={{ display: 'inline-flex', alignItems: 'center', gap: 7, background: 'color-mix(in srgb, var(--gold) 16%, var(--paper))', border: '1px solid color-mix(in oklch, var(--gold) 32%, transparent)', borderRadius: 999, padding: '7px 14px', fontSize: 12.5, fontWeight: 700, color: 'color-mix(in oklch, var(--gold) 60%, var(--ink))', marginBottom: 16 }}>
          <span>💡 こっそりメモ（あなただけに表示中 <LottieBox src="/assets/dog_blue.json" viewBox="560 120 640 820" style={{ display: 'inline-block', width: 20, height: 20, verticalAlign: 'middle' }} />）</span>
        </div>
        <h1 style={{ fontFamily: 'var(--font-serif)', fontWeight: 400, fontSize: 26, color: 'var(--ink)', margin: '0 0 6px', lineHeight: 1.4 }}>{partnerName}を、そっと喜ばせる</h1>
        <p style={{ fontSize: 13.5, color: 'var(--ink-soft)', lineHeight: 1.7, margin: '0 0 22px' }}>
          あなたの「小言キーパー」のメモから、今回のプランでできるサプライズを見つけました。相手には表示されません。
        </p>

        {items === undefined && (
          <div style={{ textAlign: 'center', padding: '30px 0', color: 'var(--ink-faint)', fontSize: 13 }}>ヒントを探しています…</div>
        )}

        {items && items.length === 0 && (
          <Card pad={20} style={{ textAlign: 'center', marginBottom: 16 }}>
            <div style={{ fontSize: 30, marginBottom: 8 }}>🌱</div>
            <div style={{ fontWeight: 700, fontSize: 15, color: 'var(--ink)' }}>まだヒントがありません</div>
            <p style={{ fontSize: 12.5, color: 'var(--ink-soft)', lineHeight: 1.7, margin: '6px 0 0' }}>
              なんでも相談チャットで「相手が言っていたこと」をメモすると、プラン生成のときにサプライズ案がここに届きます。
            </p>
          </Card>
        )}

        {liveHero && (
          <Card pad={0} style={{ overflow: 'hidden', marginBottom: 16 }}>
            <ImgSlot label={liveHero.spot?.name || 'SECRET'} src={liveHero.spot?.photoUrl} h={132} r={0} />
            <div style={{ padding: 17 }}>
              <Eyebrow color="var(--gold)">{liveHero.spot ? `経路の近く · ${liveHero.spot.name}` : 'サプライズ案'}</Eyebrow>
              <div style={{ fontWeight: 700, fontSize: 17, color: 'var(--ink)', margin: '7px 0 6px' }}>{liveHero.title || 'ちいさなサプライズ'}</div>
              <p style={{ fontSize: 13, color: 'var(--ink-soft)', lineHeight: 1.7, margin: 0 }}>{liveHero.text}</p>
            </div>
          </Card>
        )}

        {liveRest.length > 0 && (
          <div>
            <Eyebrow>他のヒント</Eyebrow>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 10 }}>
              {liveRest.map((it, i) => (
                <div key={i} style={{ display: 'flex', gap: 12, alignItems: 'center', background: 'var(--surface)', borderRadius: 16, padding: 14, border: '1px solid var(--line)' }}>
                  {it.spot?.photoUrl
                    ? <ImgSlot label="" src={it.spot.photoUrl} h={46} r={12} style={{ width: 46, flexShrink: 0 }} />
                    : <div style={{ width: 46, height: 46, borderRadius: 12, background: 'color-mix(in oklch, var(--gold) 14%, var(--surface-2))', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}><Icon name="bag" size={19} style={{ color: 'var(--gold)' }} /></div>
                  }
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontWeight: 700, fontSize: 14, color: 'var(--ink)' }}>{it.title || it.spot?.name || 'サプライズ案'}</div>
                    <div style={{ fontSize: 11.5, color: 'var(--ink-faint)', marginTop: 1 }}>{it.text}</div>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}

        <div style={{ marginTop: 22 }}>
          <Btn variant="soft" size="lg" full icon="back" onClick={back}>プランに戻る</Btn>
        </div>
      </Body>
    </div>
  );
}

/* ════════════════ PLAN PENDING (考案前 + 相談チャット + 生成CTA) ════════════════ */
function FixedStop({ time, name, kind, first, last }) {
  return (
    <div style={{ display: 'flex', gap: 12, minHeight: 44 }}>
      <div style={{ width: 42, flexShrink: 0, textAlign: 'right', paddingTop: 1 }}>
        <span style={{ fontFamily: 'var(--font-serif)', fontSize: 14, fontWeight: 600, color: 'var(--me)' }}>{time}</span>
      </div>
      <div style={{ width: 24, flexShrink: 0, position: 'relative', display: 'flex', justifyContent: 'center' }}>
        {!last && <div style={{ position: 'absolute', top: 14, bottom: -6, width: 2, background: 'var(--line-2)' }} />}
        {!first && <div style={{ position: 'absolute', top: -6, height: 20, width: 2, background: 'var(--line-2)' }} />}
        <div style={{ width: 13, height: 13, borderRadius: '50%', background: 'var(--surface)', border: '3px solid var(--me)', marginTop: 4, zIndex: 1 }} />
      </div>
      <div style={{ flex: 1, paddingBottom: 14, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
          <span style={{ fontWeight: 700, fontSize: 15, color: 'var(--ink)' }}>{name}</span>
          <span style={{ fontSize: 11, color: 'var(--ink-faint)' }}>{kind}</span>
        </div>
      </div>
    </div>
  );
}

function PendingStop() {
  return (
    <div style={{ display: 'flex', gap: 12, minHeight: 44 }}>
      <div style={{ width: 42, flexShrink: 0 }} />
      <div style={{ width: 24, flexShrink: 0, position: 'relative', display: 'flex', justifyContent: 'center' }}>
        <div style={{ position: 'absolute', top: -6, bottom: -6, width: 2, background: 'var(--line-2)' }} />
        <div style={{ width: 13, height: 13, borderRadius: '50%', background: 'var(--surface)', border: '3px solid var(--line-2)', marginTop: 4, zIndex: 1 }} />
      </div>
      <div style={{ flex: 1, paddingBottom: 14, minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ fontWeight: 700, fontSize: 15, color: 'var(--ink-soft)' }}>考え中</span>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 11, fontWeight: 700, color: 'var(--amethyst)' }}>
            <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--amethyst)', animation: 'glowpulse 1.8s ease-in-out infinite' }} />相談中
          </span>
        </div>
        <div style={{ fontSize: 12.5, color: 'var(--ink-faint)', marginTop: 3 }}>ふたりの意見で決まっていきます</div>
      </div>
    </div>
  );
}

/* ── 生成中のリビール演出（ローディングを兼ねる）──
   実測で20〜25秒（planner 単体で ~19s）かかる。ただの待ちにすると「壊れたのでは」と思われるため、
   犬が自分の思考を実況しながら進む。段は backend の実処理に対応させてある:
     grounding = searchAreaSpots / thinking = planReply(ADK planner) / enrich = enrichPlanBody / saving = writePlanVersion
   backend が events/{id}.planProgress.stage を書けばそれに追従し、無ければ経過時間から推定する。
   進捗バーは 96% に漸近するだけで自力では到達しない ＝ 完了を先に約束しない。 */
const PLAN_STAGES = [
  {
    key: 'grounding', label: 'まちを歩いています', secs: 3,
    lines: [
      'まちの匂いを、くんくん嗅いでいます',
      'このへん、よさそうな気配がする…',
      '角を曲がって、もう一本先まで見てくるね',
    ],
  },
  {
    key: 'thinking', label: 'ふたりの本音を読んでいます', secs: 19,
    lines: [
      'ふたりの本音を、こっそり読んでいます',
      'ここ、並びそうだなあ…と唸っています',
      '歩く距離を、しっぽで測っています',
      'おなかが空く時間から、逆算しています',
      '雨が降った日の道も、掘っておきますね',
      'サプライズを、どこに埋めようかな',
      '順番を並べかえて、うーん、うーん',
      'ふたりの「好き」が重なるところを探しています',
      '休憩できるベンチも、覚えておこう',
      'ちょっと欲張りすぎたかも…一つ減らします',
    ],
  },
  {
    key: 'enrich', label: '写真と地図を集めています', secs: 4,
    lines: [
      'お店の写真を、くわえて持ってきました',
      '地図にピンを、ぽす、ぽす',
      '写真がボケてないか、鼻先で確認中',
    ],
  },
  {
    key: 'saving', label: 'しおりに清書しています', secs: 2,
    lines: [
      'しおりに、清書しています',
      '最後にリボンを、きゅっと結んで',
    ],
  },
];
const PLAN_TOTAL_SECS = PLAN_STAGES.reduce((a, s) => a + s.secs, 0);
const LONG_WAIT_LINE = 'ちょっと長考しています…もう少しだけ待ってね';
const SORRY_LINE = 'ごめんなさい。うまくまとめきれませんでした…';

/* 経過秒から、いま何段目にいるかを推定する（backend の planProgress が無いときのフォールバック） */
const stageFromElapsed = (sec) => {
  let acc = 0;
  for (let i = 0; i < PLAN_STAGES.length; i++) {
    acc += PLAN_STAGES[i].secs;
    if (sec < acc) return i;
  }
  return PLAN_STAGES.length - 1;
};

function GeneratingOverlay({ progress, byPartner, error, onRetry, onClose }) {
  const [sec, setSec] = useS(0);
  const [tick, setTick] = useS(0);      // セリフの送り
  useE(() => {
    if (error) return;                  // 失敗したら時計もセリフも止める
    setSec(0); setTick(0);              // 再試行では最初から数え直す
    const a = setInterval(() => setSec(v => v + 1), 1000);
    const b = setInterval(() => setTick(v => v + 1), 2400);
    return () => { clearInterval(a); clearInterval(b); };
  }, [error]);

  // backend が段を教えてくれるならそれを信じる。無ければ経過時間から推定。
  const realIdx = progress?.stage ? PLAN_STAGES.findIndex(s => s.key === progress.stage) : -1;
  const idx = realIdx >= 0 ? realIdx : stageFromElapsed(sec);
  const stage = PLAN_STAGES[idx];

  // 予定より長引いたら、犬も正直に「長考中」と言う
  const overtime = sec > PLAN_TOTAL_SECS + 6;
  const line = error ? SORRY_LINE
    : overtime && tick % 3 === 2 ? LONG_WAIT_LINE
    : stage.lines[tick % stage.lines.length];

  // 96% への漸近。完了は親のアンマウントでしか起きない。
  const eased = 96 * (1 - Math.exp(-sec / (PLAN_TOTAL_SECS * 0.45)));
  const floor = (PLAN_STAGES.slice(0, idx).reduce((a, s) => a + s.secs, 0) / PLAN_TOTAL_SECS) * 96;
  const pct = Math.min(96, Math.max(eased, floor));

  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 90, background: 'color-mix(in srgb, var(--bg) 92%, transparent)', backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8, padding: '0 28px', animation: 'risefade .3s ease' }}>

      {/* 犬のふきだし ＝ 思考の実況。しっぽ（三角）で下の犬に繋ぐ */}
      <div key={line} style={{
        position: 'relative', maxWidth: 300, background: 'var(--paper)', border: '1px solid var(--line)',
        borderRadius: 20, padding: '13px 17px', boxShadow: '0 8px 22px rgba(61,54,44,.10)',
        fontSize: 13.5, lineHeight: 1.65, color: 'var(--ink)', textAlign: 'center', animation: 'risefade .5s ease',
      }} aria-live="polite">
        {line}
        <span style={{
          position: 'absolute', left: '50%', bottom: -6, width: 11, height: 11, marginLeft: -6,
          background: 'var(--paper)', borderRight: '1px solid var(--line)', borderBottom: '1px solid var(--line)',
          transform: 'rotate(45deg)', borderRadius: 2,
        }} />
      </div>

      {/* 失敗したら犬も足を止める（動き続けると「まだやっている」ように見えるため） */}
      <LottieBox src="/assets/dog_blue.json" viewBox="560 120 640 820" paused={!!error}
        style={{ width: 132, height: 168, marginTop: -8, filter: error ? 'grayscale(.55)' : 'none', transition: 'filter .5s ease' }} />

      <div style={{ textAlign: 'center', width: '100%', maxWidth: 300 }}>
        <div style={{ fontFamily: 'var(--font-serif)', fontSize: 21, color: 'var(--ink)', marginBottom: 4 }}>
          {error ? 'プランを作れませんでした'
            : byPartner ? 'パートナーがプランを生み出しています'
            : 'プランを生み出しています'}
        </div>

        {error ? (
          <>
            <div style={{ fontSize: 12.5, color: 'var(--ink-soft)', lineHeight: 1.7, marginTop: 8 }}>{error}</div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 9, marginTop: 20 }}>
              <Btn variant="amethyst" size="lg" full icon="sparkle" onClick={onRetry}>もう一度ためす</Btn>
              <Btn variant="ghost" full onClick={onClose}>とじる</Btn>
            </div>
          </>
        ) : (
          <>
            {/* 進捗バー */}
            <div style={{ height: 5, borderRadius: 999, background: 'var(--line-2)', overflow: 'hidden', margin: '12px 0 8px' }}>
              <div style={{ height: '100%', width: `${pct}%`, borderRadius: 999, background: 'linear-gradient(90deg, var(--me), var(--amethyst), var(--partner))', transition: 'width .9s ease-out' }} />
            </div>

            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, fontSize: 12, color: 'var(--ink-soft)' }}>
              <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--amethyst)', animation: 'glowpulse 1.8s ease-in-out infinite', flexShrink: 0 }} />
              <span>{stage.label}</span>
              <span style={{ color: 'var(--ink-faint)', fontVariantNumeric: 'tabular-nums' }}>{sec}s</span>
            </div>

            {/* 段のインジケータ */}
            <div style={{ display: 'flex', justifyContent: 'center', gap: 6, marginTop: 12 }}>
              {PLAN_STAGES.map((s, i) => (
                <span key={s.key} style={{
                  width: i === idx ? 18 : 6, height: 6, borderRadius: 999,
                  background: i <= idx ? 'var(--amethyst)' : 'var(--line-2)',
                  opacity: i < idx ? 0.45 : 1, transition: 'width .3s ease, background .3s ease',
                }} />
              ))}
            </div>
          </>
        )}
      </div>
    </div>
  );
}

/* draggable chat window — occupies the lower portion, resize by dragging the grabber */
function PlanChatWindow({ ev, onClose, title = 'プランを相談', subtitle = '気になること、なんでも相談', intro }) {
  const { live, toast } = useApp();
  const [frac, setFrac] = useS(0.56);
  const rootRef = useR(null);
  const rectRef = useR(null);
  const drag = useR(false);
  const chat = useAmuletChat(ev, live, toast, intro, 'plan');
  const [text, setText] = useS('');
  const scroller = useR(null);

  useE(() => {
    const move = e => {
      if (!drag.current || !rectRef.current) return;
      e.preventDefault();
      const r = rectRef.current;
      let f = (r.bottom - e.clientY) / r.height;
      f = Math.max(0.34, Math.min(0.94, f));
      setFrac(f);
    };
    const up = () => { drag.current = false; document.body.style.cursor = ''; };
    window.addEventListener('pointermove', move, { passive: false });
    window.addEventListener('pointerup', up);
    return () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); };
  }, []);
  useE(() => { const el = scroller.current; if (el) el.scrollTop = el.scrollHeight; }, [chat.msgs, chat.typing, frac]);

  /* キーボードの出入りで表示領域が変わったら会話を最下部に貼りつける */
  useE(() => {
    const vv = window.visualViewport;
    if (!vv) return;
    const pin = () => { const el = scroller.current; if (el) el.scrollTop = el.scrollHeight; };
    vv.addEventListener('resize', pin);
    return () => vv.removeEventListener('resize', pin);
  }, []);

  const startDrag = e => {
    drag.current = true;
    rectRef.current = rootRef.current.parentElement.getBoundingClientRect();
    document.body.style.cursor = 'ns-resize';
    e.preventDefault();
  };
  const send = () => { const v = text; if (!v.trim()) return; setText(''); chat.send(v); };

  return (
    <>
      {/* scrim */}
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, zIndex: 55, background: 'rgba(40,34,28,.30)', backdropFilter: 'blur(2px)', WebkitBackdropFilter: 'blur(2px)', animation: 'risefade .22s' }} />
      {/* window */}
      <div ref={rootRef} style={{
        position: 'absolute', left: 0, right: 0, bottom: 0, zIndex: 60,
        height: `${frac * 100}%`,
        background: 'var(--paper)', borderRadius: '26px 26px 0 0',
        boxShadow: '0 -14px 44px rgba(40,34,28,.22)',
        display: 'flex', flexDirection: 'column', overflow: 'hidden',
        transition: drag.current ? 'none' : 'height .2s ease',
        animation: 'floatup .28s ease',
      }}>
        {/* draggable header */}
        <div onPointerDown={startDrag} style={{ cursor: 'ns-resize', touchAction: 'none', paddingTop: 8, flexShrink: 0, borderBottom: '1px solid var(--line)' }}>
          <div style={{ width: 44, height: 5, borderRadius: 9, background: 'var(--line-2)', margin: '0 auto 10px' }} />
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '0 14px 12px' }}>
            <AiAvatar size={32} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontWeight: 700, fontSize: 15, color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</div>
              <div style={{ fontSize: 11, color: 'var(--ink-faint)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{subtitle}</div>
            </div>
            <PrivacyBadge>相手には見えません</PrivacyBadge>
            <button onClick={onClose} style={{ width: 34, height: 34, borderRadius: '50%', border: '1px solid var(--line-2)', background: 'var(--surface)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: 'var(--ink-soft)', flexShrink: 0 }}><Icon name="chevD" size={19} /></button>
          </div>
        </div>
        {/* messages */}
        <div ref={scroller} className="noscroll" style={{ flex: 1, overflowY: 'auto', padding: '16px 16px 12px', display: 'flex', flexDirection: 'column', gap: 14 }}>
          {chat.loading && (
            <div style={{ textAlign: 'center', color: 'var(--ink-faint)', fontSize: 12.5, padding: '24px 0' }}>これまでの会話を読み込んでいます…</div>
          )}
          {chat.msgs.map((m, i) =>
          m.kind === 'proposal' ? <ProposalBlock key={i} spots={m.spots} onPick={chat.pick} />
          : m.kind === 'plan_proposal' ? <PlanProposalCard key={i} onApply={chat.apply} />
          : m.kind === 'event_suggest' ? <EventSuggestCard key={i} data={m.data} />
          : <Bubble key={i} m={m} />)}
          {chat.typing && <TypingBubble />}
          <div style={{ height: 2 }} />
        </div>
        {/* input */}
        {/* キーボードが出たら下パディングを詰める（--kbh は app.jsx が配布） */}
        <div style={{ padding: '10px 12px', paddingBottom: 'max(12px, calc(22px - var(--kbh, 0px)))', background: 'var(--paper)', borderTop: '1px solid var(--line)', flexShrink: 0 }}>
          <div style={{ display: 'flex', alignItems: 'flex-end', gap: 8 }}>
            <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 8, background: 'var(--surface)', border: '1px solid var(--line-2)', borderRadius: 22, padding: '4px 6px 4px 16px' }}>
              <textarea value={text} onChange={e => setText(e.target.value)}
                onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); send(); } }}
                placeholder="行きたい場所や、気分を…" style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', minHeight: 38, maxHeight: 120, paddingTop: 9, paddingBottom: 0, fontFamily: 'var(--font-ui)', fontSize: 15, color: 'var(--ink)', resize: 'none' }} />
              <MicButton onText={appendVoice(setText)} />
            </div>
            <button onClick={send} style={{ width: 46, height: 46, borderRadius: '50%', border: 'none', background: text.trim() && !chat.typing ? 'var(--amethyst)' : 'var(--line-2)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'background .2s', flexShrink: 0 }}><Icon name="send" size={20} /></button>
          </div>
        </div>
      </div>
    </>
  );
}

function PlanPendingScreen({ params, startChatOpen }) {
  const { go, back, live, toast } = useApp();
  const ev = params?.ev || {};
  const isLive = !!(ev.live && live.coupleId);
  const [chat, setChat] = useS(!!startChatOpen);
  const [generating, setGenerating] = useS(false);
  const [genError, setGenError] = useS(null);   // 生成失敗のメッセージ（null=失敗していない）
  const [evDoc, setEvDoc] = useS(null);
  const [editOpen, setEditOpen] = useS(false);
  const [confirmGen, setConfirmGen] = useS(false);
  const wasGenerating = useR(false);

  /* 主要スポット選び: Amulet Map の pin をモーダルで一覧 → 選んだものをこの画面に表示（フロント完結） */
  const [pins, setPins] = useS(null); // null=読込中 / []=なし
  const [selectedIds, setSelectedIds] = useS(() => new Set());
  const [spotModal, setSpotModal] = useS(false);

  /* live: planStatus を購読（パートナーが生成した場合もここで気づく） */
  useE(() => {
    if (!isLive) return;
    return Amulet.listenEvent(live.coupleId, ev.id, setEvDoc);
  }, []);

  /* Amulet Map のスポット（couples/{id}/pins）を購読 */
  useE(() => {
    if (!isLive) { setPins([]); return; }
    return Amulet.listenPins(live.coupleId, docs => setPins(docs.map(d => ({
      id: d.id, name: d.name, cat: d.category || 'スポット', note: d.note || '', photo: d.photoUrl, lat: d.lat, lng: d.lng,
    }))));
  }, []);

  const allPins = pins || [];
  const selectedSpots = allPins.filter(p => selectedIds.has(p.id));
  const toggleSpot = (id) => setSelectedIds(prev => {
    const next = new Set(prev);
    next.has(id) ? next.delete(id) : next.add(id);
    return next;
  });

  const planStatus = evDoc?.planStatus || ev.planStatus || 'none';
  const partnerGenerating = isLive && planStatus === 'generating' && !generating;

  /* パートナー起点の生成が完了したら自動でリビールへ */
  useE(() => {
    if (partnerGenerating) wasGenerating.current = true;
    if (wasGenerating.current && planStatus === 'ready' && !generating) {
      wasGenerating.current = false;
      go('plan-reveal', { ev }, { replace: true });
    }
  }, [planStatus]);

  /* 失敗はトーストで流さず、オーバーレイをそのまま失敗表示に切り替えて、その場で再試行させる。
     （生成は20秒超かかるため、画面を戻して押し直させると「もう一度待つ」心理的負担が大きい） */
  const generate = async () => {
    setGenError(null);
    setGenerating(true);
    try {
      if (isLive) {
        // 選んだ主要スポットを「必ず入れる」として生成リクエストに載せる（晴れ/雨どちらのプランにも組み込ませる）
        const requiredSpots = selectedSpots.map(s => ({ name: s.name, category: s.cat, lat: s.lat, lng: s.lng, note: s.note }));
        await Promise.all([Amulet.generatePlan(ev.id, undefined, requiredSpots), delay(3000)]);
      } else {
        await delay(3000);
      }
      window.track && window.track('plan_generate', { ok: 'true' });
      go('plan-reveal', { ev }, { replace: true });
    } catch (e) {
      window.track && window.track('plan_generate', { ok: 'false' });
      console.error(e);
      setGenError(Amulet.friendlyError(e));   // generating は立てたまま＝オーバーレイを保持
    }
  };

  const closeGenError = () => { setGenError(null); setGenerating(false); };

  const planReady = isLive && planStatus === 'ready';
  const stats = evDoc?.inputStats || {};
  const myCount = (live.user && stats[live.user.uid]?.messageCount) || 0;
  const theirCount = live.user
    ? Object.entries(stats).filter(([k]) => k !== live.user.uid).reduce((a, [, v]) => a + (v?.messageCount || 0), 0)
    : 0;

  /* ── 基本情報（この画面で後から編集できる）──
     live購読(evDoc) を最優先。無ければ params(ev / ev.raw) にフォールバック。
     evDoc.date / ev.raw.date は ISO("YYYY-MM-DD")、ev.date は表示ラベル。 */
  const raw = evDoc || ev.raw || {};
  const curTitle = (raw.title || ev.title || 'ただの週末');
  const curDateLabel = raw.dateLabel || ev.date || '';
  const curDateISO = (raw.date && /^\d{4}-\d{1,2}-\d{1,2}/.test(raw.date)) ? raw.date : (ev.raw?.date || null);
  const curDow = raw.dow || ev.dow || '土';
  const curArea = raw.area || ev.area || '';
  const curTimeRange = raw.timeRange || null; // ["12:00","19:00"]
  const timeLabel = (Array.isArray(curTimeRange) && curTimeRange.length === 2) ? `${curTimeRange[0]}–${curTimeRange[1]}` : null;
  const [tStart, tEnd] = (Array.isArray(curTimeRange) && curTimeRange.length === 2) ? curTimeRange : ['13:00', '19:00'];

  const saveBasics = async (patch) => {
    if (!isLive) { toast('プレビューでは保存されません'); return; }
    await Amulet.updateEvent(live.coupleId, ev.id, patch);
    toast('変更を保存しました');
  };

  const deleteEvent = async () => {
    if (!isLive) { toast('プレビューでは削除されません'); setEditOpen(false); return; }
    await Amulet.deleteEvent(live.coupleId, ev.id);
    toast('🗑 デートを削除しました');
    go('home', {}, { replace: true });
  };

  // 生成CTA: 相手の本音が一つも無いまま生み出そうとしたら、まず確認を挟む
  const requestGenerate = () => {
    if (isLive && theirCount === 0) { setConfirmGen(true); return; }
    generate();
  };

  return (
    <div style={{ height: '100%', background: 'var(--bg)', display: 'flex', flexDirection: 'column', position: 'relative', overflow: 'hidden' }}>
      <TopBar onBack={back} right={<div style={{ width: 38 }} />} />
      <Body>
        {/* title + 基本情報（編集ボタンから変更） */}
        <div style={{ marginBottom: 18 }}>
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: 7, background: 'var(--surface)', border: '1px solid var(--line-2)', borderRadius: 999, padding: '5px 12px', fontSize: 11.5, fontWeight: 700, color: 'var(--ink-soft)' }}>
            <span style={{ width: 7, height: 7, borderRadius: '50%', background: planReady ? 'var(--partner)' : 'var(--amethyst)', animation: 'glowpulse 1.8s ease-in-out infinite' }} />
            {planReady ? 'プランができました' : 'プラン考案中'}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '10px 0 4px' }}>
            <h1 style={{ flex: 1, minWidth: 0, fontFamily: 'var(--font-serif)', fontWeight: 400, fontSize: 28, color: 'var(--ink)', margin: 0, textWrap: 'balance' }}>{curTitle}</h1>
            <button onClick={() => setEditOpen(true)} style={{
              display: 'inline-flex', alignItems: 'center', gap: 6, cursor: 'pointer', flexShrink: 0,
              background: 'color-mix(in oklch, var(--amethyst) 12%, var(--paper))',
              border: '1.5px solid color-mix(in oklch, var(--amethyst) 34%, transparent)', borderRadius: 999,
              padding: '9px 16px', color: 'var(--amethyst)', fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 14,
            }}>
              <Icon name="edit" size={16} sw={2.2} /> 編集
            </button>
          </div>
          <div style={{ fontSize: 13, color: 'var(--ink-soft)' }}>
            {curDateLabel}（{curDow}）{curArea ? ` · ${curArea}` : ''}{timeLabel ? ` · ${timeLabel}` : ''}
          </div>
        </div>

        {/* ふたりの蓄積（量だけ。中身は見えない） */}
        {isLive && (
          <Card pad={14} style={{ marginBottom: 18 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <span style={{ fontSize: 17 }}>🫙</span>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--ink)' }}>ふたりの本音、たまってます</div>
                <div style={{ fontSize: 11.5, color: 'var(--ink-faint)', marginTop: 1 }}>あなた {myCount} · 相手 {theirCount}（中身はおたがい見えません）</div>
              </div>
            </div>
            <div style={{ marginTop: 10, height: 6, borderRadius: 9, background: 'var(--surface-2)', overflow: 'hidden', display: 'flex' }}>
              <div style={{ width: `${Math.min(50, myCount * 6)}%`, background: 'var(--me)' }} />
              <div style={{ width: `${Math.min(50, theirCount * 6)}%`, background: 'var(--partner)' }} />
            </div>
          </Card>
        )}

        {/* ① main spots — Amulet Map から選んで主役に据える */}
        <div style={{ marginBottom: 10, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <Eyebrow color="var(--amethyst)">主要スポット{selectedSpots.length ? ` · ${selectedSpots.length}` : ' · これから'}</Eyebrow>
          {selectedSpots.length > 0 && (
            <button onClick={() => setSpotModal(true)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--amethyst)', fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 12.5, padding: 2 }}>選びなおす</button>
          )}
        </div>

        {selectedSpots.length > 0 ? (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 22 }}>
            {selectedSpots.map(s => (
              <div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 12, background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 16, padding: 12, boxShadow: '0 10px 22px -22px rgba(61,54,44,.5)' }}>
                <ImgSlot label={s.cat} src={s.photo} h={46} r={12} style={{ width: 46, flexShrink: 0 }} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 11, color: 'var(--amethyst)', fontWeight: 700 }}>{s.cat}</div>
                  <div style={{ fontWeight: 700, fontSize: 15, color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.name}</div>
                </div>
                <button onClick={() => toggleSpot(s.id)} aria-label="外す" style={{ width: 30, height: 30, borderRadius: '50%', flexShrink: 0, border: 'none', background: 'var(--surface-2)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                  <Icon name="plus" size={15} style={{ transform: 'rotate(45deg)', color: 'var(--ink-faint)' }} />
                </button>
              </div>
            ))}
            <button onClick={() => setSpotModal(true)} style={{
              width: '100%', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
              background: 'transparent', border: '1.5px dashed color-mix(in oklch, var(--amethyst) 34%, var(--line))', borderRadius: 14, padding: '12px 14px',
              color: 'var(--amethyst)', fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 13.5,
            }}>
              <Icon name="plus" size={17} style={{ color: 'var(--amethyst)' }} sw={2.2} /> スポットを追加
            </button>
          </div>
        ) : (
          <button onClick={() => setSpotModal(true)} style={{
            width: '100%', textAlign: 'left', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 14,
            background: 'var(--surface)', border: '1px solid var(--line)', borderRadius: 18, padding: 16,
            marginBottom: 22, boxShadow: '0 12px 26px -22px rgba(61,54,44,.5)',
          }}>
            <div style={{ width: 46, height: 46, borderRadius: 14, flexShrink: 0, background: 'color-mix(in oklch, var(--amethyst) 14%, var(--paper))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <Icon name="pin" size={22} style={{ color: 'var(--amethyst)' }} />
            </div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontWeight: 700, fontSize: 15.5, color: 'var(--ink)' }}>必ず行きたい場所はある？</div>
              <div style={{ fontSize: 12.5, color: 'var(--ink-soft)', lineHeight: 1.55, marginTop: 2 }}>Amulet Map から選ぶと、そこを軸にプランを組みます。</div>
            </div>
            <div style={{ width: 30, height: 30, borderRadius: '50%', flexShrink: 0, background: 'color-mix(in oklch, var(--amethyst) 12%, var(--paper))', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <Icon name="plus" size={18} style={{ color: 'var(--amethyst)' }} sw={2.2} />
            </div>
          </button>
        )}

        {/* ② route — fixed endpoints, 途中は考え中 */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 12 }}>
          <Icon name="route" size={17} style={{ color: 'var(--ink-soft)' }} />
          <Eyebrow>当日のルート案内</Eyebrow>
        </div>
        <Card pad={16} style={{ marginBottom: 18 }}>
          <div style={{ position: 'relative' }}>
            <FixedStop time={tStart} name={`${curArea || '渋谷'} 集合`} kind="集合" first />
            <PendingStop />
            <FixedStop time={tEnd} name="解散" kind="また今度" last />
          </div>
        </Card>

        {/* ③ 生成CTA — デモのクライマックスへ */}
        <div style={{
          borderRadius: 22, padding: 18, marginBottom: 14,
          background: 'linear-gradient(150deg, color-mix(in oklch, var(--amethyst) 14%, var(--paper)) 0%, color-mix(in oklch, var(--gold) 12%, var(--paper)) 100%)',
          border: '1px solid color-mix(in oklch, var(--amethyst) 26%, transparent)',
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
            <Icon name="sparkle" size={17} style={{ color: 'var(--amethyst)' }} />
            <span style={{ fontSize: 13.5, fontWeight: 700, color: 'var(--ink)' }}>{planReady ? 'プランが届いています' : 'そろそろ、かたちにする？'}</span>
          </div>
          <p style={{ fontSize: 12.5, color: 'var(--ink-soft)', lineHeight: 1.7, margin: '0 0 14px' }}>
            {planReady
              ? 'ふたりの本音をブレンドしたプランができあがりました。'
              : 'ふたりの本音と行きたい場所から、AIが一日をデザインします。本音の原文は、おたがいに見えません。'}
          </p>
          <Btn variant="amethyst" size="lg" full icon="sparkle" disabled={generating}
            onClick={planReady ? () => go('plan-reveal', { ev }, { replace: true }) : requestGenerate}>
            {planReady ? 'できあがったプランを見る' : 'プランを生み出す'}
          </Btn>
        </div>

        {/* spacer so FAB never overlaps content */}
        <div style={{ height: 56 }} />
      </Body>

      {/* FAB → opens chat */}
      {!chat && !generating && (
        <button onClick={() => setChat(true)} style={{
          position: 'absolute', right: 18, bottom: 26, zIndex: 50,
          display: 'flex', alignItems: 'center', gap: 9, padding: '14px 20px 14px 17px',
          borderRadius: 999, border: 'none', cursor: 'pointer',
          background: 'var(--amethyst)', color: '#fff',
          fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 15,
          boxShadow: '0 14px 30px -10px color-mix(in oklch, var(--amethyst) 70%, transparent), 0 4px 10px rgba(40,34,28,.18)',
          animation: 'floatup .4s ease both',
        }}>
          <Icon name="chat" size={20} /> もっと相談する
        </button>
      )}

      {chat && (
        <PlanChatWindow
          ev={ev}
          onClose={() => setChat(false)}
          intro={raw.homeChatSummary ? [
            { who: 'ai', text: `${raw.homeChatSummary}とのことでしたね。ここからは行き先やプランの中身を一緒に決めていきましょう。` },
          ] : undefined}
        />
      )}
      {spotModal && (
        <SpotPickerModal
          pins={pins}
          selectedIds={selectedIds}
          onToggle={toggleSpot}
          onClose={() => setSpotModal(false)}
        />
      )}
      {editOpen && (
        <EventEditModal
          initial={{ title: curTitle, dateISO: curDateISO, area: curArea, timeRange: curTimeRange }}
          planReady={planReady}
          onSave={saveBasics}
          onDelete={deleteEvent}
          onClose={() => setEditOpen(false)}
        />
      )}
      {confirmGen && (
        <GenerateConfirmModal
          myCount={myCount}
          onConfirm={() => { setConfirmGen(false); generate(); }}
          onCancel={() => setConfirmGen(false)}
        />
      )}
      {(generating || partnerGenerating) && (
        <GeneratingOverlay
          progress={evDoc?.planProgress}
          byPartner={partnerGenerating && !genError}
          error={genError}
          onRetry={generate}
          onClose={closeGenError}
        />
      )}
    </div>
  );
}

/* ── 主要スポット選択モーダル ──
   Amulet Map（couples/{id}/pins）のスポットを一覧表示し、タップで選択/解除する。
   選択状態は親（PlanPendingScreen）が保持し、その場で画面の「主要スポット」に反映される。 */
function SpotPickerModal({ pins, selectedIds, onToggle, onClose }) {
  const loading = pins === null;
  const list = pins || [];
  const count = selectedIds.size;
  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
      {/* backdrop */}
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(61,54,44,.44)', backdropFilter: 'blur(2px)' }} />
      {/* sheet */}
      <div style={{ position: 'relative', background: 'var(--surface)', borderRadius: '24px 24px 0 0', height: '72%', display: 'flex', flexDirection: 'column', animation: 'risefade .28s ease' }}>
        {/* drag handle */}
        <div style={{ width: 36, height: 4, borderRadius: 999, background: 'var(--line-2)', margin: '10px auto 0', flexShrink: 0 }} />
        {/* header */}
        <div style={{ padding: '10px 18px 12px', borderBottom: '1px solid var(--line)', flexShrink: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
            <div style={{ fontWeight: 700, fontSize: 16, color: 'var(--ink)' }}>主要スポットを選ぶ</div>
            <button onClick={onClose} style={{ width: 30, height: 30, borderRadius: 999, border: 'none', background: 'var(--surface-2)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <Icon name="plus" size={15} style={{ transform: 'rotate(45deg)', color: 'var(--ink-faint)' }} />
            </button>
          </div>
          <div style={{ fontSize: 12, color: 'var(--ink-soft)', marginTop: 3 }}>Amulet Map に追加した場所から、この日の軸にする場所を選べます。</div>
        </div>

        {/* list */}
        <div className="noscroll" style={{ flex: 1, overflowY: 'auto', padding: '12px 16px 8px', display: 'flex', flexDirection: 'column', gap: 10 }}>
          {loading && (
            <div style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '20px 0', justifyContent: 'center' }}>
              <div style={{ width: 20, height: 20, borderRadius: '50%', border: '2.5px solid var(--amethyst)', borderTopColor: 'transparent', animation: 'spin 1s linear infinite' }} />
              <span style={{ fontSize: 12.5, color: 'var(--ink-faint)' }}>読み込み中…</span>
            </div>
          )}
          {!loading && list.length === 0 && (
            <div style={{ textAlign: 'center', padding: '34px 12px', color: 'var(--ink-soft)' }}>
              <div style={{ marginBottom: 8, color: 'var(--ink-faint)' }}><Icon name="pin" size={26} /></div>
              <div style={{ fontWeight: 700, fontSize: 14, color: 'var(--ink)' }}>Amulet Map にまだスポットがありません</div>
              <div style={{ fontSize: 12, marginTop: 4 }}>マップ画面の＋から行きたい場所を追加してね</div>
            </div>
          )}
          {!loading && list.map(s => {
            const on = selectedIds.has(s.id);
            return (
              <button key={s.id} onClick={() => onToggle(s.id)} style={{
                width: '100%', textAlign: 'left', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 12,
                background: on ? 'color-mix(in oklch, var(--amethyst) 8%, var(--surface))' : 'var(--surface)',
                border: `1.5px solid ${on ? 'color-mix(in oklch, var(--amethyst) 55%, transparent)' : 'var(--line)'}`,
                borderRadius: 16, padding: 12, transition: 'border-color .15s, background .15s',
              }}>
                <ImgSlot label={s.cat} src={s.photo} h={48} r={12} style={{ width: 48, flexShrink: 0 }} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 11, color: 'var(--amethyst)', fontWeight: 700 }}>{s.cat}</div>
                  <div style={{ fontWeight: 700, fontSize: 15, color: 'var(--ink)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.name}</div>
                  {s.note && <div style={{ fontSize: 11.5, color: 'var(--ink-faint)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', marginTop: 1 }}>{s.note}</div>}
                </div>
                <div style={{
                  width: 26, height: 26, borderRadius: '50%', flexShrink: 0,
                  border: on ? 'none' : '1.5px solid var(--line-2)',
                  background: on ? 'var(--amethyst)' : 'transparent',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}>
                  {on && <Icon name="check" size={15} style={{ color: '#fff' }} sw={2.6} />}
                </div>
              </button>
            );
          })}
        </div>

        {/* footer */}
        <div style={{ padding: '10px 16px 28px', borderTop: '1px solid var(--line)', flexShrink: 0 }}>
          <Btn variant="amethyst" size="lg" full onClick={onClose}>
            {count > 0 ? `${count}件を主要スポットにする` : '閉じる'}
          </Btn>
        </div>
      </div>
    </div>
  );
}

/* ── 基本情報の編集シート ──
   「新しいデート」フォーム(EventCreateScreen)と同じ MonthGrid / TimeBand / Field を使い、
   日付・時間帯・エリア・タイトルをその場で編集する。保存で onSave(patch) を呼ぶ。 */
function EventEditModal({ initial, planReady, titleDateOnly, onSave, onDelete, onClose }) {
  const now = new Date();
  const p0 = parseISODate(initial.dateISO) || { y: now.getFullYear(), mo: now.getMonth() + 1, d: now.getDate() };
  const [calY, setCalY] = useS(p0.y);
  const [calM, setCalM] = useS(p0.mo);
  const [selDay, setSelDay] = useS(p0.d);
  const [title, setTitle] = useS(initial.title === 'ただの週末' ? '' : (initial.title || ''));
  const [area, setArea] = useS(initial.area === '未定' ? '' : (initial.area || ''));
  const [timeRange, setTimeRange] = useS(parseTimeRange(initial.timeRange) || { s: 13, e: 19 });
  const [busy, setBusy] = useS(false);
  const [confirmDel, setConfirmDel] = useS(false);
  const [deleting, setDeleting] = useS(false);

  const shiftMonth = (delta) => {
    let m = calM + delta, y = calY;
    if (m < 1) { m = 12; y--; } else if (m > 12) { m = 1; y++; }
    setCalM(m); setCalY(y);
    const maxD = new Date(y, m, 0).getDate();
    if (selDay > maxD) setSelDay(maxD);
  };
  const calToday = (calY === now.getFullYear() && calM === now.getMonth() + 1) ? now.getDate() : null;

  const save = async () => {
    setBusy(true);
    const iso = `${calY}-${String(calM).padStart(2, '0')}-${String(selDay).padStart(2, '0')}`;
    const dow = WD[new Date(calY, calM - 1, selDay).getDay()];
    const dateLabel = `${calM}月${selDay}日`;
    // titleDateOnly: プラン詳細から開いたときは、タイトルと日付だけを更新する
    const patch = titleDateOnly
      ? { title: title.trim() || 'ただの週末', date: iso, dateLabel, dow }
      : {
          title: title.trim() || 'ただの週末',
          date: iso, dateLabel, dow,
          area: area.trim() || '都内',
          timeRange: [hhmm(timeRange.s), hhmm(timeRange.e)],
        };
    try {
      await onSave(patch);
      onClose();
    } catch (e) {
      console.error(e);
      setBusy(false);
    }
  };

  const doDelete = async () => {
    setDeleting(true);
    try {
      await onDelete();
      // 成功時は親側で画面遷移するので、ここでは閉じない（アンマウントされる）
    } catch (e) {
      console.error(e);
      setDeleting(false);
      setConfirmDel(false);
    }
  };

  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 60, display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
      <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(61,54,44,.44)', backdropFilter: 'blur(2px)' }} />
      <div style={{ position: 'relative', background: 'var(--surface)', borderRadius: '24px 24px 0 0', height: '86%', display: 'flex', flexDirection: 'column', animation: 'risefade .28s ease' }}>
        <div style={{ width: 36, height: 4, borderRadius: 999, background: 'var(--line-2)', margin: '10px auto 0', flexShrink: 0 }} />
        {/* header */}
        <div style={{ padding: '10px 18px 12px', borderBottom: '1px solid var(--line)', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <div style={{ fontWeight: 700, fontSize: 16, color: 'var(--ink)' }}>デートの基本情報</div>
          <button onClick={onClose} style={{ width: 30, height: 30, borderRadius: 999, border: 'none', background: 'var(--surface-2)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <Icon name="plus" size={15} style={{ transform: 'rotate(45deg)', color: 'var(--ink-faint)' }} />
          </button>
        </div>

        {/* body */}
        <div className="noscroll" style={{ flex: 1, overflowY: 'auto', padding: '16px 16px 8px' }}>
          {planReady && (
            <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, background: 'color-mix(in oklch, var(--gold) 12%, var(--paper))', border: '1px solid color-mix(in oklch, var(--gold) 26%, transparent)', borderRadius: 12, padding: '9px 12px', marginBottom: 14, fontSize: 11.5, color: 'var(--ink-soft)', lineHeight: 1.55 }}>
              <Icon name="sparkle" size={14} style={{ color: 'color-mix(in oklch, var(--gold) 60%, var(--ink))', marginTop: 1, flexShrink: 0 }} />
              <span>プランはできあがっています。日付やエリアを変えたら、プランを作り直すのがおすすめです。</span>
            </div>
          )}

          {/* calendar */}
          <Card pad={16} style={{ marginBottom: 16 }}>
            <div style={{ display: 'flex', alignItems: 'center', marginBottom: 10 }}>
              <button onClick={() => shiftMonth(-1)} style={{ width: 28, height: 28, borderRadius: 999, border: '1px solid var(--line-2)', background: 'var(--surface)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--ink-soft)' }}><Icon name="chevL" size={15} /></button>
              <div style={{ flex: 1, textAlign: 'center', fontFamily: 'var(--font-serif)', fontSize: 18, color: 'var(--ink)', fontWeight: 600 }}>{calM}月 {calY}</div>
              <button onClick={() => shiftMonth(1)} style={{ width: 28, height: 28, borderRadius: 999, border: '1px solid var(--line-2)', background: 'var(--surface)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--ink-soft)' }}><Icon name="chevR" size={15} /></button>
            </div>
            <MonthGrid year={calY} month={calM} selected={selDay} onSelect={setSelDay} today={calToday} />
          </Card>

          {/* time band / area — タイトル・日付のみ編集のときは隠す */}
          {!titleDateOnly && (
            <>
              <Card pad={18} style={{ marginBottom: 16 }}>
                <TimeBand value={timeRange} onChange={setTimeRange} />
              </Card>

              <div style={{ marginBottom: 14 }}>
                <Field label="どのあたり？" icon="pin" placeholder="「渋谷あたり」「都内」くらいでOK" value={area} onChange={setArea} />
              </div>
            </>
          )}
          <div style={{ marginBottom: 8 }}>
            <Field label="タイトル（任意）" icon="edit" placeholder="記念日 / ただの週末 …" value={title} onChange={setTitle} />
          </div>

          {/* デートを削除（モーダル一番下） */}
          {onDelete && (
            <div style={{ marginTop: 22, paddingTop: 16, borderTop: '1px solid var(--line)' }}>
              <button onClick={() => setConfirmDel(true)} style={{
                width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7,
                padding: '12px', borderRadius: 12, cursor: 'pointer',
                border: '1px solid color-mix(in oklch, var(--me) 38%, transparent)',
                background: 'color-mix(in oklch, var(--me) 7%, var(--surface))',
                color: 'var(--me)', fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 14,
              }}>
                <Icon name="plus" size={15} style={{ transform: 'rotate(45deg)' }} sw={2.4} /> このデートを削除
              </button>
              <div style={{ fontSize: 11, color: 'var(--ink-faint)', textAlign: 'center', marginTop: 8, lineHeight: 1.6 }}>
                プランや相談の内容もまとめて消えます。元に戻せません。
              </div>
            </div>
          )}
        </div>

        {/* footer */}
        <div style={{ padding: '10px 16px 28px', borderTop: '1px solid var(--line)', flexShrink: 0 }}>
          <Btn variant="amethyst" size="lg" full disabled={busy} onClick={save}>
            {busy ? '保存しています…' : '変更を保存'}
          </Btn>
        </div>

        {/* 削除の確認（元に戻せない旨を伝える） */}
        {confirmDel && (
          <div style={{ position: 'absolute', inset: 0, zIndex: 80, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
            <div onClick={() => !deleting && setConfirmDel(false)} style={{ position: 'absolute', inset: 0, background: 'rgba(61,54,44,.5)', backdropFilter: 'blur(2px)' }} />
            <div style={{ position: 'relative', width: '100%', maxWidth: 340, background: 'var(--surface)', borderRadius: 22, padding: '22px 20px 18px', boxShadow: '0 24px 60px -20px rgba(40,34,28,.5)', animation: 'floatup .26s ease both' }}>
              <div style={{ width: 46, height: 46, borderRadius: 14, background: 'color-mix(in oklch, var(--me) 14%, var(--paper))', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 14 }}>
                <Icon name="plus" size={22} style={{ transform: 'rotate(45deg)', color: 'var(--me)' }} sw={2.4} />
              </div>
              <div style={{ fontFamily: 'var(--font-serif)', fontSize: 20, fontWeight: 600, color: 'var(--ink)', marginBottom: 8 }}>このデートを削除しますか？</div>
              <p style={{ fontSize: 13, color: 'var(--ink-soft)', lineHeight: 1.7, margin: '0 0 18px' }}>
                プランや相談の内容もまとめて消えます。<b>元に戻せません。</b>
              </p>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                <button onClick={doDelete} disabled={deleting} style={{
                  width: '100%', padding: '13px', borderRadius: 12, border: 'none',
                  background: 'var(--me)', color: '#fff', fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 15,
                  cursor: deleting ? 'default' : 'pointer', opacity: deleting ? 0.6 : 1,
                }}>{deleting ? '削除しています…' : '削除する'}</button>
                <Btn variant="soft" size="lg" full disabled={deleting} onClick={() => setConfirmDel(false)}>やめる</Btn>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

/* ── 生成前の確認ダイアログ ──
   相手の本音が一つも溜まっていないのに「生み出す」を押したとき、
   まだ相手の意見を聞けていない旨を伝えて意思確認する。 */
function GenerateConfirmModal({ myCount, onConfirm, onCancel }) {
  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 70, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div onClick={onCancel} style={{ position: 'absolute', inset: 0, background: 'rgba(61,54,44,.5)', backdropFilter: 'blur(2px)' }} />
      <div style={{ position: 'relative', width: '100%', maxWidth: 340, background: 'var(--surface)', borderRadius: 22, padding: '22px 20px 18px', boxShadow: '0 24px 60px -20px rgba(40,34,28,.5)', animation: 'floatup .26s ease both' }}>
        <div style={{ width: 46, height: 46, borderRadius: 14, background: 'color-mix(in oklch, var(--gold) 16%, var(--paper))', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 14 }}>
          <Icon name="chat" size={22} style={{ color: 'color-mix(in oklch, var(--gold) 62%, var(--ink))' }} />
        </div>
        <div style={{ fontFamily: 'var(--font-serif)', fontSize: 20, fontWeight: 600, color: 'var(--ink)', marginBottom: 8 }}>まだ相手の本音がありません</div>
        <p style={{ fontSize: 13, color: 'var(--ink-soft)', lineHeight: 1.7, margin: '0 0 18px' }}>
          相手はまだ本音を残していないようです。
          {myCount > 0 ? '今はあなたの入力だけでプランを組みます。' : 'ふたりの本音がまだ届いていません。'}
          このまま生み出しますか？（あとで作り直せます）
        </p>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          <Btn variant="amethyst" size="lg" full icon="sparkle" onClick={onConfirm}>このまま生み出す</Btn>
          <Btn variant="soft" size="lg" full onClick={onCancel}>相手の本音を待つ</Btn>
        </div>
      </div>
    </div>
  );
}

function PlanChatScreen({ params }) { return <PlanPendingScreen params={params} startChatOpen />; }

Object.assign(window, { HomeChatScreen, PlanPendingScreen, PlanChatScreen, PlanRevealScreen, SurpriseHintScreen, Sheet, BatterySwipe, RouteList });

})();
