// Fun-Ture v2 — shell + app
const { HomeScreen, ListScreen, TalkScreen, MemoriesScreen, UsScreen } = window.FTScreens;
const { Avatar, useData, useProfiles, Icon, Modal, Sheet, Btn } = window.FTUI;
const { useState, useEffect, useRef } = React;

function TopBar({ me, onSwitchUser }){
  const partners = usePartners();
  const d = new Date();
  const dateStr = d.toLocaleDateString('en', {weekday:'long', month:'short', day:'numeric'});
  // 3-way theme: 'auto' (follow system) | 'light' | 'dark'
  const [themeMode, setThemeMode] = useState(localStorage.getItem('ft-v2-theme-mode') || 'auto');
  const applyTheme = (mode) => {
    let resolved = mode;
    if(mode === 'auto'){
      resolved = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    }
    document.documentElement.setAttribute('data-theme', resolved);
    // legacy key for other reads
    localStorage.setItem('ft-v2-theme', resolved);
  };
  useEffect(() => {
    applyTheme(themeMode);
    localStorage.setItem('ft-v2-theme-mode', themeMode);
    if(themeMode === 'auto' && window.matchMedia){
      const mq = window.matchMedia('(prefers-color-scheme: dark)');
      const handler = () => applyTheme('auto');
      mq.addEventListener?.('change', handler);
      return () => mq.removeEventListener?.('change', handler);
    }
  }, [themeMode]);
  const cycle = () => {
    const order = ['auto','light','dark'];
    setThemeMode(order[(order.indexOf(themeMode)+1)%3]);
  };
  const glyph = themeMode==='auto' ? '◐' : themeMode==='dark' ? '🌙' : '☀️';
  const label = themeMode==='auto' ? 'Auto (follows system)' : themeMode==='dark' ? 'Dark' : 'Light';
  return (
    <div className="ft-topbar">
      <div>
        <div className="ft-top-title ft-shimmer">The Fun-Ture List</div>
        <div className="ft-top-date">{dateStr}</div>
      </div>
      <div style={{display:'flex',alignItems:'center',gap:10}}>
        <button onClick={cycle} style={{width:34,height:34,borderRadius:'50%',background:'var(--card)',border:'1px solid var(--border2)',color:'var(--text2)',fontSize:themeMode==='auto'?17:15,display:'flex',alignItems:'center',justifyContent:'center',cursor:'pointer'}} title={`Theme: ${label} · tap to cycle`}>
          <span>{glyph}</span>
        </button>
        <div className="ft-top-avatars" onClick={onSwitchUser} style={{cursor:'pointer'}}>
          {partners.map((p, i) => <Avatar key={i} person={p} size={30} ring={FT.normKey(me)===FT.normKey(p)}/>)}
        </div>
      </div>
    </div>
  );
}

function BottomNav({ tab, onChange }){
  const tabs = [
    {k:'home', label:'Home', ic:<Icon.home/>},
    {k:'list', label:'List', ic:<Icon.list/>},
    {k:'talk', label:'Talk', ic:<Icon.chat/>},
    {k:'mem', label:'Memories', ic:<Icon.heart/>},
    {k:'us', label:'Us', ic:<Icon.us/>},
  ];
  return (
    <nav className="ft-nav">
      {tabs.map(t => (
        <button key={t.k} className={`ft-nav-btn ${tab===t.k?'a':''}`} onClick={()=>onChange(t.k)}>
          {t.ic}
          <span>{t.label}</span>
        </button>
      ))}
    </nav>
  );
}

function Splash({ hide }){
  return (
    <div className={`ft-splash ${hide?'hide':''}`}>
      <div className="ft-splash-heart">♥</div>
      <div className="ft-splash-title">The Fun-Ture List</div>
      <div className="ft-splash-sub">{(FT.partners||[]).join(' & ')}</div>
    </div>
  );
}

// Simple confetti — hearts
function burstAt(el){
  if(!el) return;
  const rect = el.getBoundingClientRect();
  const cx = rect.left + rect.width/2;
  const cy = rect.top + rect.height/2;
  const container = document.getElementById('v2Confetti');
  if(!container) return;
  const colors = ['#E8A5C0','#B9A4E3','#F2B988','#8BB8B4','#E8C87A','#F09090'];
  const N = 16;
  for(let i=0;i<N;i++){
    const p = document.createElement('div');
    const sz = 8 + Math.random()*8;
    const isHeart = Math.random() < 0.5;
    p.style.cssText = `position:absolute;left:${cx}px;top:${cy}px;font-size:${sz+4}px;color:${colors[i%colors.length]};pointer-events:none;transform:translate(-50%,-50%);will-change:transform,opacity;text-shadow:0 2px 6px rgba(0,0,0,.35)`;
    p.textContent = isHeart ? '♥' : '•';
    container.appendChild(p);
    const angle = Math.random()*Math.PI*2;
    const dist = 40 + Math.random()*90;
    const dx = Math.cos(angle)*dist;
    const dy = Math.sin(angle)*dist - 20;
    const rot = (Math.random()-0.5)*360;
    p.animate([
      {transform:'translate(-50%,-50%) rotate(0) scale(.3)', opacity:0},
      {transform:`translate(calc(-50% + ${dx*0.5}px),calc(-50% + ${dy*0.5}px)) rotate(${rot*0.5}deg) scale(1)`, opacity:1, offset:0.3},
      {transform:`translate(calc(-50% + ${dx}px),calc(-50% + ${dy+60}px)) rotate(${rot}deg) scale(.5)`, opacity:0}
    ], {duration:800+Math.random()*400, easing:'cubic-bezier(.22,1,.36,1)'}).onfinish = () => p.remove();
  }
}

// Big heart fanfare from center
function heartRain(){
  const container = document.getElementById('v2Confetti');
  if(!container) return;
  const hearts = ['❤️','💕','💗','💖','💓','💞','✨','💝'];
  const n = 18;
  const cx = window.innerWidth/2, cy = window.innerHeight/2;
  for(let i=0;i<n;i++){
    const e = document.createElement('div');
    e.textContent = hearts[i%hearts.length];
    e.style.cssText = `position:absolute;left:${cx}px;top:${cy}px;font-size:${22+Math.random()*22}px;pointer-events:none;filter:drop-shadow(0 2px 6px color-mix(in oklab,var(--rose) 40%,transparent))`;
    container.appendChild(e);
    const a = (Math.PI*2*i)/n + Math.random()*0.3;
    const dd = 100 + Math.random()*120;
    const dx = Math.cos(a)*dd, dy = -Math.abs(Math.sin(a))*dd - 80;
    e.animate([
      {transform:'translate(-50%,-50%) scale(.3)', opacity:0},
      {transform:`translate(calc(-50% + ${dx*0.5}px),calc(-50% + ${dy*0.4}px)) scale(1)`, opacity:1, offset:0.25},
      {transform:`translate(calc(-50% + ${dx}px),calc(-50% + ${dy}px)) rotate(${(Math.random()-.5)*120}deg) scale(.6)`, opacity:0}
    ], {duration:1500, easing:'cubic-bezier(.25,.46,.45,.94)'}).onfinish = () => e.remove();
  }
}

// Big fanfare: glow + rings + sparks + hearts
function thinkFanfare(){
  const container = document.getElementById('v2Confetti');
  if(!container) return;
  const cx = window.innerWidth/2, cy = window.innerHeight*0.42;

  // radial glow
  const glow = document.createElement('div');
  glow.className = 'ft-think-glow';
  container.appendChild(glow);
  setTimeout(()=>glow.remove(), 2500);

  // expanding rings
  for(let i=0;i<3;i++){
    const ring = document.createElement('div');
    ring.className = 'ft-think-ring' + (i?` r${i+1}`:'');
    ring.style.left = cx+'px'; ring.style.top = cy+'px';
    container.appendChild(ring);
    setTimeout(()=>ring.remove(), 2200);
  }

  // sparkly emoji burst
  const sparks = ['✨','⭐','💫','🌟','💖','💕'];
  for(let i=0;i<14;i++){
    const s = document.createElement('div');
    s.className = 'ft-think-spark';
    s.textContent = sparks[i%sparks.length];
    s.style.left = cx+'px'; s.style.top = cy+'px';
    const ang = (Math.PI*2*i)/14 + Math.random()*0.4;
    const dist = 130 + Math.random()*100;
    s.style.setProperty('--dx', Math.cos(ang)*dist+'px');
    s.style.setProperty('--dy', Math.sin(ang)*dist+'px');
    container.appendChild(s);
    setTimeout(()=>s.remove(), 2000);
  }

  // then hearts after a beat
  setTimeout(heartRain, 180);
  setTimeout(heartRain, 700);
}

// Chime (respects sound setting)
let _audioCtx = null;
function _ensureAudio(){
  try {
    if(!_audioCtx) _audioCtx = new (window.AudioContext||window.webkitAudioContext)();
    if(_audioCtx.state === 'suspended') _audioCtx.resume().catch(()=>{});
  } catch(e){}
}
// iOS/Safari won't start AudioContext until a real user gesture. Resume on every
// user interaction so any subsequent programmatic chime() is already primed.
if(typeof document !== 'undefined'){
  ['click','touchstart','pointerdown','keydown'].forEach(ev => {
    document.addEventListener(ev, _ensureAudio, { passive: true, capture: true });
  });
}
function chime(freqs){
  if(localStorage.getItem('ft-v2-sound') === '0') return;
  try{
    _ensureAudio();
    const ctx = _audioCtx;
    if(!ctx) return;
    freqs.forEach((f,i) => {
      const osc = ctx.createOscillator(), gain = ctx.createGain();
      osc.connect(gain); gain.connect(ctx.destination);
      const start = ctx.currentTime + i*0.09;
      osc.frequency.value = f; osc.type = 'sine';
      gain.gain.setValueAtTime(0.08, start);
      gain.gain.exponentialRampToValueAtTime(0.0001, start+0.25);
      osc.start(start); osc.stop(start+0.3);
    });
  }catch(e){}
}

const REACTION_EMOJIS = ['🥰','❤️','😭','😍','🤣','✨','🔥','💕','💯','🫶','👀','😂','😘','🙌','🤍','🫠','🥹','💖','😊','🤗','😌','🫂','💘','💝','😻','💞'];

// ---------- MODALS ----------
// --- Duplicate detection (local fuzzy match, no AI needed) ---
function _normalizeTitle(s){
  return (s||'').toLowerCase().replace(/[^\w\s]/g,' ').replace(/\s+/g,' ').trim();
}
function _titleSimilarity(a, b){
  a = _normalizeTitle(a); b = _normalizeTitle(b);
  if(!a || !b) return 0;
  if(a === b) return 1;
  if(a.length > 3 && b.length > 3 && (a.includes(b) || b.includes(a))) return 0.9;
  const aw = new Set(a.split(' ').filter(w => w.length > 2));
  const bw = new Set(b.split(' ').filter(w => w.length > 2));
  if(!aw.size || !bw.size) return 0;
  let common = 0;
  aw.forEach(w => { if(bw.has(w)) common++; });
  return common / (aw.size + bw.size - common);
}
function findDupeInCat(newText, items){
  if(!newText || newText.length < 3 || !items) return null;
  let best = null, bestScore = 0;
  for(const it of items){
    if(!it.text) continue;
    const s = _titleSimilarity(newText, it.text);
    if(s > bestScore){ bestScore = s; best = it; }
  }
  return bestScore >= 0.72 ? { item: best, score: bestScore } : null;
}

function AddItemModal({ open, onClose, me, currentCat }){
  const d = useData();
  const [text, setText] = useState('');
  const [cat, setCat] = useState(currentCat || '');
  const [catAuto, setCatAuto] = useState(true); // true = cat was auto-picked, ok to overwrite
  const [autoHint, setAutoHint] = useState(null); // {catKey, reason}
  const [note, setNote] = useState('');
  const [url, setUrl] = useState('');
  const [extracting, setExtracting] = useState(false);
  const inputRef = useRef();
  useEffect(() => { if(open){ setText(''); setNote(''); setUrl(''); setCatAuto(true); setAutoHint(null); setCat(currentCat || Object.keys(d||{})[0]||''); setTimeout(()=>inputRef.current?.focus(), 100); }}, [open, currentCat]);

  // Smart category auto-pick based on what the user types
  useEffect(() => {
    if(!open || !catAuto || !d || !text.trim()) { setAutoHint(null); return; }
    if(currentCat) return; // don't override explicit current context
    const t = setTimeout(() => {
      const hay = (text + ' ' + note + ' ' + url).toLowerCase();
      // keyword -> matcher pairs, highest specificity first
      const rules = [
        { keys: /\b(movie|film|cinema|imdb|trailer|screening)\b/,        match: c => /movie|film|cinema/i.test(c.title) },
        { keys: /\b(show|series|tv|season|episode|binge|netflix|hulu|disney|hbo|prime|apple tv)\b/,  match: c => /tv|show|series|watch|stream/i.test(c.title) },
        { keys: /\b(halloween|spooky|scary|horror|costume)\b/,             match: c => /halloween|spooky|horror/i.test(c.title) },
        { keys: /\b(recipe|cook|bake|dinner|lunch|breakfast|pasta|soup|salad|cake|dessert|bread|chicken|beef|fish|tofu|veg|allrecipes|nyt cooking|bon appetit|serious eats|smitten|food52)\b/, match: c => /recipe|food|cook|kitchen|eat|bake|meal|dinner/i.test(c.title) },
        { keys: /\b(restaurant|cafe|coffee|bar|brunch|dinner out|eat out|reservation|resy|opentable|yelp)\b/, match: c => /restaurant|eat|dine|food|date|brunch|cafe|bar/i.test(c.title) },
        { keys: /\b(game|gaming|playstation|xbox|nintendo|switch|ps5|steam|metacritic)\b/, match: c => /game|play/i.test(c.title) },
        { keys: /\b(book|novel|read|goodreads|author|chapter)\b/,          match: c => /book|read|reading/i.test(c.title) },
        { keys: /\b(album|artist|song|spotify|apple music|playlist|listen)\b/, match: c => /music|listen|album|song|playlist/i.test(c.title) },
        { keys: /\b(podcast|episode|pod)\b/,                                match: c => /podcast|listen/i.test(c.title) },
        { keys: /\b(hike|trail|camp|park|beach|mountain|outdoor|nature)\b/, match: c => /outdoor|hike|trail|adventure|nature/i.test(c.title) },
        { keys: /\b(trip|travel|flight|hotel|airbnb|vacation|weekend)\b/,  match: c => /travel|trip|vacation|adventure/i.test(c.title) },
        { keys: /\b(date night|anniversary|romantic|wine|candle)\b/,       match: c => /date|romance|love|us|together|nudge/i.test(c.title) },
        { keys: /nudgetext\.com/,                                           match: c => /nudge/i.test(c.title) || /date|eat|do/i.test(c.title) },
      ];
      for(const r of rules){
        if(!r.keys.test(hay)) continue;
        const hit = Object.entries(d).find(([k,c]) => r.match(c));
        if(hit && hit[0] !== cat){
          setAutoHint({ catKey: hit[0], title: d[hit[0]].title });
          return;
        }
      }
      setAutoHint(null);
    }, 500);
    return () => clearTimeout(t);
  }, [text, note, url, open, catAuto, d, currentCat, cat]);

  const acceptHint = () => {
    if(!autoHint) return;
    setCat(autoHint.catKey);
    setAutoHint(null);
  };

  const [lookingUp, setLookingUp] = [extracting, setExtracting]; // alias — share spinner state
  const lookupFromText = async () => {
    const q = text.trim();
    if(!q) return;
    setExtracting(true);
    try{
      const catTitle = d?.[cat]?.title || '';
      const r = await fetch('https://nudge-proxy.allurhopesndreams.workers.dev/lookup', {
        method:'POST',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({ query: q, hint: catTitle })
      });
      if(!r.ok) throw new Error('HTTP '+r.status);
      const data = await r.json();
      if(!data || data.error) throw new Error(data?.error || 'no data');
      // Populate fields
      if(data.name && data.name !== text) setText(data.name);
      if(data.website && !url) setUrl(data.website);
      if(data.description){
        setNote(n => n ? n : data.description);
      }
      // Stash place info so submit picks it up as nudge-style data + tags + where
      if(data.description || data.neighborhood || data.address || data.maps_url || data.tags){
        const locationText = data.neighborhood
          ? data.neighborhood + (data.city ? ', ' + data.city : '')
          : (data.address || data.city || '');
        window._pendingNudge = {
          name: data.name,
          description: data.description,
          neighborhood: data.neighborhood,
          city: data.city,
          address: data.address,
          mapsUrl: data.maps_url,
          autoTags: data.tags,
          whereText: locationText,
        };
      }
    }catch(e){
      console.warn('lookup', e);
      alert("Couldn't look that up — " + (e.message || e));
    }
    setExtracting(false);
  };

  const extractFromUrl = async () => {
    if(!url.trim()) return;
    setExtracting(true);
    const u = url.trim();
    try {
      // 1) Try the nudge-proxy page fetcher (extracts title, description, photo, etc from the actual page)
      const resp = await fetch(`https://nudge-proxy.allurhopesndreams.workers.dev/?url=${encodeURIComponent(u)}`, {signal: AbortSignal.timeout(12000)});
      if(resp.ok){
        const data = await resp.json();
        if(data && !data.error){
          const isNudge = /nudgetext\.com/i.test(u);
          if(isNudge){
            // rich nudge — attach to item.nudge via closure escape: stash on element for submit
            setUrl(u);
            // we only populate text/note here; richer fields get saved when submit merges `pendingNudge`
            if(data.name && !text) setText(data.name);
            const desc = data.description || data.nudgeText || '';
            if(!note){
              const parts = [desc, data.neighborhood && ('📍 '+data.neighborhood), data.rating && ('⭐ '+data.rating), u].filter(Boolean);
              setNote(parts.join('\n'));
            }
            // stash for submit to pick up
            window._pendingNudge = data;
          } else {
            if(data.title && !text) setText(data.title);
            if(!note){
              const parts = [data.description || data.summary || '', u].filter(Boolean);
              setNote(parts.join('\n\n'));
            }
          }
          setExtracting(false);
          return;
        }
      }

      // 2) Recipe URLs — try extract-recipe endpoint
      if(/recipe|food|cook|eat|kitchen|bake/i.test(u)){
        try{
          const rr = await fetch('https://nudge-proxy.allurhopesndreams.workers.dev/extract-recipe', {
            method:'POST', headers:{'Content-Type':'application/json'},
            body: JSON.stringify({url: u})
          });
          if(rr.ok){
            const rd = await rr.json();
            if(rd?.title && !text) setText(rd.title);
            if(!note){
              const parts = [rd.description||'', rd.summary||'', u].filter(Boolean);
              setNote(parts.join('\n\n'));
            }
            setExtracting(false);
            return;
          }
        }catch(e){}
      }

      // 3) Fallback: AI-from-URL (path/domain inference only)
      const r = await fetch('https://nudge-proxy.allurhopesndreams.workers.dev/ai', {
        method:'POST',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({
          model:'claude-haiku-4-5-20251001',
          max_tokens:400,
          system:"Extract info from the URL provided. Return ONLY a compact JSON object with these keys (all optional): title (short dish/item name, max 60 chars), note (key details, max 180 chars like prep time, cuisine, source). No markdown, no explanation.",
          messages:[{role:'user', content:`URL: ${u}\n\nBased on the URL path/domain, extract: title and a short note.`}]
        })
      });
      const data = await r.json();
      let txt = data?.content?.[0]?.text || '';
      txt = txt.replace(/```json\s*|```\s*/g,'').trim();
      const m = txt.match(/\{[\s\S]*\}/);
      if(m){
        const obj = JSON.parse(m[0]);
        if(obj.title && !text) setText(obj.title);
        if(obj.note && !note) setNote(obj.note + '\n\n' + u);
        else if(!note) setNote(u);
      }
    } catch(e){
      console.warn('url extract', e);
      setNote(n => n ? n : u);
    }
    setExtracting(false);
  };

  const submit = () => {
    if(!text.trim() || !cat) return;
    const extras = {};
    if(note) extras.note = note;
    if(url) extras.link = url;
    // pick up extracted nudge data if present
    if(window._pendingNudge){
      const n = window._pendingNudge;
      const trimmed = {};
      if(n.name) trimmed.name = n.name;
      if(n.neighborhood) trimmed.neighborhood = n.neighborhood;
      if(n.rating) trimmed.rating = n.rating;
      if(n.reviewCount) trimmed.reviewCount = n.reviewCount;
      if(n.description) trimmed.description = n.description;
      if(n.address && n.address.length < 200) trimmed.address = n.address;
      if(n.photo) trimmed.photo = n.photo;
      if(n.nearby) trimmed.nearby = n.nearby;
      if(n.hours) trimmed.hours = n.hours;
      if(n.nudgeText) trimmed.nudgeText = n.nudgeText;
      if(Object.keys(trimmed).length) extras.nudge = trimmed;
      if(n.autoTags && n.autoTags.length) extras.tags = n.autoTags;
      if(n.whereText) extras.where = n.whereText;
      if(n.mapsUrl && !extras.link) extras.link = n.mapsUrl;
      delete window._pendingNudge;
    }
    FT.addItem(cat, text.trim(), extras, me);
    onClose();
  };
  return (
    <Modal open={open} onClose={onClose} title="Add to the list">
      <label className="ft-label">What?</label>
      <div style={{display:'flex',gap:6}}>
        <input ref={inputRef} className="ft-input" placeholder="e.g. Ramen at Ippudo" value={text} onChange={e=>setText(e.target.value)} onKeyDown={e=>e.key==='Enter'&&submit()} style={{flex:1}} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
        <button type="button" onClick={lookupFromText} disabled={!text.trim()||extracting}
          style={{padding:'0 12px',borderRadius:10,background:'linear-gradient(135deg,color-mix(in oklab,var(--rose) 18%,transparent),color-mix(in oklab,var(--lavender) 18%,transparent))',border:'1px solid var(--border-accent)',color:'var(--text)',fontSize:12,cursor:'pointer',whiteSpace:'nowrap',opacity:(!text.trim()||extracting)?.4:1,fontFamily:'inherit'}}
          title="Look up more info about this">
          {extracting?'…':'✨ look it up'}
        </button>
      </div>
      <label className="ft-label" style={{marginTop:10}}>Where?</label>
      <select className="ft-select" value={cat} onChange={e=>{setCat(e.target.value); setCatAuto(false); setAutoHint(null);}}>
        {d && Object.keys(d).map(k => <option key={k} value={k}>{d[k].emoji} {d[k].title.replace(/^[^\w]+\s*/,'')}</option>)}
      </select>
      {autoHint && autoHint.catKey !== cat && (
        <button
          type="button"
          onClick={acceptHint}
          style={{marginTop:6,padding:'7px 11px',borderRadius:12,background:'color-mix(in oklab,var(--lavender) 12%,transparent)',border:'1px dashed var(--border-accent)',color:'var(--text2)',fontSize:12.5,cursor:'pointer',fontFamily:'inherit',textAlign:'left',lineHeight:1.35,width:'100%'}}
          title="Tap to use this list"
        >
          ✨ suggested: <b style={{color:'var(--text)'}}>{autoHint.title.replace(/^[^\w]+\s*/,'')}</b> <span style={{color:'var(--text3)'}}>· tap to switch</span>
        </button>
      )}
      <label className="ft-label" style={{marginTop:10}}>Link or recipe URL (optional)</label>
      <div style={{display:'flex',gap:6}}>
        <input className="ft-input" placeholder="https://…" value={url} onChange={e=>setUrl(e.target.value)} style={{flex:1}} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
        <button onClick={extractFromUrl} disabled={!url.trim()||extracting}
          style={{padding:'0 12px',borderRadius:10,background:'var(--card)',border:'1px solid var(--border2)',color:'var(--text2)',fontSize:12,cursor:'pointer',whiteSpace:'nowrap',opacity:(!url.trim()||extracting)?.4:1}}>
          {extracting?'…':'✨ fetch'}
        </button>
      </div>
      <label className="ft-label" style={{marginTop:10}}>Note (optional)</label>
      <textarea className="ft-textarea" placeholder="why this, memories, links..." value={note} onChange={e=>setNote(e.target.value)} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
      {(() => {
        if(!d || !cat || !d[cat] || !text.trim()) return null;
        const dupe = findDupeInCat(text.trim(), d[cat].items);
        if(!dupe) return null;
        return (
          <div style={{marginTop:10,padding:'9px 12px',borderRadius:11,background:'rgba(242,185,136,.10)',border:'1px solid rgba(242,185,136,.35)',color:'var(--text)',fontSize:12.5,lineHeight:1.4,display:'flex',alignItems:'flex-start',gap:8}}>
            <div style={{flexShrink:0,fontSize:14}}>⚠️</div>
            <div style={{flex:1}}>
              <div style={{fontWeight:600,marginBottom:2}}>Possible duplicate</div>
              <div style={{color:'var(--text2)'}}>You already have <b style={{color:'var(--text)'}}>"{dupe.item.text}"</b> on this list. Same thing? You can still add below.</div>
            </div>
          </div>
        );
      })()}
      <Btn variant="solid" size="lg" style={{width:'100%',marginTop:14}} onClick={submit}>Add it ♡</Btn>
    </Modal>
  );
}

function CommentModal({ open, onClose, catKey, itemId, me }){
  const [text, setText] = useState('');
  const inputRef = useRef();
  useEffect(() => { if(open){ setText(''); setTimeout(()=>inputRef.current?.focus(),100);}}, [open]);
  const submit = () => {
    if(!text.trim()) return;
    FT.addComment(catKey, itemId, text.trim(), me);
    onClose();
  };
  return (
    <Modal open={open} onClose={onClose} title="Add a comment">
      <input ref={inputRef} className="ft-input" placeholder="say something..." value={text} onChange={e=>setText(e.target.value)} onKeyDown={e=>e.key==='Enter'&&submit()} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
      <Btn variant="solid" size="lg" style={{width:'100%',marginTop:12}} onClick={submit}>Post ♡</Btn>
    </Modal>
  );
}

function ReactPickerModal({ open, onClose, catKey, itemId, me }){
  const d = useData();
  const [showAll, setShowAll] = useState(false);
  const item = catKey && itemId ? d?.[catKey]?.items.find(i=>i.id===itemId) : null;
  useEffect(() => { if(!open) setShowAll(false); }, [open]);
  if(!open || !item) return null;
  const reactions = item.reactions || {};
  const toggle = (emoji) => { FT.toggleItemReaction(catKey, itemId, emoji, me); onClose(); };
  const emojisToShow = showAll ? REACTION_EMOJIS : REACTION_EMOJIS.slice(0,12);
  return (
    <Modal open={open} onClose={onClose} title="React">
      <div style={{fontSize:12,color:'var(--text3)',marginBottom:12,textAlign:'center'}}>to "{item.text?.slice(0,40)}{(item.text||'').length>40?'…':''}"</div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(6,1fr)',gap:6}}>
        {emojisToShow.map(emoji => {
          const users = reactions[emoji] || [];
          const mine = users.includes(me);
          return (
            <button key={emoji} onClick={()=>toggle(emoji)} style={{fontSize:26,padding:'10px 0',borderRadius:12,background:mine?'color-mix(in oklab,var(--rose) 22%,transparent)':'var(--card)',border:`1px solid ${mine?'var(--border-accent)':'var(--border)'}`,cursor:'pointer',fontFamily:'inherit',position:'relative'}}>
              {emoji}
              {users.length > 0 && <span style={{position:'absolute',bottom:2,right:6,fontSize:9,color:'var(--text3)'}}>{users.length}</span>}
            </button>
          );
        })}
      </div>
      {!showAll && REACTION_EMOJIS.length > 12 && (
        <button onClick={()=>setShowAll(true)} style={{width:'100%',marginTop:10,padding:'8px',background:'transparent',border:'1px dashed var(--border)',borderRadius:10,color:'var(--text3)',fontSize:12,cursor:'pointer',fontFamily:'inherit'}}>
          see more emojis ({REACTION_EMOJIS.length - 12} more) ▾
        </button>
      )}
      {showAll && (
        <button onClick={()=>setShowAll(false)} style={{width:'100%',marginTop:10,padding:'8px',background:'transparent',border:'1px dashed var(--border)',borderRadius:10,color:'var(--text3)',fontSize:12,cursor:'pointer',fontFamily:'inherit'}}>
          show less ▴
        </button>
      )}
    </Modal>
  );
}

function ThinkingSheet({ open, onClose, me }){
  const [sent, setSent] = useState(false);
  const [phrase, setPhrase] = useState('');
  const PHRASES = [
    "they'll feel it",
    "beamed to them ✨",
    "sent with love",
    "on its way ♡",
    "hope they smile",
    "a tiny love note",
    "telepathy delivered",
    "heart-mail sent",
    "they'll know",
    "warmth on the way",
    "a little spark sent",
    "delivered to their heart",
    "flown over ✈",
    "safe in their pocket",
    "they'll feel the flutter",
  ];
  const go = () => {
    chime([659,784,988,1175, 1319]);
    thinkFanfare();
    FT.sendThinking(me);
    // pick a different phrase than last time
    const last = sessionStorage.getItem('ft-thinking-last');
    let pool = PHRASES.filter(p => p !== last);
    const pick = pool[Math.floor(Math.random()*pool.length)];
    sessionStorage.setItem('ft-thinking-last', pick);
    setPhrase(pick);
    setSent(true);
    // Confirmation push on sender's phone (silent, no vibrate)
    try{
      if('serviceWorker' in navigator && Notification.permission === 'granted'){
        navigator.serviceWorker.ready.then(reg => {
          const to = (FT.getPartner(me) || '');
          reg.showNotification(`Thought sent to ${to} ♡`, {
            body: pick,
            icon: '/icon-192.png',
            badge: '/icon-192.png',
            tag: 'thinking-sent',
            silent: true,
            requireInteraction: false,
          });
          // auto-dismiss after 4s
          setTimeout(() => reg.getNotifications({tag:'thinking-sent'}).then(ns => ns.forEach(n => n.close())), 4000);
        });
      }
    }catch(_){}
    setTimeout(()=>{setSent(false); onClose();}, 2200);
  };
  return (
    <Sheet open={open} onClose={onClose} title={sent ? 'sent with love ♡' : 'Thinking of '+((FT.getPartner(me) || ''))+'?'}>
      {sent ? (
        <div style={{textAlign:'center',padding:'30px 0'}}>
          <div style={{fontSize:52}}>💌</div>
          <div style={{fontFamily:'Caveat,cursive',fontSize:22,marginTop:10}}>{phrase}</div>
        </div>
      ) : (
        <>
          <p style={{color:'var(--text2)',fontSize:14,marginBottom:16,textAlign:'center'}}>Send them a little nudge that you're thinking about them right now ♡</p>
          <Btn variant="solid" size="lg" style={{width:'100%'}} onClick={go}>💭 Send the thought</Btn>
        </>
      )}
    </Sheet>
  );
}

function NoteSheet({ open, onClose, me, prefill='' }){
  const [msg, setMsg] = useState('');
  const [sent, setSent] = useState(false);
  const taRef = useRef();
  useEffect(() => {
    if(open && !sent){
      setMsg(prefill || '');
      // Wait for sheet animation to settle before focusing
      const t = setTimeout(() => {
        try {
          taRef.current?.focus();
          // Put cursor at the end if there's prefill text
          if(prefill && taRef.current){
            const len = taRef.current.value.length;
            taRef.current.setSelectionRange(len, len);
          }
        } catch(e){}
      }, 340);
      return () => clearTimeout(t);
    }
  }, [open, sent, prefill]);
  const submit = () => {
    if(!msg.trim()) return;
    const forName = FT.getPartner(me);
    if(!forName) return;
    const db = window.FT.db.collection('lists').doc('notes_v2');
    db.get().then(doc => {
      const log = doc.exists && doc.data().log ? doc.data().log : [];
      log.unshift({from:me, for:forName, text:msg.trim(), ts:Date.now(), reactions:{}});
      db.set({log: log.slice(0,50)});
    });
    chime([523,659,784]);
    heartRain();
    setSent(true);
    setTimeout(() => { setSent(false); setMsg(''); onClose(); }, 1400);
  };
  return (
    <Sheet open={open} onClose={onClose} title={sent?'note tucked away ♡':'Leave a note'}>
      {sent ? (
        <div style={{textAlign:'center',padding:'30px 0'}}>
          <div style={{fontSize:52}}>💌</div>
          <div style={{fontFamily:'Caveat,cursive',fontSize:22,marginTop:10}}>saved for {(FT.getPartner(me) || '')}</div>
        </div>
      ) : (
        <>
          <textarea ref={taRef} className="ft-textarea" placeholder="write something lovely..." value={msg} onChange={e=>setMsg(e.target.value)} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
          <Btn variant="solid" size="lg" style={{width:'100%',marginTop:10}} onClick={submit}>Send ♡</Btn>
        </>
      )}
    </Sheet>
  );
}

function DateNightSheet({ open, onClose }){
  const d = useData();
  const [pick, setPick] = useState(null);
  const [spinning, setSpinning] = useState(false);
  const [theme, setTheme] = useState('anything');
  const [showCats, setShowCats] = useState(false);
  const [cats, setCats] = useState(null); // null = all

  const THEMES = {
    anything:      { label:'Anything', cats:null, match:null, icon:'✨' },
    cozy:          { label:'Cozy night in', cats:['movies','tv','recipes','food'], match:/cozy|warm|home|chill|soup|blanket|hot/i, icon:'🕯️' },
    adventurous:   { label:'Adventurous', cats:['activities','travel','food'], match:/adventure|new|explore|try|hike|walk/i, icon:'🌄' },
    'date-out':    { label:'Out on the town', cats:['activities','food','travel'], match:/out|bar|restaurant|walk|dinner/i, icon:'🌆' },
    quick:         { label:'Quick & easy', cats:null, match:/easy|quick|15|20|30 min/i, icon:'⚡' },
    romantic:      { label:'Romantic', cats:['movies','tv','food','recipes'], match:/romantic|love|wine|intimate|slow|candle/i, icon:'💕' },
  };

  const roll = () => {
    if(!d) return;
    setSpinning(true);
    let count = 0;
    const t = THEMES[theme];
    const pool = [];
    Object.entries(d).forEach(([k,c]) => {
      // user-picked cats override theme cats
      if(cats && cats.length){
        if(!cats.includes(k)) return;
      } else if(t.cats && !t.cats.includes(k)){
        return;
      }
      c.items.filter(i => !i.done).forEach(i => {
        if(t.match){
          const hay = (i.text+' '+(i.note||'')+' '+(i.mood||'')+' '+(i.tags||[]).join(' ')).toLowerCase();
          if(!t.match.test(hay)) return;
        }
        pool.push({cat:c.title, text:i.text});
      });
    });
    if(!pool.length){ setPick({cat:'', text:`Nothing matches "${t.label}" yet`}); setSpinning(false); return; }
    const timer = setInterval(() => {
      setPick(pool[Math.floor(Math.random()*pool.length)]);
      count++;
      if(count > 16){ clearInterval(timer); setSpinning(false); }
    }, 70);
  };

  useEffect(() => { if(open){ setPick(null); setTheme('anything'); setCats(null); setTimeout(roll, 300); }}, [open]);
  // Only re-roll on theme change, not on cats (so user can multi-select first)
  useEffect(() => { if(open){ roll(); }}, [theme]);

  const toggleCat = (k) => {
    setCats(prev => {
      const cur = prev || [];
      if(cur.includes(k)) return cur.filter(x => x!==k).length ? cur.filter(x => x!==k) : null;
      return [...cur, k];
    });
  };

  return (
    <Sheet open={open} onClose={onClose} title="🌙 Date Night">
      <div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:10,justifyContent:'center'}}>
        {Object.entries(THEMES).map(([k,t]) => (
          <button key={k} onClick={()=>setTheme(k)}
            style={{padding:'6px 10px',borderRadius:99,fontSize:11.5,
              background:theme===k?'color-mix(in oklab,var(--rose) 18%,transparent)':'var(--card)',
              border:`1px solid ${theme===k?'var(--border-accent)':'var(--border)'}`,
              color:theme===k?'var(--text)':'var(--text2)'}}>
            {t.icon} {t.label}
          </button>
        ))}
      </div>
      <div style={{textAlign:'center',marginBottom:10}}>
        <button onClick={()=>setShowCats(s=>!s)} style={{fontSize:11,color:'var(--text3)',background:'transparent',border:'none',cursor:'pointer',textDecoration:'underline'}}>
          {showCats?'▾':'▸'} filter by category {cats?.length?`(${cats.length})`:''}
        </button>
      </div>
      {showCats && d && (
        <div style={{display:'flex',gap:5,flexWrap:'wrap',marginBottom:14,justifyContent:'center'}}>
          {cats && cats.length > 0 ? (
            <button onClick={()=>setCats(null)} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>✓ all</button>
          ) : (
            <button onClick={()=>setCats([])} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>× none</button>
          )}
          {Object.entries(d).map(([k,c]) => {
            const active = !cats || cats.length===0 || cats.includes(k);
            const muted = cats && cats.length>0 && !cats.includes(k);
            return (
              <button key={k} onClick={()=>toggleCat(k)}
                style={{padding:'4px 9px',borderRadius:99,fontSize:11,cursor:'pointer',
                  background:muted?'transparent':'color-mix(in oklab,var(--rose) 22%,transparent)',
                  border:`1px solid ${muted?'var(--border)':'var(--border-accent)'}`,
                  color:muted?'var(--text3)':'var(--text)',
                  opacity:muted?.55:1,fontFamily:'inherit'}}>
                {c.emoji} {c.title?.replace(/^[^\w]+\s*/,'')}
              </button>
            );
          })}
        </div>
      )}
      <div style={{textAlign:'center',padding:'8px 0 18px'}}>
        <div style={{fontFamily:'Caveat,cursive',fontSize:18,color:'var(--rose)',marginBottom:8}}>tonight, the universe says…</div>
        <div className={spinning?'ft-spin':''} style={{fontFamily:'Playfair Display,serif',fontSize:22,fontWeight:700,letterSpacing:'-.015em',lineHeight:1.2,padding:'20px 16px',minHeight:90,display:'flex',alignItems:'center',justifyContent:'center'}}>
          {pick ? pick.text : '…'}
        </div>
        {pick?.cat && !spinning && <div style={{fontSize:12,color:'var(--text3)',marginBottom:16}}>from {pick.cat}</div>}
        <Btn variant="solid" size="lg" onClick={roll} disabled={spinning}>🎲 Roll again</Btn>
      </div>
    </Sheet>
  );
}

// ---------- THIS OR THAT ----------
function ThisOrThatSheet({ open, onClose }){
  const d = useData();
  const [pair, setPair] = useState(null);
  const [winner, setWinner] = useState(null);
  const [cats, setCats] = useState(() => {
    try{ const saved = localStorage.getItem('ft-tot-cats'); return saved ? JSON.parse(saved) : null; }catch(e){ return null; }
  });
  const [showCats, setShowCats] = useState(false);

  useEffect(() => {
    try{ if(cats) localStorage.setItem('ft-tot-cats', JSON.stringify(cats)); else localStorage.removeItem('ft-tot-cats'); }catch(e){}
  }, [cats]);

  const pickPair = () => {
    if(!d) return;
    const season = window.FT?.getSeasonContext?.()?.season;
    const pool = [];
    Object.entries(d).forEach(([k,c]) => {
      if(cats && cats.length && !cats.includes(k)) return;
      c.items.filter(i=>!i.done).forEach(i => {
        // Skip strongly off-season items (weight <= 0.3 per seasonFit)
        const fit = window.__ftSeasonFit ? window.__ftSeasonFit(i, season) : 1;
        if(fit <= 0.3) return;
        pool.push({cat:k, catTitle:c.title, emoji:c.emoji, it:i});
      });
    });
    if(pool.length < 2){ setPair(null); return; }
    const a = pool[Math.floor(Math.random()*pool.length)];
    let b = pool[Math.floor(Math.random()*pool.length)];
    let tries = 0;
    while((b.it.id===a.it.id || (a.cat!==b.cat && Math.random()<0.3)) && tries<6){
      b = pool[Math.floor(Math.random()*pool.length)];
      tries++;
    }
    setPair([a, b]);
    setWinner(null);
  };

  useEffect(() => { if(open){ pickPair(); }}, [open]);

  const toggleCat = (k) => {
    setCats(prev => {
      const cur = prev || [];
      if(cur.includes(k)){ const next = cur.filter(x => x !== k); return next.length ? next : null; }
      return [...cur, k];
    });
  };

  const choose = (idx) => {
    setWinner(idx);
    chime([659, 784, 988]);
    setTimeout(()=>{ pickPair(); }, 900);
  };

  if(!d) return null;
  return (
    <Sheet open={open} onClose={onClose} title="⚡ This or That?">
      <div style={{fontSize:12,color:'var(--text3)',textAlign:'center',marginBottom:10}}>tap the one you're in the mood for</div>
      <div style={{marginBottom:12}}>
        <div style={{textAlign:'center',marginBottom:showCats?8:0}}>
          <button onClick={()=>setShowCats(s=>!s)} style={{fontSize:11,color:'var(--text3)',background:'transparent',border:'none',cursor:'pointer',textDecoration:'underline',fontFamily:'inherit'}}>
            {showCats?'▾':'▸'} pick from {cats?.length ? `${cats.length} list${cats.length>1?'s':''}` : 'all lists'}
          </button>
        </div>
        {showCats && (
          <div style={{display:'flex',gap:5,flexWrap:'wrap',justifyContent:'center'}}>
            {cats && cats.length > 0 ? (
              <button onClick={()=>setCats(null)} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>✓ all</button>
            ) : (
              <button onClick={()=>setCats([])} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>× none</button>
            )}
            {Object.entries(d).map(([k,c]) => {
              const active = !cats || cats.length===0 || cats.includes(k);
              const muted = cats && cats.length>0 && !cats.includes(k);
              return (
                <button key={k} onClick={()=>toggleCat(k)}
                  style={{padding:'4px 9px',borderRadius:99,fontSize:11,cursor:'pointer',
                    background:muted?'transparent':'color-mix(in oklab,var(--rose) 22%,transparent)',
                    border:`1px solid ${muted?'var(--border)':'var(--border-accent)'}`,
                    color:muted?'var(--text3)':'var(--text)',
                    opacity:muted?.55:1,fontFamily:'inherit'}}>
                  {c.emoji} {c.title?.replace(/^[^\w]+\s*/,'')}
                </button>
              );
            })}
          </div>
        )}
      </div>
      {pair ? (
        <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10,position:'relative'}}>
          {pair.map((p,i) => {
            const poster = p.it.tmdb_poster || p.it.rawg_poster || p.it.og_image || p.it.poster || p.it.photo || p.it.nudge?.photo;
            const backdrop = p.it.tmdb_backdrop || p.it.rawg_backdrop || p.it.nudge?.photo;
            const isWinner = winner===i;
            const isLoser = winner!=null && winner!==i;
            return (
              <button key={p.it.id} onClick={()=>winner==null && choose(i)}
                className="ft-tot"
                style={{
                  opacity:isLoser?.3:1,
                  transform:isWinner?'scale(1.05)':'scale(1)',
                  boxShadow:isWinner?'0 12px 32px color-mix(in oklab,var(--rose) 40%,transparent)':'',
                  borderColor:isWinner?'var(--border-accent)':'var(--border2)',
                }}
              >
                {backdrop && <div className="ft-tot-bd" style={{backgroundImage:`url(${backdrop})`}}/>}
                <div className="ft-tot-inner">
                  {poster ? <img src={poster} className="ft-tot-poster" alt=""/> : <div className="ft-tot-glyph">{p.emoji}</div>}
                  <div className="ft-tot-cat">{p.catTitle?.replace(/^[^\w]+\s*/,'')}</div>
                  <div className="ft-tot-text">{p.it.text}</div>
                  {p.it.tmdb_rating && <div className="ft-tot-rating">★ {p.it.tmdb_rating}</div>}
                </div>
              </button>
            );
          })}
          <div style={{position:'absolute',top:'50%',left:'50%',transform:'translate(-50%,-50%)',fontFamily:'Playfair Display,serif',fontStyle:'italic',fontSize:20,color:'var(--text3)',background:'var(--bg2)',padding:'4px 12px',borderRadius:99,border:'1px solid var(--border2)',pointerEvents:'none'}}>or</div>
        </div>
      ) : (
        <div style={{textAlign:'center',padding:30,color:'var(--text3)',fontFamily:'Caveat,cursive',fontSize:17}}>need at least 2 unchecked items ♡</div>
      )}
      <Btn variant="ghost" size="md" style={{width:'100%',marginTop:14}} onClick={pickPair}>↻ new matchup</Btn>
    </Sheet>
  );
}

// ---------- SURPRISE ME ----------
function SurpriseSheet({ open, onClose, onNavigate }){
  const d = useData();
  const [picking, setPicking] = useState(true);
  const [pick, setPick] = useState(null);
  // null = all categories; array = selected subset
  const [cats, setCats] = useState(() => {
    try{ const saved = localStorage.getItem('ft-surprise-cats'); return saved ? JSON.parse(saved) : null; }catch(e){ return null; }
  });
  const [showCats, setShowCats] = useState(false);

  useEffect(() => {
    try{ if(cats) localStorage.setItem('ft-surprise-cats', JSON.stringify(cats)); else localStorage.removeItem('ft-surprise-cats'); }catch(e){}
  }, [cats]);

  // Prune stale keys from cats when data loads (lists may have been renamed/deleted)
  useEffect(() => {
    if(!d || !cats) return;
    const valid = cats.filter(k => d[k]);
    if(valid.length !== cats.length){
      setCats(valid.length ? valid : null);
    }
  }, [d]);

  const roll = () => {
    if(!d) return;
    const season = window.FT?.getSeasonContext?.()?.season;
    const pool = [];
    Object.entries(d).forEach(([k,c]) => {
      if(cats && cats.length && !cats.includes(k)) return;
      c.items.filter(i=>!i.done).forEach(i => {
        const fit = window.__ftSeasonFit ? window.__ftSeasonFit(i, season) : 1;
        if(fit <= 0.3) return; // skip strongly off-season items
        pool.push({cat:k, catTitle:c.title, emoji:c.emoji, it:i});
      });
    });
    if(!pool.length){ setPicking(false); setPick(null); return; }
    setPicking(true);
    let count = 0;
    const timer = setInterval(() => {
      setPick(pool[Math.floor(Math.random()*pool.length)]);
      count++;
      if(count > 20){
        clearInterval(timer);
        const final = pool[Math.floor(Math.random()*pool.length)];
        setPick(final);
        setPicking(false);
        chime([523, 659, 784, 988]);
      }
    }, 80);
  };

  useEffect(() => { if(open && d){ roll(); }}, [open, d]);
  // Don't auto-reroll on cats change — let user multi-select first, then they hit "Again"

  const toggleCat = (k) => {
    setCats(prev => {
      const cur = prev || [];
      if(cur.includes(k)){
        const next = cur.filter(x => x !== k);
        return next.length ? next : null; // null = all
      }
      return [...cur, k];
    });
  };

  const CatFilter = () => !d ? null : (
    <div style={{marginBottom:14}}>
      <div style={{textAlign:'center',marginBottom:showCats?8:0}}>
        <button onClick={()=>setShowCats(s=>!s)} style={{fontSize:11,color:'var(--text3)',background:'transparent',border:'none',cursor:'pointer',textDecoration:'underline',fontFamily:'inherit'}}>
          {showCats?'▾':'▸'} roll from {cats?.length ? `${cats.length} list${cats.length>1?'s':''}` : 'all lists'}
        </button>
      </div>
      {showCats && (
        <div style={{display:'flex',gap:5,flexWrap:'wrap',justifyContent:'center'}}>
          {cats && cats.length > 0 ? (
            <button onClick={()=>setCats(null)} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>✓ all</button>
          ) : (
            <button onClick={()=>setCats([])} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>× none</button>
          )}
          {Object.entries(d).map(([k,c]) => {
            const active = !cats || cats.length===0 || cats.includes(k);
            const muted = cats && cats.length>0 && !cats.includes(k);
            return (
              <button key={k} onClick={()=>toggleCat(k)}
                style={{padding:'4px 9px',borderRadius:99,fontSize:11,cursor:'pointer',
                  background:muted?'transparent':'color-mix(in oklab,var(--rose) 18%,transparent)',
                  border:`1px solid ${muted?'var(--border)':'var(--border-accent)'}`,
                  color:muted?'var(--text3)':'var(--text)',
                  opacity:muted?.55:1,fontFamily:'inherit'}}>
                {c.emoji} {c.title?.replace(/^[^\w]+\s*/,'')}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );

  if(!pick) return (
    <Sheet open={open} onClose={onClose} title="🎁 Surprise">
      <CatFilter/>
      <div style={{textAlign:'center',padding:40,color:'var(--text3)',fontFamily:'Caveat,cursive',fontSize:17}}>nothing to pick from yet</div>
    </Sheet>
  );

  const poster = pick.it.tmdb_poster || pick.it.rawg_poster || pick.it.og_image || pick.it.poster || pick.it.photo || pick.it.nudge?.photo;
  const backdrop = pick.it.tmdb_backdrop || pick.it.rawg_backdrop || pick.it.nudge?.photo;
  return (
    <Sheet open={open} onClose={onClose} title={picking?'picking…':'🎁 The universe picked:'}>
      <CatFilter/>
      <div className="ft-surprise" style={{opacity:picking?.7:1}}>
        {backdrop && !picking && <div className="ft-surprise-bd" style={{backgroundImage:`url(${backdrop})`}}/>}
        <div className="ft-surprise-inner">
          {poster ? <img src={poster} className="ft-surprise-poster" alt=""/> : <div className="ft-surprise-glyph">{pick.emoji||'✨'}</div>}
          <div className="ft-surprise-cat">from {pick.catTitle?.replace(/^[^\w]+\s*/,'')}</div>
          <div className={`ft-surprise-title ${picking?'blur':''}`}>{pick.it.text}</div>
          {pick.it.tmdb_overview && !picking && <div className="ft-surprise-ov">{pick.it.tmdb_overview}</div>}
          {pick.it.tmdb_rating && !picking && <div className="ft-surprise-rating">★ {pick.it.tmdb_rating}</div>}
        </div>
      </div>
      <div style={{display:'flex',gap:8,marginTop:14}}>
        <Btn variant="ghost" size="md" style={{flex:1}} onClick={roll} disabled={picking}>↻ Again</Btn>
        <Btn variant="solid" size="md" style={{flex:2}} onClick={()=>{onNavigate?.('list', pick.cat, pick.it.id); onClose();}} disabled={picking}>Let's do it ♡</Btn>
      </div>
    </Sheet>
  );
}

function buildListContext(d){
  if(!d) return '';
  let context = `Here are ${(FT.partners||['them']).join(' and ')}'s Fun-Ture lists:\n\n`;
  Object.keys(d).forEach(k => {
    const cat = d[k];
    if(!cat || !cat.items) return;
    const incomplete = cat.items.filter(i => !i.done);
    const completed = cat.items.filter(i => i.done);
    context += (cat.emoji||'')+' '+cat.title+' ('+incomplete.length+' to do, '+completed.length+' done):\n';
    const pack = (list, label) => {
      if(!list.length) return;
      context += label+':\n';
      list.forEach(i => {
        let info = i.text;
        const extras = [];
        if(i.extRating) extras.push('Rating: '+i.extRating+'/10');
        if(i.ratings) Object.keys(i.ratings).forEach(rk => extras.push(rk+': '+i.ratings[rk]+'★'));
        if(i.tags?.length) extras.push('Tags: '+i.tags.join(', '));
        if(i.moods?.length) extras.push('Moods: '+i.moods.join(', '));
        if(i.mood) extras.push('Mood: '+i.mood);
        if(i.where) extras.push('Where: '+i.where);
        if(i.status) extras.push('Status: '+i.status);
        if(extras.length) info += ' ('+extras.join(', ')+')';
        context += '  - '+info+'\n';
      });
    };
    pack(incomplete, 'TO DO');
    pack(completed, 'COMPLETED');
    context += '\n';
  });
  return context;
}

const AI_PRESETS = [
  {label:'🎬 What should we watch?', prompt:"What should we watch tonight? Pick something from our lists and tell us why."},
  {label:'🍳 What should we cook?', prompt:"Recommend a recipe from our list for tonight. Consider variety from what we've done recently."},
  {label:'🌙 Plan our evening', prompt:"Plan a full evening for us — pick a recipe to cook, something to watch, and an activity. Make it a cohesive vibe."},
  {label:'📅 Plan our week of meals', prompt:"Plan 4-5 dinners for this week. Mix 2-3 recipes from our existing list (balanced for variety and effort) PLUS 2 brand-new recipe suggestions that aren't on our list yet but fit our taste based on what we already have. For each new one, give me the name + one-line why we'd like it + a rough effort level. Finish with a combined shopping list of ingredients, grouped (produce / proteins / pantry / dairy)."},
  {label:'🌦️ Pick for today\'s weather', build: async () => {
    const w = await window.FT?.getWeatherContext?.({ silent: true });
    const fallback = w ? `It's ${w.cond}, ${w.t}°F (feels ${w.feel}°F) where we are.` : "I don't have the exact weather, but use your best guess for today.";
    return `${fallback} Based on the weather RIGHT NOW, pick ONE perfect thing from our lists — could be a recipe (warm soup for cold rain?), a movie (cozy rom-com for a gray day?), or an activity (sunny-day park walk?). Just one pick, with a short line about why it fits the weather.`;
  }},
  {label:'🍂 Perfect for this season', build: async () => {
    const s = window.FT?.getSeasonContext?.() || {};
    const month = s.month || '';
    const season = s.season || '';
    return `It's ${month} — deep ${season}. From our lists, suggest 3 things that feel distinctly ${season}-appropriate RIGHT NOW (seasonal produce for recipes, weather-appropriate activities, season-adjacent movies/shows). One pick from 3 different categories. Keep each suggestion short — name + 1 line on why it fits ${month}.`;
  }},
  {label:'💡 Suggest something new', prompt:"Based on the movies, shows, and recipes we already have, suggest something NEW that's not on our lists yet that we'd probably love. Give us a few ideas for each category with a one-line reason."},
  {label:'🔍 Find us new ideas (with quick-add)', action:'openGapFinder'},
  {label:'💬 Dinner conversation starters', prompt:"Give us 5 fun conversation starters for dinner tonight based on what we've recently watched, cooked, and done. Mix light and deep. Not generic."},
  {label:'🎯 This week\'s focus', prompt:"Looking at everything still pending, what should we prioritize this week? Pick 3-5 items across lists and tell us why now."},
];

const AI_VIBES = [
  {key:'cozy',       label:'🕯️ Cozy night in',     hint:'Go for something cozy, low-key, warm and comforting. Candles / soup / blanket energy.'},
  {key:'adventurous',label:'🌄 Adventurous',        hint:'Lean into adventurous, high-energy, try-something-new vibes.'},
  {key:'date-out',   label:'🌆 Out on the town',    hint:'Pick something we can do OUT — restaurants, bars, walks, shows, or outings.'},
  {key:'quick',      label:'⚡ Quick & easy',        hint:'Pick something quick and low-effort, under 45 min total if possible.'},
  {key:'romantic',   label:'💕 Romantic',            hint:'Keep it romantic, intimate, slow, swoony.'},
  {key:'cheesy',     label:'🧀 Silly & cheesy',      hint:'Pick something deliberately silly, campy, dumb-in-the-best-way.'},
  {key:'deep',       label:'🧠 Thought-provoking',   hint:'Pick something thoughtful, layered, worth discussing after.'},
];

function AISheet({ open, onClose }){
  const d = useData();
  const [messages, setMessages] = useState([]); // [{role:'user'|'assistant', content}]
  const [q, setQ] = useState('');
  const [loading, setLoading] = useState(false);
  const [cats, setCats] = useState(() => {
    try{ const saved = localStorage.getItem('ft-ai-cats'); return saved ? JSON.parse(saved) : null; }catch(e){ return null; }
  });
  const [showCats, setShowCats] = useState(false);
  const [vibe, setVibe] = useState(null); // key from AI_VIBES or null
  const [source, setSource] = useState(() => localStorage.getItem('ft-ai-source') || 'mix'); // 'mix' | 'lists' | 'new'
  const [historyOpen, setHistoryOpen] = useState(false);
  const [history, setHistory] = useState([]);
  const savedIdRef = useRef(null);
  const scrollRef = useRef();
  const me = localStorage.getItem('ft-v2-me') || '';

  // Subscribe to history when sheet opens
  useEffect(() => {
    if(!open) return;
    const unsub = window.FT?.subscribeAiHistory?.(setHistory);
    return unsub;
  }, [open]);

  useEffect(() => {
    try{ if(cats) localStorage.setItem('ft-ai-cats', JSON.stringify(cats)); else localStorage.removeItem('ft-ai-cats'); }catch(e){}
  }, [cats]);

  useEffect(() => {
    try{ localStorage.setItem('ft-ai-source', source); }catch(e){}
  }, [source]);

  const toggleCat = (k) => {
    setCats(prev => {
      const cur = prev || [];
      if(cur.includes(k)){ const next = cur.filter(x => x !== k); return next.length ? next : null; }
      return [...cur, k];
    });
  };

  const baseSysPrompt = `You are a fun, warm assistant helping a couple (${(FT.partners||['them']).join(' and ')}) decide what to watch, cook, or do together. You have access to their lists including ratings, tags, moods, and completion status. Be specific — reference actual items from their lists by name. Keep it conversational, fun, and concise (under 200 words per reply). Use emojis sparingly. When suggesting from their lists, explain WHY. When suggesting something new, explain why they'd love it based on their taste. Remember context from earlier in the conversation.`;
  const vibeObj = vibe && AI_VIBES.find(v => v.key === vibe);
  const sourceHint = source === 'lists'
    ? ' SOURCE MODE: Only recommend items that are ALREADY on their lists. Do not suggest anything new or external. Every pick must come from their existing lists.'
    : source === 'new'
    ? ' SOURCE MODE: Only suggest items that are NOT already on their lists. Use their existing lists ONLY as a taste signal to figure out what they\'d love. Never recommend something that\'s already on their lists.'
    : '';
  const sysPrompt = baseSysPrompt + (vibeObj ? ' TONIGHT\'S VIBE: ' + vibeObj.hint : '') + sourceHint;

  const buildFilteredContext = (d) => {
    if(!d) return '';
    if(!cats || cats.length === 0) return buildListContext(d);
    const filtered = {};
    cats.forEach(k => { if(d[k]) filtered[k] = d[k]; });
    const ctx = buildListContext(filtered);
    const catNames = cats.map(k => d[k]?.title || k).join(', ');
    return ctx + `\n(User has asked to focus ONLY on these lists: ${catNames}. Don't suggest from other lists unless explicitly asked.)\n`;
  };

  const send = async (override) => {
    const question = (override ?? q).trim();
    if(!question || loading) return;
    setQ('');
    const nextMsgs = [...messages, {role:'user', content: question}];
    setMessages(nextMsgs);
    setLoading(true);
    try{
      const context = buildFilteredContext(d);
      // prepend list context to the first user message only
      const apiMessages = nextMsgs.map((m, i) => {
        if(i === 0 && m.role === 'user') return { role:'user', content: context + '\n---\n\n' + m.content };
        return { role: m.role, content: m.content };
      });
      const r = await fetch('https://nudge-proxy.allurhopesndreams.workers.dev/ai', {
        method:'POST',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({
          model:'claude-haiku-4-5-20251001',
          max_tokens:1000,
          system: sysPrompt,
          messages: apiMessages
        })
      });
      const data = await r.json();
      const text = data?.content?.[0]?.text;
      if(text){
        setMessages([...nextMsgs, {role:'assistant', content: text}]);
      } else {
        throw new Error(data?.error?.message || 'No response');
      }
    }catch(e){
      setMessages([...nextMsgs, {role:'assistant', content: "Couldn't reach the AI right now. Try again in a moment. ("+(e.message||e)+")"}]);
    }
    setLoading(false);
  };

  useEffect(() => {
    if(open){
      setMessages([]); setQ(''); setHistoryOpen(false); savedIdRef.current=null;
      // Force scroll to top after the sheet & content mount
      requestAnimationFrame(() => {
        if(scrollRef.current) scrollRef.current.scrollTop = 0;
        const sheetEl = scrollRef.current?.closest('.ft-sheet');
        if(sheetEl) sheetEl.scrollTop = 0;
      });
      // Check for pending prompt (e.g. from Holiday banner tap)
      const pending = window._pendingAIPrompt;
      if(pending){
        delete window._pendingAIPrompt;
        // Small delay so sheet is visible before we kick off the request
        setTimeout(() => { send(pending); }, 300);
      }
    }
  }, [open]);
  useEffect(() => {
    // auto-scroll to bottom when messages arrive — but only if there are messages
    if(messages.length === 0) return;
    const el = scrollRef.current;
    if(el) el.scrollTop = el.scrollHeight;
  }, [messages, loading]);

  // Save conversation to Firestore when sheet closes (only if there's a real exchange)
  useEffect(() => {
    if(open) return;
    if(messages.length >= 2){
      savedIdRef.current = window.FT?.saveAiConversation?.(messages, me) || null;
    }
  }, [open]);

  const reset = () => {
    // Save current convo before clearing (if any)
    if(messages.length >= 2){
      window.FT?.saveAiConversation?.(messages, me);
    }
    setMessages([]); setQ(''); savedIdRef.current=null;
  };
  const loadConversation = (entry) => {
    setMessages(entry.messages || []);
    setHistoryOpen(false);
    savedIdRef.current = entry.id;
  };

  return (
    <Sheet open={open} onClose={onClose} title="✨ Ask AI">
      <div style={{display:'flex',flexDirection:'column',height:'65vh',maxHeight:'65vh'}}>
        <div ref={scrollRef} style={{flex:1,overflowY:'auto',paddingRight:2,marginBottom:10,minHeight:0}}>
          {messages.length === 0 && !loading && historyOpen && (
            <>
              <div style={{display:'flex',alignItems:'center',marginBottom:10}}>
                <button onClick={()=>setHistoryOpen(false)} style={{background:'none',border:'none',color:'var(--text2)',fontSize:13,cursor:'pointer',fontFamily:'inherit',padding:'4px 8px 4px 0'}}>← back</button>
                <div style={{flex:1,textAlign:'center',fontSize:13,color:'var(--text2)',fontWeight:600}}>Past conversations</div>
                <div style={{width:58}}/>
              </div>
              {history.length === 0 ? (
                <div style={{textAlign:'center',padding:30,color:'var(--text3)',fontFamily:'Caveat,cursive',fontSize:17}}>no saved chats yet — ask something first ✨</div>
              ) : (
                <div style={{display:'flex',flexDirection:'column',gap:6}}>
                  {history.map(entry => {
                    const when = new Date(entry.ts).toLocaleDateString('en',{month:'short',day:'numeric'});
                    return (
                      <div key={entry.id} style={{display:'flex',alignItems:'stretch',gap:6}}>
                        <button onClick={()=>loadConversation(entry)} style={{flex:1,background:'var(--card)',border:'1px solid var(--border)',borderRadius:10,padding:'10px 12px',textAlign:'left',cursor:'pointer',fontFamily:'inherit'}}>
                          <div style={{fontSize:12.5,color:'var(--text)',lineHeight:1.4,marginBottom:3,display:'-webkit-box',WebkitLineClamp:2,WebkitBoxOrient:'vertical',overflow:'hidden'}}>{entry.title || 'Conversation'}</div>
                          <div style={{fontSize:10.5,color:'var(--text3)'}}>{entry.by ? entry.by+' · ' : ''}{when} · {(entry.messages||[]).length} message{(entry.messages||[]).length===1?'':'s'}</div>
                        </button>
                        <button onClick={()=>{if(confirm('Delete this conversation?')) window.FT?.deleteAiConversation?.(entry.id);}} style={{background:'var(--card)',border:'1px solid var(--border)',borderRadius:10,padding:'0 10px',color:'var(--text3)',fontSize:14,cursor:'pointer',fontFamily:'inherit'}} title="Delete">🗑</button>
                      </div>
                    );
                  })}
                </div>
              )}
            </>
          )}
          {messages.length === 0 && !loading && !historyOpen && (
            <>
              <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:10,gap:8}}>
                <div style={{fontSize:12,color:'var(--text3)',lineHeight:1.5,flex:1}}>Ask about what to watch, cook, or do — I know your lists. Follow-up questions welcome 💬</div>
                {history.length > 0 && (
                  <button onClick={()=>setHistoryOpen(true)} style={{background:'var(--card)',border:'1px solid var(--border)',borderRadius:99,padding:'4px 10px',fontSize:11,color:'var(--text2)',cursor:'pointer',fontFamily:'inherit',whiteSpace:'nowrap'}}>
                    📖 {history.length} saved
                  </button>
                )}
              </div>
              {d && (
                <div style={{marginBottom:12}}>
                  <div style={{textAlign:'center',marginBottom:showCats?6:0}}>
                    <button onClick={()=>setShowCats(s=>!s)} style={{fontSize:11,color:'var(--text3)',background:'transparent',border:'none',cursor:'pointer',textDecoration:'underline',fontFamily:'inherit'}}>
                      {showCats?'▾':'▸'} consider {cats?.length ? `${cats.length} list${cats.length>1?'s':''}` : 'all lists'}
                    </button>
                  </div>
                  {showCats && (
                    <div style={{display:'flex',gap:5,flexWrap:'wrap',justifyContent:'center'}}>
                      {cats && cats.length > 0 ? (
                        <button onClick={()=>setCats(null)} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>✓ all</button>
                      ) : (
                        <button onClick={()=>setCats([])} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>× none</button>
                      )}
                      {Object.entries(d).map(([k,c]) => {
                        const muted = cats && cats.length>0 && !cats.includes(k);
                        return (
                          <button key={k} onClick={()=>toggleCat(k)}
                            style={{padding:'4px 9px',borderRadius:99,fontSize:11,cursor:'pointer',
                              background:muted?'transparent':'color-mix(in oklab,var(--rose) 22%,transparent)',
                              border:`1px solid ${muted?'var(--border)':'var(--border-accent)'}`,
                              color:muted?'var(--text3)':'var(--text)',
                              opacity:muted?.55:1,fontFamily:'inherit'}}>
                            {c.emoji} {c.title?.replace(/^[^\w]+\s*/,'')}
                          </button>
                        );
                      })}
                    </div>
                  )}
                </div>
              )}
              <div style={{fontSize:10.5,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.12em',marginBottom:6}}>source</div>
              <div style={{display:'flex',gap:5,flexWrap:'wrap',marginBottom:12}}>
                {[
                  {k:'mix',   l:'🔀 Mix'},
                  {k:'lists', l:'🗂️ From our lists'},
                  {k:'new',   l:'✨ New ideas only'}
                ].map(o => (
                  <button key={o.k} onClick={()=>setSource(o.k)} style={{padding:'4px 10px',borderRadius:99,fontSize:11,background:source===o.k?'color-mix(in oklab,var(--lavender) 22%,transparent)':'var(--card)',border:`1px solid ${source===o.k?'var(--border-accent)':'var(--border)'}`,color:source===o.k?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>
                    {o.l}
                  </button>
                ))}
              </div>
              <div style={{fontSize:10.5,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.12em',marginBottom:6}}>vibe</div>
              <div style={{display:'flex',gap:5,flexWrap:'wrap',marginBottom:12}}>
                <button onClick={()=>setVibe(null)} style={{padding:'4px 10px',borderRadius:99,fontSize:11,background:vibe===null?'color-mix(in oklab,var(--rose) 22%,transparent)':'transparent',border:`1px ${vibe===null?'solid':'dashed'} ${vibe===null?'var(--border-accent)':'var(--border)'}`,color:vibe===null?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>✨ anything</button>
                {AI_VIBES.map(v => (
                  <button key={v.key} onClick={()=>setVibe(v.key)} style={{padding:'4px 10px',borderRadius:99,fontSize:11,background:vibe===v.key?'color-mix(in oklab,var(--rose) 22%,transparent)':'var(--card)',border:`1px solid ${vibe===v.key?'var(--border-accent)':'var(--border)'}`,color:vibe===v.key?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>
                    {v.label}
                  </button>
                ))}
              </div>
              <div style={{display:'flex',flexDirection:'column',gap:6}}>
                {AI_PRESETS.map((p,i) => (
                  <button key={i} onClick={async ()=>{
                    if(loading) return;
                    if(p.action === 'openGapFinder'){
                      onClose?.();
                      setTimeout(()=>window.FTAPP?.openGapFinder?.(), 250);
                      return;
                    }
                    const prompt = p.build ? await p.build() : p.prompt;
                    if(prompt) send(prompt);
                  }} disabled={loading}
                    style={{background:'var(--card)',color:'var(--text)',border:'1px solid var(--border2)',textAlign:'left',padding:'10px 12px',fontSize:13,borderRadius:10,cursor:'pointer',fontFamily:'inherit',opacity:loading?.5:1}}>
                    {p.label}
                  </button>
                ))}
              </div>
            </>
          )}
          {messages.map((m, i) => (
            <div key={i} style={{display:'flex',justifyContent: m.role==='user' ? 'flex-end' : 'flex-start',marginBottom:10}}>
              <div style={{
                maxWidth:'85%',
                padding:'10px 13px',
                borderRadius: m.role==='user' ? '16px 16px 4px 16px' : '16px 16px 16px 4px',
                background: m.role==='user' ? 'color-mix(in oklab,var(--rose) 18%,transparent)' : 'var(--card)',
                border: `1px solid ${m.role==='user' ? 'var(--border-accent)' : 'var(--border)'}`,
                fontSize:13.5, lineHeight:1.55, color:'var(--text)'
              }}>
                {m.role==='user' ? (
                  <div style={{whiteSpace:'pre-wrap'}}>{m.content}</div>
                ) : (
                  <FTUI.Markdown text={m.content}/>
                )}
              </div>
            </div>
          ))}
          {loading && (
            <div style={{display:'flex',justifyContent:'flex-start',marginBottom:10}}>
              <div style={{padding:'10px 13px',borderRadius:'16px 16px 16px 4px',background:'var(--card)',border:'1px solid var(--border)',fontSize:13,color:'var(--text3)',fontStyle:'italic'}}>thinking<span style={{display:'inline-block',animation:'pulse 1s ease-in-out infinite'}}>…</span></div>
            </div>
          )}
        </div>
        <div style={{position:'relative',flexShrink:0}}>
          <input className="ft-input" placeholder={messages.length?"reply…":"Or describe what you're in the mood for…"} value={q} onChange={e=>setQ(e.target.value)} onKeyDown={e=>e.key==='Enter'&&send()} style={{paddingRight:48}} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
          <button onClick={()=>send()} disabled={loading||!q.trim()} style={{position:'absolute',right:5,top:5,bottom:5,width:38,padding:0,background:'var(--accent-violet,#B9A4E3)',color:'#fff',border:'none',borderRadius:8,cursor:'pointer',fontSize:16,fontWeight:600,opacity:(loading||!q.trim())?.4:1}}>→</button>
        </div>
        {messages.length>0 && (
          <button onClick={reset} style={{marginTop:8,background:'transparent',border:'none',color:'var(--text3)',fontSize:11,cursor:'pointer',textDecoration:'underline',alignSelf:'center',fontFamily:'inherit'}}>start new conversation</button>
        )}
      </div>
    </Sheet>
  );
}

// ---------- GAP FINDER (What's Missing + quick-add) ----------
function GapFinderSheet({ open, onClose, me }){
  const d = useData();
  const [cat, setCat] = useState(null); // category key or null = all
  const [items, setItems] = useState([]); // suggestions
  const [intro, setIntro] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [addedIds, setAddedIds] = useState({}); // idx -> new item id

  useEffect(() => {
    if(open){
      setItems([]); setIntro(''); setError(null); setAddedIds({});
      // Default to "all" so user sees everything unless they pick a cat
      setCat(null);
    }
  }, [open]);

  const run = async () => {
    if(!d || loading) return;
    setLoading(true); setError(null); setAddedIds({});
    try {
      let context;
      let focus;
      if(cat){
        const filtered = { [cat]: d[cat] };
        context = buildListContext(filtered);
        focus = `Focus on the ${d[cat]?.title || cat} list.`;
      } else {
        context = buildListContext(d);
        focus = 'Consider all our lists.';
      }
      const season = window.FT?.getSeasonContext?.();
      const seasonHint = season ? ` Current season: ${season.month} (${season.season}). Lean slightly toward season-appropriate picks where it fits (not every pick needs to match the season).` : '';
      const prompt = `${focus}${seasonHint} Based on our taste signals (what we've added, rated highly, and completed), suggest 6-8 items we DON'T already have that we'd probably love. Mix across our categories. Return ONLY a JSON array with this exact shape: [{"category":"<one of: ${Object.keys(d).join(', ')}>","name":"<item name>","why":"<1 short sentence on why we'd love it>"}]. No prose before or after the JSON. Do not suggest anything already on our lists.`;
      const r = await fetch('https://nudge-proxy.allurhopesndreams.workers.dev/ai', {
        method:'POST',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({
          model:'claude-haiku-4-5-20251001',
          max_tokens:900,
          system:`You're a taste-smart recommender for a couple (${(FT.partners||['them']).join(' and ')}). You know their lists intimately and only suggest NEW items (never already on their lists). You respond in clean JSON.`,
          messages:[{role:'user', content: context + '\n---\n\n' + prompt}]
        })
      });
      const resp = await r.json();
      const text = resp?.content?.[0]?.text || '';
      const match = text.match(/\[[\s\S]*\]/);
      if(!match) throw new Error('Could not parse suggestions.');
      const arr = JSON.parse(match[0]);
      // Filter out any that happen to match existing items (defensive)
      const existingNames = new Set();
      Object.values(d).forEach(c => c.items.forEach(i => existingNames.add((i.text||'').toLowerCase().trim())));
      const filtered = arr.filter(x => x.name && !existingNames.has(x.name.toLowerCase().trim()));
      setItems(filtered);
      // Any prose before the JSON becomes the intro
      const before = text.slice(0, match.index).trim();
      if(before) setIntro(before);
    } catch(e){
      setError(e.message || 'Something went wrong.');
    }
    setLoading(false);
  };

  const addOne = (idx) => {
    const it = items[idx];
    if(!it || !d[it.category]) return;
    window.FT?.addItem?.(it.category, it.name, { note: it.why || '' }, me);
    setAddedIds(prev => ({ ...prev, [idx]: true }));
  };

  const undoAdd = (idx) => {
    // We don't track the created id, so simplest: decrement via find + remove
    // This is a best-effort — find the most-recent item in that category with the same text.
    const it = items[idx];
    if(!it) return;
    const cat = d[it.category];
    if(!cat) return;
    const found = cat.items.find(x => (x.text||'').toLowerCase().trim() === it.name.toLowerCase().trim());
    if(found){
      window.FT?.removeItem?.(it.category, found.id);
    }
    setAddedIds(prev => { const n = { ...prev }; delete n[idx]; return n; });
  };

  if(!d) return null;
  return (
    <Sheet open={open} onClose={onClose} title="🔍 Find us new ideas">
      <div style={{fontSize:12.5,color:'var(--text3)',lineHeight:1.5,marginBottom:12}}>
        I'll look at your lists and suggest brand-new things you'd love — nothing already on your lists. Tap <b>+</b> on any pick to add it instantly.
      </div>

      <div style={{fontSize:10.5,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.12em',marginBottom:6}}>focus on</div>
      <div style={{display:'flex',gap:5,flexWrap:'wrap',marginBottom:14}}>
        <button onClick={()=>setCat(null)} style={{padding:'5px 11px',borderRadius:99,fontSize:11.5,background:cat===null?'color-mix(in oklab,var(--lavender) 22%,transparent)':'var(--card)',border:`1px solid ${cat===null?'var(--border-accent)':'var(--border)'}`,color:cat===null?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>all lists</button>
        {Object.entries(d).map(([k,c]) => (
          <button key={k} onClick={()=>setCat(k)} style={{padding:'5px 11px',borderRadius:99,fontSize:11.5,background:cat===k?'color-mix(in oklab,var(--lavender) 22%,transparent)':'var(--card)',border:`1px solid ${cat===k?'var(--border-accent)':'var(--border)'}`,color:cat===k?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>
            {c.emoji} {(c.title||'').replace(/^[^\w]+\s*/,'')}
          </button>
        ))}
      </div>

      <button onClick={run} disabled={loading} style={{width:'100%',padding:'11px 14px',borderRadius:11,background:'linear-gradient(135deg,var(--rose),var(--lavender))',color:'#fff',border:'none',fontSize:13.5,fontWeight:600,cursor:'pointer',fontFamily:'inherit',marginBottom:14,opacity:loading?.6:1}}>
        {loading ? 'finding picks…' : items.length ? '↻ try again' : '✨ find new ideas'}
      </button>

      {error && <div style={{fontSize:12,color:'var(--rose)',marginBottom:12}}>{error}</div>}

      {intro && <div style={{fontSize:12.5,color:'var(--text2)',lineHeight:1.5,marginBottom:10}}>{intro}</div>}

      <div style={{display:'flex',flexDirection:'column',gap:8}}>
        {items.map((it, i) => {
          const catInfo = d[it.category];
          const added = !!addedIds[i];
          return (
            <div key={i} style={{padding:'10px 12px',borderRadius:11,background:'var(--card)',border:'1px solid var(--border)',display:'flex',alignItems:'flex-start',gap:10}}>
              <div style={{flex:1,minWidth:0}}>
                <div style={{fontSize:10,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.1em',marginBottom:3}}>{catInfo?.emoji} {(catInfo?.title||it.category).replace(/^[^\w]+\s*/,'')}</div>
                <div style={{fontSize:13.5,color:'var(--text)',fontWeight:600,marginBottom:3,lineHeight:1.3}}>{it.name}</div>
                {it.why && <div style={{fontSize:11.5,color:'var(--text3)',lineHeight:1.4,fontStyle:'italic'}}>{it.why}</div>}
              </div>
              {added ? (
                <button onClick={()=>undoAdd(i)} style={{flexShrink:0,padding:'6px 10px',borderRadius:8,background:'rgba(109,201,156,.15)',color:'var(--teal,#6DC99C)',border:'1px solid rgba(109,201,156,.4)',fontSize:11,cursor:'pointer',fontFamily:'inherit',whiteSpace:'nowrap'}} title="Undo">
                  ✓ added
                </button>
              ) : (
                <button onClick={()=>addOne(i)} style={{flexShrink:0,width:32,height:32,borderRadius:'50%',background:'linear-gradient(135deg,var(--rose),var(--lavender))',color:'#fff',border:'none',fontSize:16,cursor:'pointer',fontFamily:'inherit',display:'flex',alignItems:'center',justifyContent:'center'}} title="Add to list">+</button>
              )}
            </div>
          );
        })}
      </div>

      {!loading && !items.length && !error && (
        <div style={{textAlign:'center',fontSize:13,color:'var(--text3)',padding:'24px 0',fontFamily:'Caveat,cursive',fontSize:16}}>
          tap "find new ideas" to see picks ✨
        </div>
      )}
    </Sheet>
  );
}

// ---------- SMART SEARCH (natural language across all lists) ----------
function SmartSearchSheet({ open, onClose, initialQuery }){
  const d = useData();
  const [q, setQ] = useState(initialQuery || '');
  const [loading, setLoading] = useState(false);
  const [results, setResults] = useState([]);
  const [error, setError] = useState(null);
  const inputRef = useRef();

  useEffect(() => {
    if(open){
      setQ(initialQuery || '');
      setResults([]);
      setError(null);
      setTimeout(() => { try { inputRef.current?.focus(); } catch(e){} }, 340);
    }
  }, [open, initialQuery]);

  const run = async () => {
    const query = q.trim();
    if(!query || !d || loading) return;
    setLoading(true); setError(null); setResults([]);
    try {
      // Build a compact list of all items with ids so Claude can reference them
      const idx = [];
      Object.entries(d).forEach(([catKey, cat]) => {
        cat.items.forEach(it => {
          const bits = [it.text];
          if(it.note) bits.push('note: ' + it.note.slice(0, 120));
          if(it.where) bits.push('@ ' + it.where);
          if(it.mood) bits.push('['+it.mood+']');
          if(it.tags?.length) bits.push(it.tags.slice(0,4).join(','));
          if(it.tmdb_year) bits.push(it.tmdb_year);
          if(it.tmdb_rating) bits.push('★'+it.tmdb_rating);
          if(it.done) bits.push('✓ done');
          idx.push({ id: it.id, cat: catKey, catTitle: cat.title, text: bits.join(' | ') });
        });
      });
      if(!idx.length){ setError('no items yet'); setLoading(false); return; }
      const prompt = `We're looking for: "${query}"

Our lists:
${idx.map(x => `- [${x.cat}:${x.id}] ${x.text}`).join('\n')}

Find the 5 best matches. Return ONLY a JSON array shaped:
[{"id":"<id>","cat":"<catKey>","why":"<1 short sentence on why it matches>"}]

Matching rules:
- Be flexible on spelling/fuzzy matches ("thai place" matches "Night + Market")
- Understand intent: "cozy rom-com" → match items with those moods/tags/genres
- "that place we talked about" → prefer items with recent activity or comments
- Return items from ANY list (restaurants, recipes, movies, whatever)
- If fewer than 5 good matches, return only the genuine ones (could be 1 or 0)
- No prose. JSON only.`;
      const r = await fetch('https://nudge-proxy.allurhopesndreams.workers.dev/ai', {
        method:'POST',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({
          model:'claude-haiku-4-5-20251001',
          max_tokens:700,
          system:"You're a warm, smart search assistant. You find items on a couple's lists with fuzzy, natural-language matching. Return clean JSON only.",
          messages:[{role:'user', content: prompt}]
        })
      });
      const resp = await r.json();
      const text = resp?.content?.[0]?.text || '';
      const match = text.match(/\[[\s\S]*\]/);
      if(!match){ setError('no matches'); setLoading(false); return; }
      const hits = JSON.parse(match[0]);
      // Hydrate each hit with the actual item object
      const hydrated = hits.map(h => {
        const cat = d[h.cat];
        const item = cat?.items.find(i => i.id === h.id);
        if(!item) return null;
        return { ...h, item, cat: h.cat, catTitle: cat.title, emoji: cat.emoji };
      }).filter(Boolean);
      setResults(hydrated);
      if(!hydrated.length) setError("couldn't find anything matching");
    } catch(e){
      setError(e.message || 'search failed');
    }
    setLoading(false);
  };

  const openItem = (hit) => {
    onClose();
    setTimeout(() => window.FTAPP?.navigate?.('list', hit.cat, hit.item.id), 100);
  };

  return (
    <Sheet open={open} onClose={onClose} title="✨ Smart search">
      <div style={{fontSize:12.5,color:'var(--text3)',lineHeight:1.45,marginBottom:10}}>
        Search your lists the way you'd describe it out loud — "that thai place", "cozy rom-com", "something we saved last month"…
      </div>
      <div style={{position:'relative'}}>
        <input ref={inputRef} className="ft-input" placeholder="describe what you're looking for…" value={q} onChange={e=>setQ(e.target.value)} onKeyDown={e=>e.key==='Enter'&&run()} style={{paddingRight:48}} autoCorrect="on" autoCapitalize="sentences"/>
        <button onClick={run} disabled={loading||!q.trim()} style={{position:'absolute',right:5,top:5,bottom:5,width:38,padding:0,background:'linear-gradient(135deg,var(--rose),var(--lavender))',color:'#fff',border:'none',borderRadius:8,cursor:'pointer',fontSize:16,fontWeight:600,opacity:(loading||!q.trim())?.4:1}}>→</button>
      </div>
      {error && <div style={{marginTop:10,fontSize:12.5,color:'var(--rose)',fontFamily:'Caveat,cursive',fontSize:15}}>{error}</div>}
      {loading && (
        <div style={{marginTop:14,padding:16,textAlign:'center',fontSize:13,color:'var(--text3)',fontStyle:'italic'}}>searching across all your lists…</div>
      )}
      {results.length > 0 && (
        <div style={{display:'flex',flexDirection:'column',gap:8,marginTop:14}}>
          {results.map((hit, i) => (
            <button key={i} onClick={()=>openItem(hit)} style={{textAlign:'left',background:'var(--card)',border:'1px solid var(--border)',borderRadius:12,padding:'11px 13px',cursor:'pointer',fontFamily:'inherit',color:'var(--text)',display:'flex',flexDirection:'column',gap:4}}>
              <div style={{fontSize:10,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.12em'}}>{hit.emoji} {(hit.catTitle||'').replace(/^[^\w]+\s*/,'')}</div>
              <div style={{fontSize:13.5,fontWeight:600,color:'var(--text)'}}>{hit.item.text}</div>
              {hit.why && <div style={{fontSize:11.5,color:'var(--text3)',fontStyle:'italic',lineHeight:1.4}}>{hit.why}</div>}
            </button>
          ))}
        </div>
      )}
    </Sheet>
  );
}

// ---------- SETTINGS ----------
const FT_PALETTES = [
  { key:'rose',   label:'Rose',   colors:['#E8A5C0','#B9A4E3'] }, // pink → lavender
  { key:'sunset', label:'Sunset', colors:['#F09090','#E8C87A'] }, // coral → gold
  { key:'ocean',  label:'Ocean',  colors:['#7AC8EA','#2F5EBD'] }, // coastal blue → deep navy
  { key:'garden', label:'Garden', colors:['#A8C3A0','#F2B988'] }, // sage → apricot
  { key:'mono',   label:'Mono',   colors:['#C8C1CC','#A89AC6'] }, // gray → lavender
];

function SettingsSheet({ open, onClose, me, onSignOut }){
  const profiles = useProfiles();
  const [themeMode, setThemeMode] = useState(localStorage.getItem('ft-v2-theme-mode') || 'auto');
  const [palette, setPaletteState] = useState(localStorage.getItem('ft-v2-palette') || 'rose');
  const [sound, setSound] = useState(localStorage.getItem('ft-v2-sound') !== '0');
  const [notif, setNotif] = useState(typeof Notification !== 'undefined' ? Notification.permission : 'default');
  const [ownWizardOpen, setOwnWizardOpen] = useState(false);
  const [fbOpen, setFbOpen] = useState(false);
  const [annDate, setAnnDate] = useState(profiles?.meta?.anniversary || '');
  useEffect(() => { setAnnDate(profiles?.meta?.anniversary || ''); }, [profiles?.meta?.anniversary]);
  const saveAnn = (val) => {
    setAnnDate(val);
    window.FT?.saveCoupleMeta?.({ anniversary: val || null });
  };
  const storageMode = window.FT?.mode;
  const pairCode = window.FTStorage?.getRoomCode?.();

  const setPalette = (k) => {
    setPaletteState(k);
    try { localStorage.setItem('ft-v2-palette', k); } catch(e){}
    if(k === 'rose') document.documentElement.removeAttribute('data-palette');
    else document.documentElement.setAttribute('data-palette', k);
  };

  const changePairCode = () => {
    if(!confirm('Switch to a different pair code? Your current lists stay in the cloud under the old code — you can always come back by entering that code again.')) return;
    window.FTStorage.clearMode();
    window.location.reload();
  };

  useEffect(() => {
    // Apply + resolve
    localStorage.setItem('ft-v2-theme-mode', themeMode);
    let resolved = themeMode;
    if(themeMode==='auto'){
      resolved = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    }
    document.documentElement.setAttribute('data-theme', resolved);
    localStorage.setItem('ft-v2-theme', resolved);
  }, [themeMode]);
  useEffect(() => { localStorage.setItem('ft-v2-sound', sound ? '1':'0'); }, [sound]);

  const askNotif = async () => {
    if(!('Notification' in window)) return alert('Not supported on this device');
    const res = await (window.FT?.setupPush ? window.FT.setupPush(true) : Promise.resolve('no-setup'));
    setNotif(typeof Notification !== 'undefined' ? Notification.permission : 'default');
    if(res==='ok'){ window.FT?.showPushToast?.('Notifications enabled! 🎉'); }
    else if(res==='denied'){ alert('Notifications were blocked. Enable them in browser settings.'); }
    else if(res && res.startsWith?.('error')){ alert('Could not enable notifications: '+res); }
  };

  const Row = ({label, sub, children}) => (
    <div style={{display:'flex',alignItems:'center',gap:12,padding:'14px 2px',borderBottom:'1px solid var(--border)'}}>
      <div style={{flex:1,minWidth:0}}>
        <div style={{fontSize:14,fontWeight:500}}>{label}</div>
        {sub && <div style={{fontSize:11.5,color:'var(--text3)',marginTop:2}}>{sub}</div>}
      </div>
      {children}
    </div>
  );
  const Toggle = ({on, onChange}) => (
    <button onClick={()=>onChange(!on)} style={{width:46,height:26,borderRadius:99,background:on?'linear-gradient(135deg,var(--rose),var(--lavender))':'var(--card-strong)',border:'1px solid var(--border2)',position:'relative',transition:'all .2s',padding:0,flexShrink:0}}>
        <span style={{position:'absolute',top:2,left:on?22:2,width:20,height:20,borderRadius:'50%',background:'#fff',transition:'left .2s',boxShadow:'0 1px 3px rgba(0,0,0,.3)'}}/>
    </button>
  );

  return (
    <Sheet open={open} onClose={onClose} title="Settings">
      <Row label="Theme" sub={themeMode==='auto'?'Follows your device':themeMode==='dark'?'Midnight — easier on the eyes':'Daylight'}>
        <div style={{display:'flex',gap:4,padding:3,borderRadius:99,background:'var(--card-strong)',border:'1px solid var(--border)'}}>
          {[['auto','◐','Auto'],['light','☀️','Light'],['dark','🌙','Dark']].map(([k,g,lbl]) => (
            <button key={k} onClick={()=>setThemeMode(k)} title={lbl} style={{padding:'5px 9px',borderRadius:99,background:themeMode===k?'var(--card)':'transparent',border:'none',color:themeMode===k?'var(--text)':'var(--text3)',fontSize:12,cursor:'pointer',fontWeight:themeMode===k?600:400}}>
              <span style={{marginRight:4}}>{g}</span>{lbl}
            </button>
          ))}
        </div>
      </Row>
      <Row label="Color palette" sub={`${FT_PALETTES.find(p=>p.key===palette)?.label || 'Rose'} — brand tint across the app`}>
        <div style={{display:'flex',gap:8,flexWrap:'wrap',justifyContent:'flex-end'}}>
          {FT_PALETTES.map(p => {
            const selected = palette === p.key;
            return (
              <button key={p.key} onClick={()=>setPalette(p.key)} title={p.label} aria-label={p.label}
                style={{
                  width:30,height:30,borderRadius:'50%',padding:0,cursor:'pointer',border:'none',
                  background:`linear-gradient(135deg, ${p.colors[0]}, ${p.colors[1]})`,
                  boxShadow: selected
                    ? '0 0 0 2px var(--bg), 0 0 0 4px var(--text), 0 2px 8px rgba(0,0,0,.25)'
                    : '0 1px 3px rgba(0,0,0,.2)',
                  transition: 'transform .15s ease',
                  transform: selected ? 'scale(1.08)' : 'scale(1)',
                }}>
              </button>
            );
          })}
        </div>
      </Row>
      <Row label="Sounds" sub="Little chimes on check-offs">
        <Toggle on={sound} onChange={setSound}/>
      </Row>
      <Row label="Notifications" sub={notif==='granted'?'On — you\'ll feel the nudges':notif==='denied'?'Blocked in browser settings':'Not yet enabled'}>
        {notif==='granted' ? <span style={{fontSize:11,color:'var(--teal)'}}>✓ On</span> : <button onClick={askNotif} className="ft-btn ft-btn-soft ft-btn-sm">Enable</button>}
      </Row>
      <Row label="Anniversary" sub={annDate ? 'Set — banner appears the week of' : 'When\'s your together-date?'}>
        <input type="date" value={annDate} onChange={e=>saveAnn(e.target.value)} style={{background:'var(--card)',border:'1px solid var(--border2)',color:'var(--text)',padding:'5px 8px',borderRadius:8,fontSize:12,fontFamily:'inherit',colorScheme:'dark'}}/>
      </Row>
      <Row label="Signed in as" sub={me}>
        <button onClick={onSignOut} className="ft-btn ft-btn-ghost ft-btn-sm">Switch</button>
      </Row>
      {storageMode === 'shared' && pairCode && (
        <Row label="Pair code" sub={pairCode}>
          <button onClick={changePairCode} className="ft-btn ft-btn-ghost ft-btn-sm">Change</button>
        </Row>
      )}
      {storageMode === 'own' && (
        <Row label="Sync" sub="Using your own Firebase project">
          <span style={{fontSize:11,color:'var(--teal)'}}>✓ Self-hosted</span>
        </Row>
      )}
      {storageMode === 'shared' && (
        <Row label="Feedback" sub="Send Lauren a note — bugs, ideas, wishes">
          <button onClick={()=>setFbOpen(true)} className="ft-btn ft-btn-ghost ft-btn-sm">Send</button>
        </Row>
      )}
      {storageMode === 'shared' && (
        <Row label="Advanced" sub="Use your own Firebase project (optional, for full ownership of your data)">
          <button onClick={()=>setOwnWizardOpen(true)} className="ft-btn ft-btn-ghost ft-btn-sm">Set up</button>
        </Row>
      )}
      <div style={{marginTop:18,padding:14,borderRadius:12,background:'var(--card)',border:'1px solid var(--border)',fontSize:11.5,color:'var(--text3)',textAlign:'center',lineHeight:1.5}}>
        ♡ made with love<br/>syncs across your devices via the cloud
      </div>
      <OwnFirebaseWizard open={ownWizardOpen} onClose={()=>setOwnWizardOpen(false)}/>
      <FeedbackSheet open={fbOpen} onClose={()=>setFbOpen(false)} me={me}/>
    </Sheet>
  );
}

// ---------- FEEDBACK SHEET ----------
// Sends the message + optional photos via Telegram bot → Lauren's phone. See FT.sendFeedback.
function FeedbackSheet({ open, onClose, me }){
  const [text, setText] = useState('');
  const [photos, setPhotos] = useState([]); // array of { file, url }
  const [status, setStatus] = useState('');
  const fileInputRef = useRef();
  useEffect(() => {
    if(open){ setText(''); setStatus(''); setPhotos([]); }
    // Revoke object URLs on close to free memory
    return () => { photos.forEach(p => { try{ URL.revokeObjectURL(p.url); }catch(e){} }); };
  }, [open]);
  const addFiles = (fileList) => {
    const list = Array.from(fileList||[]).filter(f => /^image\//i.test(f.type));
    if(!list.length) return;
    setPhotos(prev => [...prev, ...list.map(f => ({ file: f, url: URL.createObjectURL(f) }))]);
  };
  const removePhoto = (i) => {
    setPhotos(prev => {
      try{ URL.revokeObjectURL(prev[i].url); }catch(e){}
      return prev.filter((_, idx) => idx !== i);
    });
  };
  const submit = async () => {
    if(!text.trim() && !photos.length){ setStatus('error:Add a message or a photo'); return; }
    setStatus('sending');
    try{
      await FT.sendFeedback(text, me, photos.map(p => p.file));
      setStatus('sent');
      setTimeout(onClose, 1600);
    }catch(e){ setStatus('error:' + (e.message||'Failed')); }
  };
  if(!open) return null;
  return (
    <div style={{position:'fixed',inset:0,zIndex:9999,background:'rgba(0,0,0,.6)',display:'flex',alignItems:'flex-end',justifyContent:'center'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:480,background:'var(--bg)',borderRadius:'20px 20px 0 0',padding:'22px 20px 36px',color:'var(--text)',fontFamily:'Inter Tight, system-ui',maxHeight:'92dvh',overflowY:'auto'}}>
        <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}>
          <div style={{fontFamily:'Playfair Display, serif',fontWeight:700,fontSize:22}}>Send Lauren a note</div>
          <button onClick={onClose} style={{background:'none',border:'none',color:'var(--text2)',fontSize:22,cursor:'pointer',padding:4}}>×</button>
        </div>
        <p style={{fontSize:13.5,color:'var(--text2)',margin:'0 0 12px',lineHeight:1.5}}>Bugs, feature ideas, or anything you want different. Goes straight to her phone.</p>
        {status === 'sent' ? (
          <div style={{textAlign:'center',padding:'36px 0'}}>
            <div style={{fontSize:48,marginBottom:8}}>💌</div>
            <div style={{fontFamily:'Caveat,cursive',fontSize:22}}>sent!</div>
          </div>
        ) : (
          <>
            <textarea
              value={text}
              onChange={e=>setText(e.target.value)}
              placeholder="what's on your mind?"
              rows={5}
              autoFocus
              style={{width:'100%',padding:12,fontSize:14,fontFamily:'inherit',border:'1px solid var(--border2)',borderRadius:12,background:'var(--card)',color:'var(--text)',outline:'none',boxSizing:'border-box',resize:'vertical',marginBottom:10}}
            />
            {/* Photo previews */}
            {photos.length > 0 && (
              <div style={{display:'flex',flexWrap:'wrap',gap:8,marginBottom:10}}>
                {photos.map((p, i) => (
                  <div key={i} style={{position:'relative',width:72,height:72,borderRadius:10,overflow:'hidden',border:'1px solid var(--border2)',background:'var(--card)'}}>
                    <img src={p.url} alt="" style={{width:'100%',height:'100%',objectFit:'cover'}}/>
                    <button onClick={()=>removePhoto(i)} style={{position:'absolute',top:2,right:2,width:20,height:20,borderRadius:'50%',background:'rgba(0,0,0,.7)',color:'#fff',border:'none',fontSize:14,lineHeight:1,cursor:'pointer',display:'flex',alignItems:'center',justifyContent:'center',padding:0}}>×</button>
                  </div>
                ))}
              </div>
            )}
            {/* Attach + send row */}
            <input
              ref={fileInputRef}
              type="file"
              accept="image/*"
              multiple
              onChange={e=>{ addFiles(e.target.files); e.target.value=''; }}
              style={{display:'none'}}
            />
            <div style={{display:'flex',gap:8,marginBottom:10}}>
              <button
                onClick={()=>fileInputRef.current?.click()}
                disabled={status === 'sending'}
                style={{padding:'12px 14px',fontSize:13,fontFamily:'inherit',border:'1px solid var(--border2)',borderRadius:12,background:'var(--card)',color:'var(--text2)',cursor:'pointer',flexShrink:0}}
                title="Attach a photo or screenshot"
              >📎 Attach</button>
              <button
                onClick={submit}
                disabled={status === 'sending'}
                style={{flex:1,padding:'12px',fontSize:15,fontWeight:600,fontFamily:'inherit',border:'none',borderRadius:12,background:'linear-gradient(135deg,#F2B988,#E8A5C0,#B9A4E3)',color:'#fff',cursor:status === 'sending' ? 'wait' : 'pointer',opacity:status === 'sending' ? 0.6 : 1}}
              >{status === 'sending' ? 'Sending…' : 'Send ♡'}</button>
            </div>
            {status.startsWith('error:') && <div style={{color:'#E8A5C0',fontSize:13}}>{status.slice(6)}</div>}
          </>
        )}
      </div>
    </div>
  );
}

// ---------- OWN-FIREBASE UPGRADE WIZARD ----------
// Walks the user through pasting their own Firebase config, migrates data from the
// current (shared) backend, and switches mode.
function OwnFirebaseWizard({ open, onClose }){
  const [step, setStep] = useState(1);
  const [configText, setConfigText] = useState('');
  const [status, setStatus] = useState('');
  const [migrating, setMigrating] = useState(false);

  useEffect(() => { if(open){ setStep(1); setConfigText(''); setStatus(''); setMigrating(false); } }, [open]);

  const parseConfig = () => {
    try{
      // Accept either pasted JS object (apiKey: "...") or JSON
      let txt = configText.trim();
      // Strip `const firebaseConfig =` prefix + trailing semicolon if present
      txt = txt.replace(/^\s*(const|let|var)\s+\w+\s*=\s*/, '').replace(/;\s*$/, '');
      // Convert JS object to JSON (quote keys, single→double quotes)
      const asJson = txt
        .replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3')
        .replace(/'/g, '"');
      const cfg = JSON.parse(asJson);
      if(!cfg.apiKey || !cfg.projectId) throw new Error('Missing apiKey or projectId');
      return cfg;
    }catch(e){
      throw new Error('Could not parse that — make sure it looks like the snippet from Firebase Console → Project settings → Your apps → Config.');
    }
  };

  const runMigration = async () => {
    setMigrating(true); setStatus('Parsing config…');
    let cfg;
    try{ cfg = parseConfig(); }
    catch(e){ setStatus(e.message); setMigrating(false); return; }

    try{
      setStatus('Reading your data from the shared cloud…');
      // Read every doc we know about from the current (shared) backend
      const currentDb = window.FT.db;
      const docsToCopy = [
        ['lists','main'], ['lists','profiles_v2'], ['lists','activity'],
        ['lists','thinking_of_you'], ['lists','ai_history'], ['lists','streaks_v2'],
        ['lists','notes_v2'], ['lists','surprise_history'],
        ['lists','activity_reactions'],
      ];
      const snapshots = await Promise.all(
        docsToCopy.map(([c,d]) => currentDb.collection(c).doc(d).get().catch(()=>null))
      );

      setStatus('Connecting to your Firebase project…');
      // Initialize a SECONDARY firebase app with the new config (can't re-init default)
      const secondaryName = 'ft-own-' + Date.now();
      const secondaryApp = firebase.initializeApp(cfg, secondaryName);
      const newDb = secondaryApp.firestore();

      setStatus('Copying data to your Firebase project…');
      let ok = 0, fail = 0;
      for(let i = 0; i < docsToCopy.length; i++){
        const snap = snapshots[i];
        if(!snap || !snap.exists) continue;
        const [col, doc] = docsToCopy[i];
        try{
          await newDb.collection(col).doc(doc).set(snap.data());
          ok++;
        }catch(e){ console.warn('migrate', col, doc, e); fail++; }
      }

      setStatus(`Done — copied ${ok} doc${ok===1?'':'s'}. Switching over…`);
      // Save new mode; reload so storage.js re-initializes against the user's own project
      window.FTStorage.setMode({ mode: 'own', firebaseConfig: cfg });
      setTimeout(()=>window.location.reload(), 1200);
    }catch(e){
      console.error(e);
      setStatus('Something went wrong: ' + (e.message||e));
      setMigrating(false);
    }
  };

  if(!open) return null;

  return (
    <div style={{position:'fixed',inset:0,zIndex:9999,background:'rgba(0,0,0,.6)',display:'flex',alignItems:'flex-end',justifyContent:'center'}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{width:'100%',maxWidth:520,maxHeight:'92dvh',overflowY:'auto',background:'var(--bg)',borderRadius:'20px 20px 0 0',padding:'22px 20px 40px',color:'var(--text)',fontFamily:'Inter Tight, system-ui'}}>
        <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:14}}>
          <div style={{fontFamily:'Playfair Display, serif',fontWeight:700,fontSize:22}}>Use your own Firebase</div>
          <button onClick={onClose} style={{background:'none',border:'none',color:'var(--text2)',fontSize:22,cursor:'pointer',padding:4}}>×</button>
        </div>

        {step === 1 && (
          <>
            <p style={{fontSize:14,lineHeight:1.5,color:'var(--text2)',marginBottom:14}}>
              This moves your data out of the shared cloud and into a Firebase project you own. Optional — do this if you want full control. Free tier is more than enough.
            </p>
            <ol style={{fontSize:13.5,lineHeight:1.65,color:'var(--text2)',paddingLeft:20,marginBottom:18}}>
              <li>Go to <b>console.firebase.google.com</b> → create a new project (any name).</li>
              <li>Inside it: <b>Build → Firestore Database → Create database</b> (start in production mode, pick any region).</li>
              <li>Go to <b>Project settings</b> (gear icon) → scroll to <b>Your apps</b> → click the <b>&lt;/&gt;</b> (web) icon → register a nickname.</li>
              <li>Copy the <code style={{background:'var(--card-strong)',padding:'1px 5px',borderRadius:4}}>firebaseConfig</code> object that appears and paste it below.</li>
            </ol>
            <button onClick={()=>setStep(2)} style={{width:'100%',padding:'14px',fontSize:15,fontWeight:600,fontFamily:'inherit',border:'none',borderRadius:12,background:'linear-gradient(135deg,#F2B988,#E8A5C0,#B9A4E3)',color:'#fff',cursor:'pointer'}}>I have my config</button>
          </>
        )}

        {step === 2 && (
          <>
            <p style={{fontSize:13,color:'var(--text2)',marginBottom:10,lineHeight:1.5}}>Paste the whole <code style={{background:'var(--card-strong)',padding:'1px 5px',borderRadius:4}}>firebaseConfig</code> block:</p>
            <textarea
              value={configText}
              onChange={e=>setConfigText(e.target.value)}
              placeholder={'{\n  apiKey: "...",\n  authDomain: "...",\n  projectId: "...",\n  ...\n}'}
              rows={9}
              style={{width:'100%',padding:12,fontSize:12.5,fontFamily:'ui-monospace, SFMono-Regular, monospace',border:'1px solid var(--border2)',borderRadius:12,background:'var(--card)',color:'var(--text)',outline:'none',boxSizing:'border-box',resize:'vertical',marginBottom:8}}
            />
            <p style={{fontSize:12,color:'var(--text3)',marginBottom:14,lineHeight:1.5}}>
              Don't forget: in your new project's Firestore <b>Rules</b> tab, paste:<br/>
              <code style={{display:'block',marginTop:6,padding:10,background:'var(--card-strong)',borderRadius:8,fontSize:11.5,whiteSpace:'pre-wrap'}}>{`rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} { allow read, write: if true; }\n  }\n}`}</code>
            </p>
            {status && <div style={{padding:10,marginBottom:10,background:'var(--card)',border:'1px solid var(--border)',borderRadius:8,fontSize:13,color:'var(--text2)'}}>{status}</div>}
            <div style={{display:'flex',gap:8}}>
              <button onClick={()=>setStep(1)} disabled={migrating} className="ft-btn ft-btn-ghost" style={{flex:1}}>Back</button>
              <button onClick={runMigration} disabled={migrating || !configText.trim()} style={{flex:2,padding:'14px',fontSize:15,fontWeight:600,fontFamily:'inherit',border:'none',borderRadius:12,background:'linear-gradient(135deg,#F2B988,#E8A5C0,#B9A4E3)',color:'#fff',cursor:migrating?'wait':'pointer',opacity:migrating?0.6:1}}>{migrating ? 'Migrating…' : 'Migrate my data'}</button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ---------- NOTES HISTORY ----------
function NotesHistorySheet({ open, onClose, me }){
  const [notes, setNotes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [tick, setTick] = useState(0);
  useEffect(() => {
    if(!open) return;
    setLoading(true);
    const db = window.FT.db;
    Promise.all([
      db.collection('lists').doc('notes_v2').get().catch(()=>null),
      db.collection('lists').doc('surprise_history').get().catch(()=>null),
    ]).then(([v2, v1]) => {
      const combined = [];
      if(v2 && v2.exists && v2.data().log) v2.data().log.forEach((n,idx) => combined.push({...n, _src:'v2', _idx:idx}));
      if(v1 && v1.exists && v1.data().log) v1.data().log.forEach((n,idx) => combined.push({from:n.from,for:n.for,text:n.msg||n.text,ts:n.ts,reactions:n.reactions||{}, _src:'v1', _idx:idx}));
      combined.sort((a,b) => b.ts - a.ts);
      setNotes(combined);
      setLoading(false);
    });
  }, [open, tick]);

  const toggleReaction = (note, emoji) => {
    chime([659,784]);
    const db = window.FT.db.collection('lists').doc(note._src==='v1'?'surprise_history':'notes_v2');
    db.get().then(doc => {
      if(!doc.exists) return;
      const data = doc.data();
      const log = data.log || [];
      const entry = log[note._idx];
      if(!entry) return;
      entry.reactions = entry.reactions || {};
      entry.reactions[emoji] = entry.reactions[emoji] || [];
      const i = entry.reactions[emoji].indexOf(me);
      if(i>-1) entry.reactions[emoji].splice(i,1);
      else entry.reactions[emoji].push(me);
      if(!entry.reactions[emoji].length) delete entry.reactions[emoji];
      db.set({...data, log});
      setTick(t=>t+1);
    });
  };

  return (
    <Sheet open={open} onClose={onClose} title="💌 Notes we've left">
      {loading ? (
        <div style={{textAlign:'center',padding:30,color:'var(--text3)'}}>loading…</div>
      ) : notes.length === 0 ? (
        <div style={{textAlign:'center',padding:40,color:'var(--text3)',fontFamily:'Caveat,cursive',fontSize:18}}>no notes yet — leave the first one ♡</div>
      ) : notes.map((n,i) => {
        const dt = new Date(n.ts);
        const when = dt.toLocaleDateString('en',{month:'short',day:'numeric',year:'numeric'});
        const reactions = n.reactions || {};
        const activeEmojis = Object.keys(reactions).filter(e => reactions[e]?.length);
        return (
          <div key={i} style={{padding:'14px 2px',borderBottom:'1px solid var(--border)',display:'flex',gap:10,alignItems:'flex-start'}}>
            <Avatar person={n.from} size={28}/>
            <div style={{flex:1,minWidth:0}}>
              <div style={{fontSize:11,color:'var(--text3)',marginBottom:3}}><b style={{color:'var(--text2)'}}>{n.from}</b> → {n.for} · {when}</div>
              <div style={{fontFamily:'Caveat,cursive',fontSize:17,color:'var(--text)',lineHeight:1.35,marginBottom:6}}>{n.text}</div>
              <div style={{display:'flex',gap:4,flexWrap:'wrap',alignItems:'center'}}>
                {activeEmojis.map(e => {
                  const mine = reactions[e].includes(me);
                  return (
                    <button key={e} onClick={()=>toggleReaction(n, e)} style={{fontSize:12,padding:'2px 8px',borderRadius:10,background:mine?'color-mix(in oklab,var(--rose) 18%,transparent)':'var(--card)',border:`1px solid ${mine?'var(--border-accent)':'var(--border)'}`,color:'var(--text2)',cursor:'pointer'}}>
                      {e} <span style={{fontSize:10,color:'var(--text3)',marginLeft:2}}>{reactions[e].length}</span>
                    </button>
                  );
                })}
                {REACTION_EMOJIS.filter(e => !activeEmojis.includes(e)).slice(0,4).map(e => (
                  <button key={e} onClick={()=>toggleReaction(n, e)} style={{fontSize:14,padding:'2px 6px',borderRadius:10,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',cursor:'pointer',opacity:.55}}>{e}</button>
                ))}
              </div>
            </div>
          </div>
        );
      })}
    </Sheet>
  );
}

function ThinkingReceived({ msg, me, onClose, onSendBack }){
  const [reacted, setReacted] = useState(null);
  const cardRef = useRef();
  const startY = useRef(null);
  const dy = useRef(0);

  useEffect(() => {
    if(msg){
      chime([659,784,988,1175,1319]);
      setTimeout(thinkFanfare, 100);
      setReacted(null);
    }
  }, [msg]);

  const handleTouchStart = (e) => { startY.current = e.touches[0].clientY; };
  const handleTouchMove = (e) => {
    if(startY.current == null) return;
    dy.current = Math.max(0, e.touches[0].clientY - startY.current);
    if(cardRef.current) cardRef.current.style.transform = `translateY(${dy.current}px)`;
    if(cardRef.current) cardRef.current.style.opacity = Math.max(0.2, 1 - dy.current/400);
  };
  const handleTouchEnd = () => {
    if(dy.current > 55){ onClose(); }
    else if(cardRef.current){
      cardRef.current.style.transform = 'translateY(0)';
      cardRef.current.style.opacity = '1';
    }
    startY.current = null; dy.current = 0;
  };

  const react = (emoji) => {
    // Toggle — if same reaction tapped again, remove it
    const newReacted = reacted === emoji ? null : emoji;
    setReacted(newReacted);
    if(newReacted) chime([784,988]);
    // Persist reaction on the thinking_of_you doc (for the sender's instant feedback)
    try{
      const ref = window.FT.db.collection('lists').doc('thinking_of_you');
      ref.get().then(d => {
        const data = d.exists ? d.data() : {};
        const reactions = data.reactions || {};
        Object.keys(reactions).forEach(e => {
          reactions[e] = (reactions[e]||[]).filter(n => n !== me);
          if(!reactions[e].length) delete reactions[e];
        });
        if(newReacted){
          reactions[newReacted] = reactions[newReacted] || [];
          reactions[newReacted].push(me);
        }
        ref.set({...data, reactions}, {merge:true});
      });
    }catch(e){}
    // Also persist in the activity_reactions doc (keyed by ts-who) so it shows on the history feed
    try{
      if(msg?.ts && msg?.from){
        const entryKey = `${msg.ts}-${msg.from}`;
        const aref = window.FT.db.collection('lists').doc('activity_reactions');
        aref.get().then(d => {
          const data = d.exists ? (d.data() || {}) : {};
          const entry = {...(data[entryKey] || {})};
          // Clear previous reactions by this user (one-reaction-per-thinking-note semantics)
          Object.keys(entry).forEach(e => {
            entry[e] = (entry[e]||[]).filter(n => n !== me);
            if(!entry[e].length) delete entry[e];
          });
          if(newReacted){
            entry[newReacted] = entry[newReacted] || [];
            entry[newReacted].push(me);
          }
          if(Object.keys(entry).length === 0) {
            const next = {...data}; delete next[entryKey];
            aref.set(next);
          } else {
            aref.set({...data, [entryKey]: entry});
          }
        });
      }
    }catch(e){}
    // No auto-close — let them decide whether to send one back or dismiss
  };

  if(!msg) return null;
  return (
    <div className="ft-modal-ov" onClick={onClose} style={{zIndex:150}}>
      <div
        ref={cardRef}
        className="ft-modal"
        onClick={e=>e.stopPropagation()}
        onTouchStart={handleTouchStart}
        onTouchMove={handleTouchMove}
        onTouchEnd={handleTouchEnd}
        style={{textAlign:'center',padding:'28px 24px 24px',transition:'transform .3s ease, opacity .3s ease',touchAction:'pan-y',cursor:'grab'}}
      >
        <div style={{width:40,height:4,borderRadius:99,background:'var(--border2)',margin:'0 auto 16px'}}/>
        <div style={{fontSize:62,animation:'pulse 1.4s infinite'}}>💭</div>
        <div style={{fontFamily:'Playfair Display,serif',fontSize:22,fontWeight:700,margin:'12px 0 4px'}}>{msg.from} is thinking of you</div>
        <div style={{fontFamily:'Caveat,cursive',fontSize:18,color:'var(--rose)',marginBottom:18}}>right now ♡</div>
        <div style={{display:'flex',justifyContent:'center',gap:6,marginBottom:14,flexWrap:'wrap'}}>
          {REACTION_EMOJIS.map(e => (
            <button key={e} onClick={()=>react(e)} style={{fontSize:26,padding:'6px 10px',borderRadius:12,background:reacted===e?'color-mix(in oklab,var(--rose) 25%,transparent)':'var(--card)',border:`1px solid ${reacted===e?'var(--border-accent)':'var(--border2)'}`,cursor:'pointer',transition:'all .15s',transform:reacted===e?'scale(1.15)':'scale(1)'}}>{e}</button>
          ))}
        </div>
        {reacted && (
          <div style={{fontFamily:'Caveat,cursive',fontSize:16,color:'var(--rose)',marginBottom:14,animation:'fadeInUp .3s ease'}}>
            {reacted} sent ♡
          </div>
        )}
        <div style={{display:'flex',flexDirection:'column',gap:8}}>
          <Btn variant="solid" size="md" style={{width:'100%'}} onClick={()=>{onClose(); onSendBack?.();}}>💭 {reacted ? 'Send one back too' : 'Send one back'}</Btn>
          <button onClick={onClose} style={{width:'100%',padding:'8px',background:'transparent',border:'none',color:'var(--text3)',fontSize:13,cursor:'pointer',fontFamily:'inherit'}}>
            {reacted ? 'close' : 'close without reacting'}
          </button>
        </div>
        <div style={{fontSize:10,color:'var(--text3)',marginTop:10,fontFamily:'Caveat,cursive',fontSize:14}}>swipe down or tap outside to close</div>
      </div>
    </div>
  );
}

// ---------- EDIT CATEGORY ----------
function EditCategoryModal({ open, onClose, catKey }){
  const d = useData();
  const [title, setTitle] = useState('');
  const [emoji, setEmoji] = useState('');
  useEffect(() => {
    if(open && d && d[catKey]){
      setTitle((d[catKey].title||'').replace(/^[^\w]+\s*/,''));
      setEmoji(d[catKey].emoji||'✨');
    }
  }, [open, catKey, d]);
  if(!open || !d || !d[catKey]) return null;
  const cat = d[catKey];
  const itemCount = cat.items?.length || 0;
  const emojiOpts = ['✨','🎯','🍜','🍳','🎬','📺','📚','🎵','✈️','🎲','🌸','🎨','🏃','🧘','🍷','☕','🎂','🎁','💕','🔥'];
  const save = () => {
    if(!title.trim()) return;
    FT.renameCategory(catKey, title.trim(), emoji);
    onClose();
  };
  const remove = () => {
    if(!confirm(`Delete "${title}"? ${itemCount} items will be removed. This can't be undone.`)) return;
    FT.deleteCategory(catKey);
    onClose();
  };
  return (
    <Modal open={open} onClose={onClose} title="Edit list">
      <label className="ft-label">Icon</label>
      <div style={{display:'flex',flexWrap:'wrap',gap:6,marginBottom:12}}>
        {emojiOpts.map(e => (
          <button key={e} onClick={()=>setEmoji(e)} style={{width:38,height:38,fontSize:20,borderRadius:10,background:emoji===e?'color-mix(in oklab,var(--rose) 18%,transparent)':'var(--card)',border:`1px solid ${emoji===e?'var(--border-accent)':'var(--border)'}`}}>{e}</button>
        ))}
      </div>
      <label className="ft-label">Name</label>
      <input className="ft-input" value={title} onChange={e=>setTitle(e.target.value)} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
      <div style={{fontSize:11,color:'var(--text3)',marginTop:10}}>{itemCount} items in this list</div>
      <div style={{display:'flex',gap:8,marginTop:14}}>
        <Btn variant="solid" size="md" style={{flex:1}} onClick={save}>Save</Btn>
        <Btn variant="danger" size="md" onClick={remove}>Delete</Btn>
      </div>
    </Modal>
  );
}
function ItemEditSheet({ ctx, onClose }){
  const d = useData();
  const [text, setText] = useState('');
  const [link, setLink] = useState('');
  const [where, setWhere] = useState('');
  const [note, setNote] = useState('');
  const [targetCat, setTargetCat] = useState('');
  const [tags, setTags] = useState([]);
  const [tagDraft, setTagDraft] = useState('');
  const [aiTags, setAiTags] = useState([]);
  const [aiLoading, setAiLoading] = useState(false);
  const item = ctx && d?.[ctx.cat]?.items.find(i=>i.id===ctx.id);
  useEffect(() => {
    if(item){
      setText(item.text||'');
      setLink(item.link||'');
      setWhere(item.where||'');
      setNote(item.note||'');
      setTags([...(item.tags||[])]);
      setTagDraft('');
      setTargetCat(ctx.cat);
    }
  }, [item?.id]);
  // Suggested tags from other items in the same category — must be called every render (hooks rule)
  const tagSuggestions = React.useMemo(() => {
    if(!d || !ctx) return [];
    const counts = {};
    (d[ctx.cat]?.items || []).forEach(i => {
      (i.tags || []).forEach(t => { if(!tags.includes(t)) counts[t] = (counts[t]||0)+1; });
    });
    return Object.entries(counts).sort((a,b)=>b[1]-a[1]).slice(0,8).map(e=>e[0]);
  }, [d, ctx, tags.join('|')]);
  if(!ctx || !item) return null;
  const catTitle = d[ctx.cat]?.title || ctx.cat;
  const me = localStorage.getItem('ft-v2-me') || '';
  const catEntries = Object.entries(d || {}).filter(([k,c]) => c && typeof c === 'object' && Array.isArray(c.items));
  const addTag = (t) => {
    const v = (t || tagDraft).trim();
    if(!v || tags.includes(v)) return;
    setTags([...tags, v]);
    setTagDraft('');
  };
  const removeTag = (t) => setTags(tags.filter(x => x !== t));
  const suggestAiTags = async () => {
    if(!text.trim() || aiLoading) return;
    setAiLoading(true);
    setAiTags([]);
    try{
      const catTitle = d?.[ctx.cat]?.title || '';
      const existing = tags.length ? `The user already has these tags: ${tags.join(', ')}. Suggest different ones.` : '';
      const r = await fetch('https://nudge-proxy.allurhopesndreams.workers.dev/ai', {
        method:'POST',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({
          model:'claude-haiku-4-5-20251001',
          max_tokens:200,
          system:"You suggest concise, useful tags for items on a couple's shared list. Return ONLY a JSON array of 4-6 lowercase short tags (1-2 words each). No prose, no markdown, just the array. Examples: [\"cozy\",\"rainy day\",\"comfort food\"].",
          messages:[{role:'user', content:`Item: "${text}"\nCategory: ${catTitle}\nNote: ${note||''}\nLink: ${link||''}\nWhere: ${where||''}\n${existing}\n\nSuggest tags.`}]
        })
      });
      const data = await r.json();
      let txt = (data?.content?.[0]?.text || '').trim();
      txt = txt.replace(/```json\s*|```/g,'').trim();
      const m = txt.match(/\[[\s\S]*\]/);
      if(m){
        const arr = JSON.parse(m[0]);
        if(Array.isArray(arr)) setAiTags(arr.map(t => String(t).trim()).filter(t => t && !tags.includes(t)).slice(0,6));
      }
    }catch(e){ console.warn('ai tags', e); }
    setAiLoading(false);
  };
  const save = () => {
    FT.updateItem(ctx.cat, ctx.id, {
      text: text.trim() || item.text,
      link: link.trim() || null,
      where: where.trim() || null,
      note: note.trim() || null,
      tags: tags.length ? tags : null,
    });
    if(targetCat && targetCat !== ctx.cat){
      FT.moveItem(ctx.cat, ctx.id, targetCat, me);
    }
    onClose();
  };
  return (
    <Sheet open={!!ctx} onClose={onClose} title={`✏️ Edit · ${catTitle}`}>
      <label className="ft-label">Name</label>
      <input className="ft-input" value={text} onChange={e=>setText(e.target.value)} placeholder="what is it?" autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
      <label className="ft-label" style={{marginTop:10}}>List</label>
      <select
        className="ft-input"
        value={targetCat}
        onChange={e=>setTargetCat(e.target.value)}
        style={{WebkitAppearance:'none',appearance:'none',backgroundImage:'linear-gradient(45deg, transparent 50%, var(--text2) 50%), linear-gradient(135deg, var(--text2) 50%, transparent 50%)',backgroundPosition:'calc(100% - 16px) 50%, calc(100% - 11px) 50%',backgroundSize:'5px 5px, 5px 5px',backgroundRepeat:'no-repeat',paddingRight:34}}
      >
        {catEntries.map(([k,c]) => (
          <option key={k} value={k}>{c.title || k}</option>
        ))}
      </select>
      {targetCat !== ctx.cat && (
        <div style={{fontSize:11,color:'var(--gold)',marginTop:4,lineHeight:1.4}}>
          ↳ moving to "{d[targetCat]?.title || targetCat}" — media info will re-match for the new list type
        </div>
      )}
      <label className="ft-label" style={{marginTop:10}}>Link (recipe URL, article, trailer…)</label>
      <input className="ft-input" value={link} onChange={e=>setLink(e.target.value)} placeholder="https://…" autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
      <label className="ft-label" style={{marginTop:10}}>Where / location</label>
      <input className="ft-input" value={where} onChange={e=>setWhere(e.target.value)} placeholder="neighborhood, city…" autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
      <label className="ft-label" style={{marginTop:10}}>Tags</label>
      <div style={{display:'flex',flexWrap:'wrap',gap:5,marginBottom:6,minHeight:tags.length?'auto':0}}>
        {tags.map(t => (
          <span key={t} style={{display:'inline-flex',alignItems:'center',gap:4,padding:'3px 4px 3px 10px',borderRadius:99,background:'color-mix(in oklab,var(--rose) 18%,transparent)',border:'1px solid var(--border-accent)',fontSize:12,color:'var(--text)'}}>
            {t}
            <button onClick={()=>removeTag(t)} style={{background:'none',border:'none',color:'var(--text2)',fontSize:14,padding:'0 4px',cursor:'pointer',lineHeight:1,fontFamily:'inherit'}} title="Remove">×</button>
          </span>
        ))}
      </div>
      <div style={{display:'flex',gap:6,marginBottom:6}}>
        <input className="ft-input" value={tagDraft} onChange={e=>setTagDraft(e.target.value)} onKeyDown={e=>{if(e.key==='Enter'||e.key===','){e.preventDefault();addTag();}}} placeholder="add a tag + Enter" style={{flex:1}} autoCorrect="off" autoCapitalize="none" spellCheck="false"/>
        <button onClick={()=>addTag()} disabled={!tagDraft.trim()} style={{padding:'0 14px',borderRadius:10,background:'var(--card)',border:'1px solid var(--border2)',color:'var(--text2)',fontSize:12,cursor:'pointer',opacity:tagDraft.trim()?1:.4,fontFamily:'inherit'}}>+ add</button>
        <button onClick={suggestAiTags} disabled={aiLoading||!text.trim()} style={{padding:'0 12px',borderRadius:10,background:'linear-gradient(135deg,color-mix(in oklab,var(--rose) 18%,transparent),color-mix(in oklab,var(--lavender) 18%,transparent))',border:'1px solid var(--border-accent)',color:'var(--text)',fontSize:12,cursor:'pointer',whiteSpace:'nowrap',opacity:(aiLoading||!text.trim())?.4:1,fontFamily:'inherit'}} title="Let Claude suggest tags based on the name and category">
          {aiLoading?'…':'✨ AI'}
        </button>
      </div>
      {aiTags.length > 0 && (
        <div style={{display:'flex',flexWrap:'wrap',gap:4,marginBottom:6,padding:'6px 8px',borderRadius:10,background:'linear-gradient(135deg,color-mix(in oklab,var(--rose) 06%,transparent),color-mix(in oklab,var(--lavender) 06%,transparent))',border:'1px solid var(--border-accent)'}}>
          <span style={{fontSize:10,color:'var(--lav)',textTransform:'uppercase',letterSpacing:'.1em',alignSelf:'center',fontWeight:600}}>✨ ai suggests:</span>
          {aiTags.map(t => (
            <button key={t} onClick={()=>{addTag(t); setAiTags(prev => prev.filter(x => x !== t));}} style={{padding:'3px 8px',borderRadius:99,background:'color-mix(in oklab,var(--lavender) 18%,transparent)',border:'1px solid var(--border-accent)',color:'var(--text)',fontSize:11,cursor:'pointer',fontFamily:'inherit'}}>+ {t}</button>
          ))}
          <button onClick={()=>{aiTags.forEach(t => addTag(t)); setAiTags([]);}} style={{padding:'3px 8px',borderRadius:99,background:'var(--card-strong)',border:'1px solid var(--border2)',color:'var(--text2)',fontSize:11,cursor:'pointer',fontFamily:'inherit'}}>+ all</button>
          <button onClick={()=>setAiTags([])} style={{padding:'3px 8px',borderRadius:99,background:'transparent',border:'none',color:'var(--text3)',fontSize:11,cursor:'pointer',fontFamily:'inherit'}}>×</button>
        </div>
      )}
      {tagSuggestions.length > 0 && (
        <div style={{display:'flex',flexWrap:'wrap',gap:4,marginBottom:4}}>
          <span style={{fontSize:10,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.1em',alignSelf:'center'}}>from your other items:</span>
          {tagSuggestions.map(t => (
            <button key={t} onClick={()=>addTag(t)} style={{padding:'3px 8px',borderRadius:99,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)',fontSize:11,cursor:'pointer',fontFamily:'inherit'}}>+ {t}</button>
          ))}
        </div>
      )}
      <label className="ft-label" style={{marginTop:10}}>Note</label>
      <textarea className="ft-input" value={note} onChange={e=>setNote(e.target.value)} placeholder="anything to remember…" style={{minHeight:60,resize:'vertical',fontFamily:'inherit'}} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
      <div style={{fontSize:11,color:'var(--text3)',marginTop:10,lineHeight:1.4}}>
        {/movie|tv|show/i.test(d[ctx.cat]?.title||'') || ['movies','tv','shows'].includes(ctx.cat)
          ? 'Changing the name will re-match TMDB poster & info.'
          : item.link ? 'Changing the link will refresh the preview image.' : ''}
      </div>
      <div style={{display:'flex',gap:8,marginTop:14}}>
        <Btn variant="solid" size="md" style={{flex:1}} onClick={save}>Save</Btn>
        <Btn variant="ghost" size="md" onClick={onClose}>Cancel</Btn>
      </div>
    </Sheet>
  );
}

function TmdbPickSheet({ ctx, onClose }){
  const d = useData();
  const [q, setQ] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const item = ctx && d?.[ctx.cat]?.items.find(i=>i.id===ctx.id);
  const type = item?.tmdb_type || (ctx && (ctx.cat==='movies' || /movie/i.test(d?.[ctx.cat]?.title||'') ? 'movie' : 'tv'));
  useEffect(() => {
    if(!item) return;
    setQ(item.text || '');
    search(item.text || '');
  }, [item?.id]);
  const search = async (query) => {
    if(!query?.trim()) return;
    setLoading(true);
    const r = await window.FT.tmdbSearch(type, query);
    setResults(r || []);
    setLoading(false);
  };
  const pick = (r) => {
    FT.updateItem(ctx.cat, ctx.id, {
      tmdb_id: r.id,
      tmdb_type: type,
      tmdb_poster: r.poster,
      tmdb_backdrop: r.backdrop,
      tmdb_rating: r.rating,
      tmdb_year: r.year,
      tmdb_overview: (r.overview||'').slice(0,220),
      tmdb_skip: false,
    });
    onClose();
  };
  if(!ctx || !item) return null;
  return (
    <Sheet open={!!ctx} onClose={onClose} title={`🎬 Re-match · ${type==='movie'?'Movie':'Show'}`}>
      <div style={{display:'flex',gap:8,marginBottom:12}}>
        <input className="ft-input" value={q} onChange={e=>setQ(e.target.value)} onKeyDown={e=>e.key==='Enter'&&search(q)} placeholder="search TMDB…" style={{flex:1}} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
        <Btn variant="solid" size="md" onClick={()=>search(q)} disabled={loading}>Search</Btn>
      </div>
      {loading && <div style={{textAlign:'center',color:'var(--text3)',padding:'20px 0',fontSize:13}}>searching…</div>}
      <div style={{display:'flex',flexDirection:'column',gap:8}}>
        {results.map(r => (
          <button key={r.id} onClick={()=>pick(r)} style={{display:'flex',gap:10,padding:8,background:'var(--card)',border:'1px solid var(--border)',borderRadius:10,cursor:'pointer',textAlign:'left',alignItems:'center'}}>
            {r.poster ? <img src={r.poster} alt="" style={{width:54,height:80,objectFit:'cover',borderRadius:6,flexShrink:0}}/> : <div style={{width:54,height:80,background:'var(--bg2)',borderRadius:6,display:'flex',alignItems:'center',justifyContent:'center',color:'var(--text3)',fontSize:22,flexShrink:0}}>{type==='movie'?'🎬':'📺'}</div>}
            <div style={{flex:1,minWidth:0}}>
              <div style={{fontFamily:'Playfair Display,serif',fontSize:15,fontWeight:600,color:'var(--text)',lineHeight:1.25}}>{r.title} <span style={{color:'var(--text3)',fontSize:12,fontWeight:400}}>{r.year && `· ${r.year}`}</span></div>
              {r.rating && <div style={{fontSize:11,color:'var(--rose)',marginTop:2}}>★ {r.rating}</div>}
              <div style={{fontSize:12,color:'var(--text2)',marginTop:3,lineHeight:1.4,display:'-webkit-box',WebkitLineClamp:3,WebkitBoxOrient:'vertical',overflow:'hidden'}}>{r.overview}</div>
            </div>
          </button>
        ))}
        {!loading && results.length===0 && q && <div style={{textAlign:'center',color:'var(--text3)',padding:'20px 0',fontSize:13}}>no matches — try a different title</div>}
      </div>
    </Sheet>
  );
}

function GameInfoSheet({ ctx, onClose }){
  const d = useData();
  const item = ctx && d?.[ctx.cat]?.items.find(i=>i.id===ctx.id);
  const isGame = item && (item.rawg_poster || item.rawg_metacritic || item.rawg_source || item.rawg_overview || item.rawg_year);
  if(!ctx || !item || !isGame) return null;
  const poster = item.rawg_poster;
  const mc = item.rawg_metacritic;
  const mcColor = mc ? (mc>=75?'var(--sage)':mc>=50?'var(--gold)':'var(--rose)') : 'var(--text3)';
  const sourceLabel = item.rawg_source === 'igdb' ? 'IGDB' : item.rawg_source === 'wikipedia' ? 'Wikipedia' : item.rawg_source ? item.rawg_source.toUpperCase() : '';
  return (
    <Sheet open={!!ctx} onClose={onClose} title={`🎮 ${item.text}`}>
      {poster && (
        <div style={{margin:'-6px -16px 14px',height:180,backgroundImage:`linear-gradient(to bottom, transparent 40%, var(--bg) 100%), url(${poster})`,backgroundSize:'cover',backgroundPosition:'center',position:'relative'}}/>
      )}
      <div style={{display:'flex',gap:14,marginBottom:14}}>
        {poster && <img src={poster} alt="" style={{width:100,height:130,objectFit:'cover',borderRadius:10,flexShrink:0,boxShadow:'0 6px 18px rgba(0,0,0,.18)'}}/>}
        <div style={{flex:1,minWidth:0}}>
          <div style={{fontFamily:'Playfair Display,serif',fontSize:19,fontWeight:700,color:'var(--text)',lineHeight:1.2,marginBottom:4}}>{item.text}</div>
          <div style={{fontSize:12,color:'var(--text2)',display:'flex',gap:8,flexWrap:'wrap',marginBottom:6}}>
            {item.rawg_year && <span>{item.rawg_year}</span>}
            {item.rawg_rating && <span>· ★ {item.rawg_rating}</span>}
          </div>
          {mc && (
            <div style={{display:'inline-flex',alignItems:'center',gap:6,padding:'4px 10px',background:'var(--card)',border:`1px solid ${mcColor}`,borderRadius:8,marginBottom:6}}>
              <span style={{fontSize:11,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.06em'}}>Metacritic</span>
              <span style={{fontSize:16,fontWeight:700,color:mcColor}}>{mc}</span>
            </div>
          )}
          {item.rawg_genres?.length > 0 && (
            <div style={{display:'flex',gap:4,flexWrap:'wrap',marginTop:6}}>
              {item.rawg_genres.slice(0,4).map(g => <span key={g} style={{fontSize:10.5,padding:'2px 8px',background:'var(--card)',border:'1px solid var(--border)',borderRadius:99,color:'var(--text2)'}}>{g}</span>)}
            </div>
          )}
        </div>
      </div>
      {item.rawg_platforms?.length > 0 && (
        <div style={{marginBottom:14}}>
          <div className="ft-section-label">available on</div>
          <div style={{display:'flex',gap:4,flexWrap:'wrap'}}>
            {item.rawg_platforms.map(p => <span key={p} style={{fontSize:11,padding:'3px 9px',background:'var(--card-strong)',border:'1px solid var(--border)',borderRadius:99,color:'var(--text2)'}}>{p}</span>)}
          </div>
        </div>
      )}
      {item.rawg_overview && (
        <div style={{fontSize:13.5,lineHeight:1.6,color:'var(--text)',marginBottom:18}}>{item.rawg_overview}</div>
      )}
      <div style={{display:'flex',gap:8,marginTop:10,flexWrap:'wrap',alignItems:'center'}}>
        <Btn variant="ghost" size="sm" onClick={()=>window.open(`https://www.google.com/search?q=${encodeURIComponent(item.text+' game')}`,'_blank')}>🔎 Search</Btn>
        <Btn variant="ghost" size="sm" onClick={()=>window.open(`https://store.steampowered.com/search/?term=${encodeURIComponent(item.text)}`,'_blank')}>🎮 Steam</Btn>
        {item.link && <Btn variant="ghost" size="sm" onClick={()=>window.open(item.link,'_blank')}>🔗 Link</Btn>}
        {sourceLabel && <span style={{fontSize:10,color:'var(--text3)',marginLeft:'auto'}}>via {sourceLabel}</span>}
      </div>
    </Sheet>
  );
}

function TmdbInfoSheet({ ctx, onClose }){
  const d = useData();
  const [details, setDetails] = useState(null);
  const [loading, setLoading] = useState(false);
  const item = ctx && d?.[ctx.cat]?.items.find(i=>i.id===ctx.id);
  // Route games to GameInfoSheet; null out if this isn't a TMDB item
  const isGame = item && (item.rawg_poster || item.rawg_metacritic || item.rawg_source || item.rawg_overview);
  const isTmdb = item && (item.tmdb_id && item.tmdb_type);
  useEffect(() => {
    if(!item || !isTmdb){ setDetails(null); return; }
    setLoading(true);
    setDetails(null);
    window.FT.tmdbDetails(item.tmdb_type, item.tmdb_id).then(d => {
      setDetails(d);
      setLoading(false);
    });
  }, [item?.tmdb_id]);
  if(!ctx || !item) return null;
  if(isGame) return null; // handled by GameInfoSheet
  if(!isTmdb) return null;
  const poster = item.tmdb_poster || details?.poster;
  const backdrop = item.tmdb_backdrop;
  return (
    <Sheet open={!!ctx} onClose={onClose} title={`${item.tmdb_type==='movie'?'🎬':'📺'} ${item.text}`}>
      {backdrop && (
        <div style={{margin:'-6px -16px 14px',height:160,backgroundImage:`url(${backdrop})`,backgroundSize:'cover',backgroundPosition:'center',borderRadius:0,position:'relative'}}>
          <div style={{position:'absolute',inset:0,background:'linear-gradient(to bottom, transparent 40%, var(--bg) 100%)'}}/>
        </div>
      )}
      <div style={{display:'flex',gap:14,marginBottom:14}}>
        {poster && <img src={poster} alt="" style={{width:100,height:150,objectFit:'cover',borderRadius:10,flexShrink:0,boxShadow:'0 6px 18px rgba(0,0,0,.18)'}}/>}
        <div style={{flex:1,minWidth:0}}>
          <div style={{fontFamily:'Playfair Display,serif',fontSize:19,fontWeight:700,color:'var(--text)',lineHeight:1.2,marginBottom:4}}>{details?.title || item.text}</div>
          {details?.tagline && <div style={{fontSize:12,fontStyle:'italic',color:'var(--text3)',marginBottom:6}}>"{details.tagline}"</div>}
          <div style={{fontSize:12,color:'var(--text2)',display:'flex',gap:8,flexWrap:'wrap',marginBottom:4}}>
            {item.tmdb_year && <span>{item.tmdb_year}</span>}
            {details?.runtime && <span>· {details.runtime} min</span>}
            {details?.seasons && <span>· {details.seasons} season{details.seasons>1?'s':''}</span>}
            {details?.episodes && <span>· {details.episodes} eps</span>}
          </div>
          {item.tmdb_rating && <div style={{fontSize:13,color:'var(--rose)',fontWeight:600}}>★ {item.tmdb_rating}<span style={{color:'var(--text3)',fontWeight:400,fontSize:11,marginLeft:4}}>{details?.voteCount?`(${details.voteCount.toLocaleString()} votes)`:''}</span></div>}
          {details?.genres?.length>0 && (
            <div style={{display:'flex',gap:4,flexWrap:'wrap',marginTop:6}}>
              {details.genres.map(g => <span key={g} style={{fontSize:10.5,padding:'2px 8px',background:'var(--card)',border:'1px solid var(--border)',borderRadius:99,color:'var(--text2)'}}>{g}</span>)}
            </div>
          )}
        </div>
      </div>
      {loading && <div style={{textAlign:'center',color:'var(--text3)',padding:'20px 0',fontSize:13}}>loading details…</div>}
      {(details?.overview || item.tmdb_overview) && (
        <div style={{fontSize:13.5,lineHeight:1.6,color:'var(--text)',marginBottom:18}}>{details?.overview || item.tmdb_overview}</div>
      )}
      {details?.crew?.length>0 && (
        <div style={{marginBottom:14}}>
          <div className="ft-section-label">{item.tmdb_type==='movie'?'directed by':'created by'}</div>
          <div style={{fontSize:13,color:'var(--text2)'}}>
            {details.crew.map((p,i) => <span key={i}>{i>0?', ':''}{p.name} <span style={{color:'var(--text3)',fontSize:11}}>({p.job})</span></span>)}
          </div>
        </div>
      )}
      {details?.cast?.length>0 && (
        <div style={{marginBottom:14}}>
          <div className="ft-section-label">cast</div>
          <div style={{display:'flex',gap:8,overflowX:'auto',paddingBottom:4,scrollbarWidth:'none',WebkitOverflowScrolling:'touch'}}>
            {details.cast.map((p,i) => (
              <div key={i} style={{flexShrink:0,width:72,textAlign:'center'}}>
                {p.photo ? <img src={p.photo} alt="" style={{width:64,height:64,borderRadius:'50%',objectFit:'cover',margin:'0 auto 4px',display:'block'}}/> : <div style={{width:64,height:64,borderRadius:'50%',background:'var(--card)',border:'1px solid var(--border)',margin:'0 auto 4px',display:'flex',alignItems:'center',justifyContent:'center',color:'var(--text3)',fontSize:20}}>{p.name?.[0]||'?'}</div>}
                <div style={{fontSize:10.5,color:'var(--text)',fontWeight:500,lineHeight:1.2}}>{p.name}</div>
                <div style={{fontSize:9.5,color:'var(--text3)',lineHeight:1.2,marginTop:1}}>{p.character}</div>
              </div>
            ))}
          </div>
        </div>
      )}
      <div style={{display:'flex',gap:8,marginTop:10,flexWrap:'wrap'}}>
        {details?.trailerKey && <Btn variant="ghost" size="sm" onClick={()=>{const a=document.createElement('a');a.href=`https://youtu.be/${details.trailerKey}`;a.target='_blank';a.rel='noopener noreferrer';document.body.appendChild(a);a.click();a.remove();}}>▶ Trailer</Btn>}
        <Btn variant="ghost" size="sm" onClick={()=>{const a=document.createElement('a');a.href=`https://www.themoviedb.org/${item.tmdb_type}/${item.tmdb_id}`;a.target='_blank';a.rel='noopener noreferrer';document.body.appendChild(a);a.click();a.remove();}}>TMDB</Btn>
        <Btn variant="ghost" size="sm" onClick={()=>{onClose(); setTimeout(()=>window.FTAPP?.pickTmdb?.(ctx.cat, ctx.id), 150);}}>🔄 Re-match</Btn>
      </div>
    </Sheet>
  );
}

// ---------- SIGN-IN SCREEN (email magic link) ----------
// Shown when there's no authenticated Firebase user. User enters an email; we send a
// one-tap sign-in link via Firebase Auth (passwordless email link). When the user
// taps the link in their inbox, they return to the same URL with magic-link params,
// which AppShell detects and completes via signInWithEmailLink().
function SignInScreen(){
  const [email, setEmail] = useState(localStorage.getItem('ft-signin-email') || '');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const [sent, setSent] = useState(false);

  const send = async () => {
    const e = (email||'').trim().toLowerCase();
    if(!e || !e.includes('@')){ setErr('Enter a real email.'); return; }
    setBusy(true); setErr('');
    try{
      await firebase.auth().sendSignInLinkToEmail(e, {
        url: window.location.origin + window.location.pathname,
        handleCodeInApp: true,
      });
      localStorage.setItem('ft-signin-email', e);
      setSent(true);
    }catch(er){
      setErr(er.message || 'Could not send the sign-in link. Try again.');
    }finally{ setBusy(false); }
  };

  if(sent){
    return (
      <div style={{minHeight:'100dvh',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',padding:'24px',background:'var(--bg)',color:'var(--text)',fontFamily:'Inter Tight, system-ui, sans-serif'}}>
        <div style={{maxWidth:420,width:'100%',textAlign:'center'}}>
          <div style={{fontSize:56,marginBottom:8}}>📬</div>
          <h1 style={{fontFamily:'Playfair Display, serif',fontWeight:700,fontSize:30,margin:'0 0 8px'}}>Check your email</h1>
          <p style={{color:'var(--text2)',fontSize:15,lineHeight:1.5,margin:'0 0 24px'}}>We sent a sign-in link to <b style={{color:'var(--text)'}}>{email}</b>. Tap it from this same device to come back signed in.</p>
          <button onClick={()=>{ setSent(false); setErr(''); }} style={{padding:'12px 22px',fontSize:14,fontWeight:600,fontFamily:'inherit',border:'1px solid var(--border2)',borderRadius:12,background:'var(--card)',color:'var(--text)',cursor:'pointer'}}>Use a different email</button>
        </div>
      </div>
    );
  }

  return (
    <div style={{minHeight:'100dvh',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',padding:'24px',background:'var(--bg)',color:'var(--text)',fontFamily:'Inter Tight, system-ui, sans-serif'}}>
      <div style={{maxWidth:420,width:'100%',textAlign:'center'}}>
        <div style={{fontSize:64,marginBottom:8,background:'linear-gradient(135deg,#F2B988,#E8A5C0,#B9A4E3)',WebkitBackgroundClip:'text',backgroundClip:'text',color:'transparent'}}>♥</div>
        <h1 style={{fontFamily:'Playfair Display, serif',fontWeight:700,fontSize:32,margin:'0 0 8px'}}>Welcome to Fun-Ture</h1>
        <p style={{color:'var(--text2)',fontSize:15,lineHeight:1.5,margin:'0 0 28px'}}>Enter your email — we'll send a one-tap sign-in link. No password to remember.</p>
        <input
          type="email"
          value={email}
          onChange={e=>setEmail(e.target.value)}
          onKeyDown={e=>{ if(e.key==='Enter') send(); }}
          placeholder="you@example.com"
          autoFocus
          autoCapitalize="off"
          autoCorrect="off"
          spellCheck={false}
          inputMode="email"
          style={{width:'100%',padding:'16px 18px',fontSize:17,fontFamily:'inherit',border:'1px solid var(--border2)',borderRadius:14,background:'var(--card)',color:'var(--text)',outline:'none',marginBottom:12,boxSizing:'border-box',textAlign:'center'}}
        />
        {err && <div style={{color:'#E8A5C0',fontSize:13,marginBottom:12}}>{err}</div>}
        <button
          onClick={send}
          disabled={busy}
          style={{width:'100%',padding:'16px',fontSize:16,fontWeight:600,fontFamily:'inherit',border:'none',borderRadius:14,background:'linear-gradient(135deg,#F2B988,#E8A5C0,#B9A4E3)',color:'#fff',cursor:'pointer',opacity:busy?0.6:1,transition:'opacity .2s'}}
        >{busy ? 'Sending…' : 'Send sign-in link'}</button>
        <p style={{color:'var(--text2)',fontSize:12,lineHeight:1.5,marginTop:20,opacity:0.8}}>You and your partner each sign in with your own email. After that, you'll join (or create) your shared pair.</p>
      </div>
    </div>
  );
}

// ---------- FIRST-RUN PAIR-CODE SETUP ----------
// Shown when FT._needsSetup is true (no storage mode chosen yet). User picks/enters a
// pair code, which activates 'shared' mode and reloads the app so data.js initializes.
function PairCodeSetup(){
  const [code, setCode] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const submit = () => {
    const norm = window.FTStorage.normalizeRoomCode(code);
    if(norm.length < 4){ setErr('Pick something at least 4 characters — like your names stuck together.'); return; }
    setBusy(true); setErr('');
    try{
      window.FTStorage.setMode({ mode: 'shared', roomCode: norm });
      setTimeout(()=>window.location.reload(), 300);
    }catch(e){ setErr(e.message||'Something went wrong'); setBusy(false); }
  };
  return (
    <div style={{minHeight:'100dvh',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',padding:'24px',background:'var(--bg)',color:'var(--text)',fontFamily:'Inter Tight, system-ui, sans-serif'}}>
      <div style={{maxWidth:420,width:'100%',textAlign:'center'}}>
        <div style={{fontSize:64,marginBottom:8,background:'linear-gradient(135deg,#F2B988,#E8A5C0,#B9A4E3)',WebkitBackgroundClip:'text',backgroundClip:'text',color:'transparent'}}>♥</div>
        <h1 style={{fontFamily:'Playfair Display, serif',fontWeight:700,fontSize:32,margin:'0 0 8px'}}>Welcome to Fun-Ture</h1>
        <p style={{color:'var(--text2)',fontSize:15,lineHeight:1.5,margin:'0 0 28px'}}>Make a pair code with your partner. It's whatever you want — a nickname, a word, a date. Both of you enter the same thing on your own phones and your lists sync.</p>
        <input
          type="text"
          value={code}
          onChange={e=>setCode(e.target.value)}
          onKeyDown={e=>{ if(e.key==='Enter') submit(); }}
          placeholder="our-secret-word"
          autoFocus
          autoCapitalize="off"
          autoCorrect="off"
          spellCheck={false}
          style={{width:'100%',padding:'16px 18px',fontSize:17,fontFamily:'inherit',border:'1px solid var(--border2)',borderRadius:14,background:'var(--card)',color:'var(--text)',outline:'none',marginBottom:12,boxSizing:'border-box',textAlign:'center'}}
        />
        {err && <div style={{color:'#E8A5C0',fontSize:13,marginBottom:12}}>{err}</div>}
        <button
          onClick={submit}
          disabled={busy}
          style={{width:'100%',padding:'16px',fontSize:16,fontWeight:600,fontFamily:'inherit',border:'none',borderRadius:14,background:'linear-gradient(135deg,#F2B988,#E8A5C0,#B9A4E3)',color:'#fff',cursor:'pointer',opacity:busy?0.6:1,transition:'opacity .2s'}}
        >{busy ? 'Connecting…' : "Let's go"}</button>
        <p style={{color:'var(--text2)',fontSize:12,lineHeight:1.5,marginTop:20,opacity:0.8}}>Case doesn't matter. Spaces become dashes. Lists, photos, and love notes are shared between anyone with the same code — so keep it to just the two of you.</p>
      </div>
    </div>
  );
}

// ---------- PARTNER NAMES SETUP ----------
// Shown once per pair code, after pair-code entry, when nobody has written the two
// partner names yet. Writes to lists/pair_info under the pair-code's namespace.
function PartnerNamesSetup(){
  const [n1, setN1] = useState('');
  const [n2, setN2] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const submit = async () => {
    const a = n1.trim(), b = n2.trim();
    if(!a || !b){ setErr('Enter both names.'); return; }
    if(a.toLowerCase() === b.toLowerCase()){ setErr('Pick two different names.'); return; }
    setBusy(true); setErr('');
    try{
      await FT.savePartners([a, b]);
      setTimeout(()=>window.location.reload(), 500);
    }catch(e){ setErr(e.message||'Something went wrong'); setBusy(false); }
  };
  const inputStyle = {width:'100%',padding:'16px 18px',fontSize:17,fontFamily:'inherit',border:'1px solid var(--border2)',borderRadius:14,background:'var(--card)',color:'var(--text)',outline:'none',marginBottom:10,boxSizing:'border-box',textAlign:'center'};
  return (
    <div style={{minHeight:'100dvh',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',padding:'24px',background:'var(--bg)',color:'var(--text)',fontFamily:'Inter Tight, system-ui, sans-serif'}}>
      <div style={{maxWidth:420,width:'100%',textAlign:'center'}}>
        <div style={{fontSize:56,marginBottom:8}}>🌱</div>
        <h1 style={{fontFamily:'Playfair Display, serif',fontWeight:700,fontSize:30,margin:'0 0 8px'}}>Who are you two?</h1>
        <p style={{color:'var(--text2)',fontSize:15,lineHeight:1.5,margin:'0 0 24px'}}>Just your first names (or nicknames). Once set, you'll see these everywhere in the app.</p>
        <input type="text" value={n1} onChange={e=>setN1(e.target.value)} placeholder="Your name" autoFocus autoCapitalize="words" style={inputStyle}/>
        <input type="text" value={n2} onChange={e=>setN2(e.target.value)} placeholder="Your partner's name" autoCapitalize="words" onKeyDown={e=>{ if(e.key==='Enter') submit(); }} style={inputStyle}/>
        {err && <div style={{color:'#E8A5C0',fontSize:13,marginBottom:10,marginTop:6}}>{err}</div>}
        <button onClick={submit} disabled={busy} style={{width:'100%',padding:'16px',fontSize:16,fontWeight:600,fontFamily:'inherit',border:'none',borderRadius:14,background:'linear-gradient(135deg,#F2B988,#E8A5C0,#B9A4E3)',color:'#fff',cursor:'pointer',marginTop:4,opacity:busy?0.6:1,transition:'opacity .2s'}}>{busy ? 'Setting up…' : "That's us"}</button>
      </div>
    </div>
  );
}

// ---------- BOOT SHELL ----------
// Picks between sign-in, setup screens, and the full app based on auth + storage state.
function AppShell(){
  const [authReady, setAuthReady] = useState(false);
  const [user, setUser] = useState(null);
  const [partners, setPartners] = useState(FT.partners || []);
  const [waited, setWaited] = useState(false);

  // Auth bootstrap: handle returning email-link sign-in, then watch auth state.
  useEffect(() => {
    let auth;
    try { auth = firebase.auth(); }
    catch(e){ console.error('firebase.auth() unavailable', e); setAuthReady(true); return; }

    // If the URL is an email-magic-link callback, complete sign-in.
    if(auth.isSignInWithEmailLink(window.location.href)){
      let email = localStorage.getItem('ft-signin-email');
      if(!email) email = window.prompt('Confirm the email you signed in with:');
      if(email){
        auth.signInWithEmailLink(email, window.location.href)
          .then(() => {
            localStorage.removeItem('ft-signin-email');
            // Strip link params from URL so a refresh doesn't loop
            window.history.replaceState({}, document.title, window.location.pathname);
          })
          .catch(err => {
            console.error('Sign-in failed:', err);
            window.alert('Sign-in failed: ' + (err.message || err.code || 'unknown error'));
          });
      }
    }

    const unsub = auth.onAuthStateChanged(u => {
      setUser(u);
      setAuthReady(true);
    });
    return unsub;
  }, []);

  // Partner-name subscription only kicks in once we have a user AND a storage mode.
  useEffect(() => {
    if(!user) return;
    if(window.FT._needsSetup) return;
    const unsub = FT.subscribePartners ? FT.subscribePartners(setPartners) : null;
    const t = setTimeout(() => setWaited(true), 900);
    return () => { if(unsub) unsub(); clearTimeout(t); };
  }, [user]);

  if(!authReady) return null; // initial auth check — splash
  if(!user) return <SignInScreen/>;
  if(window.FT && window.FT._needsSetup) return <PairCodeSetup/>;
  if(partners.length < 2){
    if(!waited) return null; // quick loading gap while the first snapshot lands
    return <PartnerNamesSetup/>;
  }
  return <App/>;
}

function App(){
  const d = useData();
  const partners = usePartners();
  const [ready, setReady] = useState(false);
  const [tab, setTab] = useState('home');
  const [me, setMe] = useState(localStorage.getItem('ft-v2-me') || null);
  const [focusCat, setFocusCat] = useState(null);
  const [focusId, setFocusId] = useState(null);

  const [addOpen, setAddOpen] = useState(false);
  const [commentCtx, setCommentCtx] = useState(null);
  const [reactCtx, setReactCtx] = useState(null);
  const [thinkOpen, setThinkOpen] = useState(false);
  const [noteOpen, setNoteOpen] = useState(false);
  const [notePrefill, setNotePrefill] = useState('');
  const [dnOpen, setDnOpen] = useState(false);
  const [aiOpen, setAiOpen] = useState(false);
  const [gapOpen, setGapOpen] = useState(false);
  const [smartSearchCtx, setSmartSearchCtx] = useState(null); // { query } or null
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [notesOpen, setNotesOpen] = useState(false);
  const [feedbackOpen, setFeedbackOpen] = useState(false);
  const [totOpen, setTotOpen] = useState(false);
  const [surpriseOpen, setSurpriseOpen] = useState(false);
  const [editCat, setEditCat] = useState(null);
  const [thinkMsg, setThinkMsg] = useState(null);
  const [editItemCtx, setEditItemCtx] = useState(null); // {cat, id}
  const [pickTmdbCtx, setPickTmdbCtx] = useState(null); // {cat, id}
  const [infoCtx, setInfoCtx] = useState(null); // {cat, id}
  const [photoCtx, setPhotoCtx] = useState(null); // {cat, id}
  const [activityOpen, setActivityOpen] = useState(false);

  useEffect(() => {
    const mode = localStorage.getItem('ft-v2-theme-mode') || 'auto';
    let resolved = mode;
    if(mode === 'auto'){
      resolved = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    } else {
      // back-compat: mode might be missing on first run but legacy theme key exists
      resolved = localStorage.getItem('ft-v2-theme') || resolved;
    }
    document.documentElement.setAttribute('data-theme', resolved);
  }, []);

  // Minimum splash time (feels intentional, not jarring)
  const appStart = useRef(Date.now()).current;
  useEffect(() => {
    if(d && me){
      const elapsed = Date.now() - appStart;
      const wait = Math.max(0, 3000 - elapsed);
      const t = setTimeout(()=>setReady(true), wait);
      return () => clearTimeout(t);
    }
  }, [d, me]);

  useEffect(() => {
    if(!me) return;
    const unsub = FT.subscribeThinking(msg => {
      if(msg && msg.for && msg.for.toLowerCase() === me.toLowerCase()){
        // only show if recent
        if(Date.now() - msg.ts < 1000*60*60*12){
          setThinkMsg(msg);
          // Fire OS-level notification via service worker (works when app in bg)
          try{
            if('serviceWorker' in navigator && 'Notification' in window && Notification.permission === 'granted'){
              navigator.serviceWorker.ready.then(reg => {
                reg.showNotification(`${msg.from} is thinking of you ♡`, {
                  body: 'Tap to see their thought',
                  icon: '/icon-192.png',
                  badge: '/icon-192.png',
                  tag: 'thinking-'+msg.ts,
                  vibrate: [120, 60, 120, 60, 300],
                  requireInteraction: false,
                });
              });
            }
          }catch(_){}
        }
      }
    });
    return unsub;
  }, [me]);

  // Subscribe to save errors (e.g. photos exceeding Firestore's 1MB doc limit) and surface them to the user.
  useEffect(() => {
    if(!window.FT?.subscribeSaveError) return;
    const unsub = window.FT.subscribeSaveError((err) => {
      const msg = err?.message || 'Save failed. Try again or remove a recent photo.';
      if(window.FT?.showPushToast) window.FT.showPushToast('⚠️ ' + msg);
      else alert(msg);
    });
    return unsub;
  }, []);

  useEffect(() => {
    window.FTAPP = {
      buildListContext,
      openThinking: () => setThinkOpen(true),
      openNote: (prefill) => { setNotePrefill(prefill || ''); setNoteOpen(true); },
      openDateNight: () => setDnOpen(true),
      openAI: (prompt) => {
        if(prompt) window._pendingAIPrompt = prompt;
        setAiOpen(true);
      },
      openGapFinder: () => setGapOpen(true),
      openSmartSearch: (query) => setSmartSearchCtx({ query: query || '' }),
      openThisOrThat: () => setTotOpen(true),
      openSurprise: () => setSurpriseOpen(true),
      openNotesHistory: () => setNotesOpen(true),
      openFeedback: () => setFeedbackOpen(true),
      openSettings: () => setSettingsOpen(true),
      addComment: (cat, id) => setCommentCtx({cat, id}),
      openReact: (cat, id) => setReactCtx({cat, id}),
      editCategory: (cat) => setEditCat(cat),
      editItem: (cat, id) => setEditItemCtx({cat, id}),
      pickTmdb: (cat, id) => setPickTmdbCtx({cat, id}),
      moreInfo: (cat, id) => setInfoCtx({cat, id}),
      addPhoto: (cat, id) => setPhotoCtx({cat, id}),
      openActivity: () => setActivityOpen(true),
      navigate: (targetTab, cat, id) => {
        setTab(targetTab || 'list');
        if(cat) setFocusCat(cat);
        if(id) setFocusId(id);
      },
      burst: burstAt,
    };
  }, []);

  // Re-register push on sign-in (silent — only if already granted)
  useEffect(() => {
    if(!me) return;
    if(typeof Notification==='undefined') return;
    if(Notification.permission==='granted'){
      window.FT?.setupPush?.(false);
    }
  }, [me]);

  // One-time TMDB backfill for movies/tv items missing posters
  useEffect(() => {
    if(!d || !me) return;
    const key = 'ft-v2-tmdb-backfill-done-v6';
    if(sessionStorage.getItem(key)) return;
    const targets = [];
    const gameTargets = [];
    const recipeTargets = [];
    const nudgeTargets = [];
    // Find movie/tv style categories by key OR title
    Object.keys(d).forEach(k => {
      const cat = d[k]; if(!cat || !cat.items) return;
      const title = (cat.title||'').toLowerCase();
      const isMovie = k==='movies' || k==='movie' || /movie|film|halloween|christmas|holiday|spooky|horror/.test(title);
      const isTv = k==='tv' || k==='shows' || k==='show' || /\btv\b|show|series|stream|watch|binge/.test(title);
      const isGame = k==='games' || k==='videogames' || k==='video_games' || /video ?game|gaming\b|console|\bps\d|xbox|nintendo|steam/.test(title);
      const isRecipe = k==='recipes' || k==='cook' || /recipe|cook|food|kitchen|eat/.test(title);
      // Nudge backfill: ANY item with a nudgetext.com link but no .nudge.photo
      cat.items.forEach(it => {
        if(it.link && /nudgetext\.com/i.test(it.link) && !it.nudge?.photo && !it.nudge_skip){
          nudgeTargets.push({cat:k, id:it.id});
        }
      });
      if(isMovie || isTv){
        cat.items.forEach(it => {
          const hasFull = it.tmdb_overview && it.tmdb_rating && it.tmdb_year;
          if(!hasFull && !it.tmdb_skip){
            targets.push({cat:k, id:it.id, kind:'tmdb'});
          }
        });
      } else if(isGame){
        cat.items.forEach(it => {
          const hasPoster = it.rawg_poster || it.rawg_backdrop;
          if(!hasPoster && !it.rawg_skip){
            gameTargets.push({cat:k, id:it.id, kind:'rawg'});
          }
        });
      } else if(isRecipe){
        cat.items.forEach(it => {
          if(it.link && !it.og_image && !it.og_skip){
            recipeTargets.push({cat:k, id:it.id, kind:'recipe'});
          }
        });
      }
    });
    const totalTargets = targets.length + gameTargets.length + recipeTargets.length + nudgeTargets.length;
    console.log('[FT backfill] targets:', {tmdb:targets.length, games:gameTargets.length, recipe:recipeTargets.length, nudge:nudgeTargets.length});
    if(!totalTargets) return; // don't lock the session guard if nothing to do — allows retry after adding items
    sessionStorage.setItem(key, '1');
    // Rate-limit: spread requests
    targets.slice(0,40).forEach((t,i) => {
      setTimeout(()=>window.FT?.tmdbAutoEnrich?.(t.cat, t.id), 400*i + 1500);
    });
    gameTargets.slice(0,30).forEach((t,i) => {
      setTimeout(()=>window.FT?.rawgAutoEnrich?.(t.cat, t.id), 500*i + 2500 + targets.length*400);
    });
    recipeTargets.slice(0,20).forEach((t,i) => {
      setTimeout(()=>window.FT?.recipeEnrich?.(t.cat, t.id), 500*i + 3500 + targets.length*400 + gameTargets.length*500);
    });
    // Nudge enrichment — inline since data.js doesn't expose a helper yet
    // Retries once on transient failures (network/timeout); only sets nudge_skip after 2 consecutive failures.
    // Empty payloads do NOT set skip — next session can try again in case nudgetext.com had a bad response.
    nudgeTargets.slice(0,20).forEach((t,i) => {
      const attempt = async (tryN) => {
        try{
          const cat = window.FT?.data?.[t.cat]; if(!cat) return;
          const it = cat.items.find(x => x.id===t.id); if(!it || it.nudge?.photo) return;
          const resp = await fetch(`https://nudge-proxy.allurhopesndreams.workers.dev/?url=${encodeURIComponent(it.link)}`, {signal: AbortSignal.timeout(12000)});
          if(!resp.ok) throw new Error('HTTP '+resp.status);
          const data = await resp.json();
          if(!data || data.error) throw new Error(data?.error || 'no data');
          const trimmed = {};
          ['photo','neighborhood','rating','reviewCount','hours','description','nudgeText','address','nearby','tags'].forEach(k => {
            if(data[k] != null && data[k] !== '') trimmed[k] = data[k];
          });
          if(Object.keys(trimmed).length){
            window.FT?.updateItem?.(t.cat, t.id, { nudge: { ...(it.nudge||{}), ...trimmed } });
          }
          // Empty trimmed → leave as-is; next session will retry
        }catch(e){
          console.warn('[FT nudge backfill] attempt', tryN, 'for', t.id, e?.message||e);
          if(tryN < 2){
            setTimeout(()=>attempt(tryN+1), 3000);
          } else {
            window.FT?.updateItem?.(t.cat, t.id, { nudge_skip: true });
          }
        }
      };
      setTimeout(()=>attempt(1), 600*i + 4500 + targets.length*400 + gameTargets.length*500 + recipeTargets.length*500);
    });
  }, [d, me]);

  // Pull-to-refresh
  useEffect(() => {
    let startY=0, pulling=false, triggered=false, rafId=null, lastDy=0;
    let cancelled=false; // becomes true if user scrolls down during gesture
    const threshold = 280;
    const DEAD_ZONE = 110; // must pull this far before pill starts animating
    const ind = () => document.getElementById('ft-pull-indicator');
    const icon = () => document.getElementById('ft-pull-icon');
    const label = () => document.getElementById('ft-pull-label');

    const applyPull = (dy) => {
      const el=ind(); const ic=icon(); const lb=label();
      if(!el) return;
      if(dy < DEAD_ZONE){
        // hidden state
        el.style.opacity='0';
        el.style.transform='translateX(-50%) translateY(-24px) scale(.9)';
        return;
      }
      const effective = dy - DEAD_ZONE;
      const thresholdEff = threshold - DEAD_ZONE;
      const prog = Math.min(1, effective/thresholdEff);
      // rubber-band translate: fast start, slow near limit
      const tY = -24 + Math.min(48, 48 * (1 - Math.exp(-effective/45)));
      const scale = 0.9 + prog*0.12;
      el.style.opacity = Math.min(1, effective/20);
      el.style.transform = `translate3d(-50%, ${tY}px, 0) scale(${scale})`;
      const atThreshold = effective > thresholdEff;
      if(ic){
        ic.style.transform = `rotate(${atThreshold?180:prog*180}deg)`;
        ic.style.color = atThreshold ? 'var(--rose)' : 'var(--text2)';
      }
      if(lb){
        const want = atThreshold ? 'release to refresh' : 'pull to refresh';
        if(lb.textContent !== want) lb.textContent = want;
        lb.style.color = atThreshold ? 'var(--rose)' : 'var(--text2)';
      }
      el.style.borderColor = atThreshold ? 'var(--border-accent)' : 'var(--border2)';
      el.style.background = atThreshold ? 'color-mix(in oklab,var(--rose) 14%,transparent)' : 'var(--card)';
      triggered = atThreshold;
    };

    const schedule = () => {
      if(rafId != null) return;
      rafId = requestAnimationFrame(() => {
        rafId = null;
        if(pulling && !cancelled) applyPull(lastDy);
      });
    };

    const hide = (withTransition = true) => {
      const el = ind();
      if(!el) return;
      if(withTransition) el.style.transition = 'opacity .28s cubic-bezier(.4,0,.2,1), transform .28s cubic-bezier(.4,0,.2,1), background .2s, border-color .2s';
      el.style.opacity='0';
      el.style.transform='translate3d(-50%, -24px, 0) scale(.9)';
      if(withTransition){
        setTimeout(()=>{ if(el) el.style.transition = ''; }, 300);
      }
    };

    const onStart = (e) => {
      if(e.touches.length > 1) return;
      // only arm if at top, no open overlays, and not from inside a scrollable sheet
      if(window.scrollY > 3) return;
      if(document.querySelector('.ft-sheet-ov, .ft-modal-ov')) return;
      startY = e.touches[0].clientY;
      pulling = true;
      triggered = false;
      cancelled = false;
      lastDy = 0;
      // disable transition for smooth tracking during drag
      const el = ind();
      if(el) el.style.transition = 'background .2s, border-color .2s';
    };

    const onMove = (e) => {
      if(!pulling || cancelled) return;
      if(e.touches.length > 1){ cancelled = true; hide(); return; }
      const dy = e.touches[0].clientY - startY;
      // if user is scrolling UP (dy < 0) just cancel without flicker
      if(dy < -8){ cancelled = true; hide(); return; }
      lastDy = Math.max(0, dy);
      schedule();
    };

    const onEnd = () => {
      if(!pulling) return;
      pulling = false;
      if(rafId != null){ cancelAnimationFrame(rafId); rafId = null; }
      const el = ind();
      if(triggered && !cancelled){
        const lb=label(); const ic=icon();
        if(lb) lb.textContent = 'refreshing…';
        if(ic) ic.style.animation = 'spin 0.8s linear infinite';
        if(el){
          el.style.transition = 'transform .35s cubic-bezier(.4,0,.2,1), background .2s';
          el.style.transform = 'translate3d(-50%, 22px, 0) scale(1)';
        }
        setTimeout(()=>window.location.reload(), 400);
      } else {
        hide();
      }
    };

    document.addEventListener('touchstart', onStart, {passive:true});
    document.addEventListener('touchmove', onMove, {passive:true});
    document.addEventListener('touchend', onEnd, {passive:true});
    document.addEventListener('touchcancel', onEnd, {passive:true});
    return () => {
      document.removeEventListener('touchstart', onStart);
      document.removeEventListener('touchmove', onMove);
      document.removeEventListener('touchend', onEnd);
      document.removeEventListener('touchcancel', onEnd);
      if(rafId != null) cancelAnimationFrame(rafId);
    };
  }, []);

  // Choose user
  if(!me){
    const signOutFromPicker = () => {
      if(!confirm('Sign out of this pair code? Your lists stay in the cloud — you can come back any time by entering the code again.')) return;
      try { window.FTStorage?.clearMode?.(); } catch(_){}
      try { localStorage.removeItem('ft-v2-me'); } catch(_){}
      window.location.reload();
    };
    return (
      <>
        <div className="ft-splash">
          <div className="ft-splash-heart">♥</div>
          <div className="ft-splash-title">Who's this?</div>
          <div className="ft-splash-sub">pick one to sign in</div>
          <div style={{display:'flex',gap:18,marginTop:26}}>
            {partners.map(n => (
              <button key={n} onClick={()=>{setMe(n); localStorage.setItem('ft-v2-me',n);}} style={{background:'none',border:'none',cursor:'pointer',display:'flex',flexDirection:'column',alignItems:'center',gap:10}}>
                <Avatar person={n} size={72}/>
                <span style={{fontFamily:'Playfair Display,serif',fontSize:16,fontWeight:700,color:'var(--text)'}}>{n}</span>
              </button>
            ))}
          </div>
          <button onClick={signOutFromPicker} style={{marginTop:36,background:'none',border:'none',color:'var(--text3)',fontSize:12,cursor:'pointer',fontFamily:'inherit',textDecoration:'underline',textUnderlineOffset:3,opacity:.8}}>
            neither of us — sign out
          </button>
        </div>
      </>
    );
  }

  const handleNav = (targetTab, cat, id) => {
    setTab(targetTab);
    if(cat) setFocusCat(cat);
    if(id) setFocusId(id);
  };

  return (
    <>
      <Splash hide={ready}/>
      <div className="ft-app">
        <TopBar me={me} onSwitchUser={()=>{localStorage.removeItem('ft-v2-me'); setMe(null);}}/>
        {tab==='home' && <HomeScreen me={me} onNavigate={handleNav}/>}
        {tab==='list' && <ListScreen me={me} focusCat={focusCat} focusId={focusId} onCatChange={setFocusCat}/>}
        {tab==='talk' && <TalkScreen me={me}/>}
        {tab==='mem' && <MemoriesScreen/>}
        {tab==='us' && <UsScreen me={me} onSwitchUser={()=>{localStorage.removeItem('ft-v2-me'); setMe(null);}}/>}
        <FTUI.DraggableFab onClick={()=>setAddOpen(true)}><Icon.plus/></FTUI.DraggableFab>
        <BottomNav tab={tab} onChange={setTab}/>
      </div>
      <AddItemModal open={addOpen} onClose={()=>setAddOpen(false)} me={me} currentCat={tab==='list'?focusCat:null}/>
      <CommentModal open={!!commentCtx} onClose={()=>setCommentCtx(null)} catKey={commentCtx?.cat} itemId={commentCtx?.id} me={me}/>
      <ReactPickerModal open={!!reactCtx} onClose={()=>setReactCtx(null)} catKey={reactCtx?.cat} itemId={reactCtx?.id} me={me}/>
      <ThinkingSheet open={thinkOpen} onClose={()=>setThinkOpen(false)} me={me}/>
      <NoteSheet open={noteOpen} onClose={()=>{setNoteOpen(false); setNotePrefill('');}} me={me} prefill={notePrefill}/>
      <DateNightSheet open={dnOpen} onClose={()=>setDnOpen(false)}/>
      <AISheet open={aiOpen} onClose={()=>setAiOpen(false)}/>
      <GapFinderSheet open={gapOpen} onClose={()=>setGapOpen(false)} me={me}/>
      <SmartSearchSheet open={!!smartSearchCtx} onClose={()=>setSmartSearchCtx(null)} initialQuery={smartSearchCtx?.query}/>
      <SettingsSheet open={settingsOpen} onClose={()=>setSettingsOpen(false)} me={me} onSignOut={()=>{setSettingsOpen(false); localStorage.removeItem('ft-v2-me'); setMe(null);}}/>
      <NotesHistorySheet open={notesOpen} onClose={()=>setNotesOpen(false)} me={me}/>
      <FeedbackSheet open={feedbackOpen} onClose={()=>setFeedbackOpen(false)} me={me}/>
      <ThisOrThatSheet open={totOpen} onClose={()=>setTotOpen(false)}/>
      <SurpriseSheet open={surpriseOpen} onClose={()=>setSurpriseOpen(false)} onNavigate={handleNav}/>
      <EditCategoryModal open={!!editCat} onClose={()=>setEditCat(null)} catKey={editCat}/>
      <ThinkingReceived msg={thinkMsg} me={me} onClose={()=>{setThinkMsg(null); FT.clearThinking();}} onSendBack={()=>{ setTimeout(()=>setThinkOpen(true), 300); }}/>
      <ItemEditSheet ctx={editItemCtx} onClose={()=>setEditItemCtx(null)}/>
      <TmdbPickSheet ctx={pickTmdbCtx} onClose={()=>setPickTmdbCtx(null)}/>
      <TmdbInfoSheet ctx={infoCtx} onClose={()=>setInfoCtx(null)}/>
      <GameInfoSheet ctx={infoCtx} onClose={()=>setInfoCtx(null)}/>
      <AddPhotoSheet ctx={photoCtx} onClose={()=>setPhotoCtx(null)}/>
      <ActivityFeedSheet open={activityOpen} onClose={()=>setActivityOpen(false)} me={me}/>
      <div id="ft-pull-indicator" style={{position:'fixed',top:'calc(env(safe-area-inset-top, 0px) + 92px)',left:'50%',transform:'translateX(-50%) translateY(-24px) scale(.9)',zIndex:200,padding:'7px 14px',background:'var(--card)',border:'1px solid var(--border2)',borderRadius:99,fontSize:12,opacity:0,transition:'opacity .12s, transform .12s cubic-bezier(.4,0,.2,1), background .2s, border-color .2s',pointerEvents:'none',display:'flex',alignItems:'center',gap:7,boxShadow:'0 6px 20px rgba(0,0,0,.14)',backdropFilter:'blur(10px)',WebkitBackdropFilter:'blur(10px)'}}>
        <span id="ft-pull-icon" style={{display:'inline-block',fontSize:13,transition:'transform .18s cubic-bezier(.4,0,.2,1), color .2s',color:'var(--text2)',lineHeight:1}}>↓</span>
        <span id="ft-pull-label" style={{fontFamily:'Caveat,cursive',fontSize:14.5,letterSpacing:'.02em',color:'var(--text2)',transition:'color .2s',lineHeight:1}}>pull to refresh</span>
      </div>
      <div id="v2Confetti"/>
    </>
  );
}

// AppShell picks between the setup screens (pair code, partner names) and the full app.
ReactDOM.createRoot(document.getElementById('root')).render(<AppShell/>);
