// Fun-Ture v2 — screens
const { Avatar, useData, useProfiles, useActivity, Icon, Squiggle, Card, Btn, Sheet, Modal, Empty, SwipeToDelete } = window.FTUI;
const { useState, useEffect, useMemo, useRef } = React;

const CAT_META = {
  activities:{emoji:'🎯',accent:'apricot',label:'Do'},
  food:{emoji:'🍜',accent:'apricot',label:'Eat'},
  recipes:{emoji:'🍳',accent:'rose',label:'Cook'},
  movies:{emoji:'🎬',accent:'lavender',label:'Watch'},
  tv:{emoji:'📺',accent:'lavender',label:'Shows'},
  books:{emoji:'📚',accent:'teal',label:'Read'},
  music:{emoji:'🎵',accent:'rose',label:'Listen'},
  travel:{emoji:'✈️',accent:'teal',label:'Travel'},
  default:{emoji:'✨',accent:'lavender',label:''}
};
function catMeta(key,cat){
  const m = CAT_META[key] || CAT_META.default;
  return {...m, emoji: cat?.emoji || m.emoji, label: cat?.title?.replace(/^[^\w]+\s*/,'') || m.label};
}

function timeAgo(ts){
  if(!ts) return '';
  const s = (Date.now()-ts)/1000;
  if(s<60) return 'just now';
  if(s<3600) return Math.floor(s/60)+'m';
  if(s<86400) return Math.floor(s/3600)+'h';
  if(s<604800) return Math.floor(s/86400)+'d';
  return Math.floor(s/604800)+'w';
}

// --- Seasonal keyword hints (US / northern hemisphere oriented) ---
const SEASON_KEYWORDS = {
  summer: ['beach','picnic','swim','swimming','bbq','barbecue','ice cream','popsicle','snorkel','kayak','surf','pool','patio','fireworks','lemonade','watermelon','sunset'],
  winter: ['ski','snow','sled','sledding','hot cocoa','hot chocolate','fireplace','ice skat','hanukkah','christmas','eggnog','nye','hibernate','cozy'],
  fall:   ['pumpkin','apple picking','foliage','harvest','haunted','haunt','cider','halloween','thanksgiving','sweater','october'],
  spring: ['easter','tulip','bloom','cherry blossom','daffodil','fresh flower','spring clean']
};
// Returns 1 if item fits current season, 0.2 if it strongly matches a DIFFERENT season, 0.85 neutral
function seasonFit(item, season){
  if(!item?.text) return 0.85;
  const hay = (item.text + ' ' + (item.note||'') + ' ' + (item.where||'') + ' ' + (item.tags||[]).join(' ') + ' ' + (item.moods||[]).join(' ')).toLowerCase();
  for(const s of ['summer','winter','fall','spring']){
    const hits = SEASON_KEYWORDS[s].some(k => hay.includes(k));
    if(hits){ return s === season ? 1.3 : 0.25; }
  }
  return 0.85;
}
// Cat weight based on weather conditions. Higher = more preferred right now.
function weatherCatWeight(catKey, weather){
  if(!weather) return 1;
  const indoor = weather.rainy || weather.snowy || weather.cold || weather.foggy;
  const outdoor = !indoor && (weather.clear && !weather.hot);
  const niceDay = outdoor && weather.isDay;
  const catLower = catKey.toLowerCase();
  const isIndoor = /movie|tv|show|watch|book|read|recipe|cook|game|music|podcast/.test(catLower);
  const isOutdoor = /activit|outdoor|travel|hike|trail|trip|adventur|walk|park|nature/.test(catLower);
  const isFoodOut = /restaurant|eat|dine|date|brunch|cafe|bar/.test(catLower);
  if(indoor && isIndoor) return 1.6;
  if(indoor && isOutdoor) return 0.35;
  if(niceDay && isOutdoor) return 1.7;
  if(niceDay && isFoodOut) return 1.3;
  if(weather.hot && isOutdoor && !weather.isDay) return 1.2;
  if(weather.hot && isOutdoor && weather.isDay) return 0.8; // too hot for midday outdoor
  return 1;
}
// Weighted random pick from array of {item, weight}
function weightedPick(items){
  if(!items.length) return null;
  const total = items.reduce((s,x)=>s + Math.max(0.01, x.weight||1), 0);
  let r = Math.random() * total;
  for(const x of items){
    r -= Math.max(0.01, x.weight||1);
    if(r <= 0) return x;
  }
  return items[items.length-1];
}

// Expose helpers so other files (app.jsx) can reuse
window.__ftSeasonFit = seasonFit;
window.__ftWeatherCatWeight = weatherCatWeight;
window.__ftWeightedPick = weightedPick;

// Text similarity: returns true if a and b are substantially the same
// (prevents duplicate blurbs between item.note and item.nudge.description etc.)
function isSimilarText(a, b){
  if(!a || !b) return false;
  const norm = s => (s||'').toLowerCase().trim().replace(/\s+/g,' ').replace(/["'""'']/g,'');
  const na = norm(a), nb = norm(b);
  if(!na || !nb) return false;
  if(na === nb) return true;
  // substring match when both are meaningfully long
  if(na.length > 25 && nb.length > 25){
    if(na.includes(nb) || nb.includes(na)) return true;
    // common prefix ratio
    const minLen = Math.min(na.length, nb.length);
    let common = 0;
    for(let i = 0; i < minLen; i++){
      if(na[i] === nb[i]) common++;
      else break;
    }
    if(common >= 40 && common / minLen > 0.7) return true;
  }
  return false;
}
window.__ftIsSimilarText = isSimilarText;

// ---------- HOME ----------
function DailyRec({ me, d }){
  const todayKey = new Date().toISOString().split('T')[0];
  const [rec, setRec] = useState(() => {
    try{
      const saved = JSON.parse(localStorage.getItem('ft-daily-rec') || 'null');
      if(saved && saved.day === todayKey){
        // Invalidate stale error-state replies
        if(/don't\s+(actually\s+)?have\s+access|could\s+you\s+share|share\s+what'?s\s+on/i.test(saved.text||'')){
          localStorage.removeItem('ft-daily-rec');
          return null;
        }
        return saved.text;
      }
    }catch(e){}
    return null;
  });
  const [loading, setLoading] = useState(false);
  const generate = async () => {
    if(loading || !d) return;
    setLoading(true);
    try{
      const context = window.FTAPP?.buildListContext?.(d) || '';
      const dayName = new Date().toLocaleDateString('en',{weekday:'long'});
      const season = window.FT?.getSeasonContext?.();
      const holiday = window.FT?.getHolidayContext?.();
      const weather = await window.FT?.getWeatherContext?.({ silent: true });
      const atmo = [];
      atmo.push(`It's ${dayName}, ${season?.month || ''} (${season?.season || ''}).`);
      if(weather) atmo.push(`Weather right now: ${weather.cond}, ${weather.t}°F.`);
      if(holiday) atmo.push(`${holiday.name} is ${holiday.isToday?'today':(holiday.isPast?'yesterday':'in '+holiday.diff+' days')}.`);
      // Pull recent picks history so we don't repeat ourselves
      let history = [];
      try{ history = JSON.parse(localStorage.getItem('ft-daily-rec-history') || '[]') || []; }catch(e){}
      const recent = history.slice(-14);
      const recentBlock = recent.length
        ? `\n\nRECENT SUGGESTIONS YOU HAVE ALREADY MADE (DO NOT repeat the same item or any close variation; pick something noticeably different in topic AND specific item):\n${recent.map((p,i)=>`${i+1}. ${p.text}`).join('\n')}\n`
        : '';
      const prompt = `${atmo.join(' ')}${recentBlock}\nBased on our lists AND the current atmosphere (season, weather, any holidays), give ONE specific, fun, concrete suggestion for what we should do or watch or make today. Keep it to 1-2 sentences, warm and conversational. Reference a specific item from our lists by name. Naturally tie the pick to the weather/season/holiday where it makes sense. Use "you two" or "you both". Don't list multiple options — just one delightful pick. The new pick MUST reference a different specific list item than any of your recent suggestions above and feel meaningfully different in vibe.`;
      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:250,
          system:`You're a warm, fun assistant for a couple ${(FT.partners||['them']).join(' & ')}. Give short, specific daily suggestions that feel grounded in today — the weather, the season, any holidays.`,
          messages:[{role:'user', content: context + '\n---\n\n' + prompt}]
        })
      });
      const data = await r.json();
      const text = data?.content?.[0]?.text?.trim();
      if(text){
        setRec(text);
        try{ localStorage.setItem('ft-daily-rec', JSON.stringify({day:todayKey, text})); }catch(e){}
        try{
          const newHistory = [...history, {day:todayKey, text}].slice(-30);
          localStorage.setItem('ft-daily-rec-history', JSON.stringify(newHistory));
        }catch(e){}
      }
    }catch(e){ console.warn('daily rec', e); }
    setLoading(false);
  };
  useEffect(() => {
    if(!rec && d) generate();
  }, [d]);
  if(!rec && !loading) return null;
  return (
    <div style={{marginTop:12,padding:'11px 14px',borderRadius:12,background:'linear-gradient(135deg,rgba(236,72,153,.08),rgba(167,139,250,.08))',border:'1px solid var(--border-accent)',position:'relative'}}>
      <div style={{fontSize:10,color:'var(--lav)',textTransform:'uppercase',letterSpacing:'.15em',fontWeight:600,marginBottom:4,display:'flex',alignItems:'center',justifyContent:'space-between'}}>
        <span>✨ today's suggestion</span>
        <button onClick={()=>{try{localStorage.removeItem('ft-daily-rec');}catch(e){} setRec(null); generate();}} disabled={loading} style={{background:'none',border:'none',color:'var(--text3)',fontSize:12,cursor:'pointer',padding:2,fontFamily:'inherit',opacity:loading?.4:1}} title="Regenerate">↻</button>
      </div>
      {loading && !rec ? (
        <div style={{fontSize:12,color:'var(--text3)',fontStyle:'italic'}}>thinking of something good for today…</div>
      ) : (() => {
        const MD = window.FTUI && window.FTUI.Markdown;
        return <div style={{fontSize:12.5,color:'var(--text)',lineHeight:1.5}}>{MD ? <MD text={rec}/> : rec}</div>;
      })()}
    </div>
  );
}

// ---------- Convo Starter of the day ----------
function ConvoStarterCard({ d }){
  const todayKey = new Date().toISOString().split('T')[0];
  const [data, setData] = useState(() => {
    try{
      const saved = JSON.parse(localStorage.getItem('ft-convo-starter') || 'null');
      if(saved && saved.day === todayKey) return saved;
    }catch(e){}
    return null;
  });
  const [loading, setLoading] = useState(false);
  const [expanded, setExpanded] = useState(false);

  const generate = async () => {
    if(loading) return;
    setLoading(true);
    try{
      const context = window.FTAPP?.buildListContext?.(d) || '';
      const prompt = `Give us THREE conversation starters for tonight — questions we can actually ask each other. Return them as a JSON array with this exact shape: [{"vibe":"intimate","q":"..."},{"vibe":"silly","q":"..."},{"vibe":"big-picture","q":"..."}]. One intimate/soft, one silly/playful, one big-picture/curious. Each question under 20 words. No prefixes like "Q:" — just the question text. Reference specific items from our lists when it makes sense. Return ONLY the JSON array, no other text.`;
      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:`You're a warm, thoughtful companion for a couple (${(FT.partners||['them']).join(' and ')}). You write conversation starters that feel natural and real — never generic.`,
          messages:[{role:'user', content: context + '\n---\n\n' + prompt}]
        })
      });
      const resp = await r.json();
      const text = resp?.content?.[0]?.text?.trim() || '';
      // Try to extract JSON array (tolerate prose around it)
      const match = text.match(/\[[\s\S]*\]/);
      if(match){
        const arr = JSON.parse(match[0]);
        const next = { day: todayKey, questions: arr };
        setData(next);
        try{ localStorage.setItem('ft-convo-starter', JSON.stringify(next)); }catch(e){}
      }
    }catch(e){ console.warn('convo starter', e); }
    setLoading(false);
  };
  useEffect(()=>{ if(!data && d) generate(); }, [d]);

  if(!data && !loading) return null;
  const qs = data?.questions || [];
  const vibeIcon = { intimate:'💞', silly:'🪩', 'big-picture':'🔭' };
  const shown = expanded ? qs : qs.slice(0,1);

  return (
    <div style={{marginTop:10,padding:'11px 14px',borderRadius:12,background:'linear-gradient(135deg,rgba(251,191,154,.08),rgba(236,72,153,.08))',border:'1px solid var(--border-accent)'}}>
      <div style={{fontSize:10,color:'var(--rose)',textTransform:'uppercase',letterSpacing:'.15em',fontWeight:600,marginBottom:6,display:'flex',alignItems:'center',justifyContent:'space-between'}}>
        <span>💬 convo starters for today</span>
        <button onClick={()=>{try{localStorage.removeItem('ft-convo-starter');}catch(e){} setData(null); generate();}} disabled={loading} style={{background:'none',border:'none',color:'var(--text3)',fontSize:12,cursor:'pointer',padding:2,fontFamily:'inherit',opacity:loading?.4:1}} title="Regenerate">↻</button>
      </div>
      {loading && !data ? (
        <div style={{fontSize:12,color:'var(--text3)',fontStyle:'italic'}}>picking three good ones…</div>
      ) : (
        <>
          <div style={{display:'flex',flexDirection:'column',gap:8}}>
            {shown.map((q,i) => (
              <button key={i} onClick={()=>window.FTAPP?.openNote?.(q.q + '\n\n')} style={{textAlign:'left',background:'var(--card)',border:'1px solid var(--border2)',borderRadius:10,padding:'9px 11px',cursor:'pointer',fontFamily:'inherit',color:'var(--text)',fontSize:12.5,lineHeight:1.45,display:'flex',gap:8,alignItems:'flex-start'}} title="Tap to reply as a note">
                <span style={{fontSize:13,flexShrink:0}}>{vibeIcon[q.vibe] || '💭'}</span>
                <span>{q.q}</span>
              </button>
            ))}
          </div>
          {qs.length > 1 && (
            <button onClick={()=>setExpanded(e=>!e)} style={{marginTop:6,background:'none',border:'none',color:'var(--lav)',fontSize:11,cursor:'pointer',padding:2,fontFamily:'inherit'}}>
              {expanded ? 'show less ↑' : `show ${qs.length - 1} more ↓`}
            </button>
          )}
        </>
      )}
    </div>
  );
}

// ---------- Weather chip ----------
function WeatherChip(){
  const [weather, setWeather] = useState(null);
  const [asked, setAsked] = useState(false);
  useEffect(() => {
    let cancelled = false;
    (async () => {
      const w = await window.FT?.getWeatherContext?.({ silent: true });
      if(!cancelled && w) setWeather(w);
    })();
    return () => { cancelled = true; };
  }, []);
  const enable = async () => {
    setAsked(true);
    const w = await window.FT?.getWeatherContext?.({ silent: false });
    if(w) setWeather(w);
  };
  if(!weather){
    if(asked) return null;
    return (
      <button onClick={enable} style={{background:'none',border:'1px solid var(--border2)',borderRadius:99,padding:'3px 9px',color:'var(--text3)',fontSize:10.5,cursor:'pointer',fontFamily:'inherit',whiteSpace:'nowrap',flexShrink:0}}>
        🌤 weather
      </button>
    );
  }
  const icon = weather.snowy ? '❄️' : weather.rainy ? '🌧' : weather.foggy ? '🌫' : weather.clear ? (weather.isDay?'☀️':'🌙') : '☁️';
  return (
    <div style={{fontSize:10.5,color:'var(--text3)',display:'flex',alignItems:'center',gap:4,background:'var(--card)',border:'1px solid var(--border2)',borderRadius:99,padding:'3px 9px',whiteSpace:'nowrap',flexShrink:0}} title={`feels like ${weather.feel}°F`}>
      <span style={{fontSize:12}}>{icon}</span>
      <span>{weather.t}°</span>
    </div>
  );
}

// ---------- Inbox banner — unread notes/messages from partner ----------
function InboxBanner({ me }){
  const [unread, setUnread] = useState([]);
  useEffect(() => {
    if(!me) return;
    const unsub = window.FT.db.collection('lists').doc('notes_v2').onSnapshot(doc => {
      const log = (doc.exists && doc.data().log) ? doc.data().log : [];
      // Notes addressed to me OR (legacy) not addressed but from my partner
      const forMe = log.filter(n => {
        if(n.for) return n.for === me;
        return n.from && n.from !== me;
      });
      let lastSeen = 0;
      try { lastSeen = parseInt(localStorage.getItem('ft-last-seen-note-' + me) || '0'); } catch(e){}
      setUnread(forMe.filter(n => (n.ts || 0) > lastSeen));
    });
    return unsub;
  }, [me]);

  if(!unread.length) return null;
  const newest = unread[0];
  const sender = newest.from || 'someone';
  const preview = (newest.text || '').slice(0, 60);

  const open = () => {
    try { localStorage.setItem('ft-last-seen-note-' + me, String(newest.ts || Date.now())); } catch(e){}
    setUnread([]);
    window.FTAPP?.openNotesHistory?.();
  };
  const dismiss = (e) => {
    e.stopPropagation();
    try { localStorage.setItem('ft-last-seen-note-' + me, String(newest.ts || Date.now())); } catch(err){}
    setUnread([]);
  };

  return (
    <div onClick={open} style={{marginBottom:10,padding:'10px 13px',borderRadius:12,background:'linear-gradient(135deg,color-mix(in oklab,var(--rose) 18%,transparent),color-mix(in oklab,var(--lavender) 10%,transparent))',border:'1px solid var(--border-accent)',cursor:'pointer',display:'flex',alignItems:'center',gap:10,position:'relative'}}>
      <div style={{fontSize:22,flexShrink:0}}>💌</div>
      <div style={{flex:1,minWidth:0}}>
        <div style={{fontSize:10,color:'var(--rose)',textTransform:'uppercase',letterSpacing:'.15em',fontWeight:600,marginBottom:2}}>
          {unread.length > 1 ? `${unread.length} new notes` : 'new note'} from {sender}
        </div>
        <div style={{fontSize:12.5,color:'var(--text)',lineHeight:1.4,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
          {preview}{preview.length >= 60 ? '…' : ''}
        </div>
      </div>
      <button onClick={dismiss} aria-label="Dismiss" style={{background:'none',border:'none',color:'var(--text3)',fontSize:18,cursor:'pointer',padding:'0 4px',flexShrink:0}}>×</button>
    </div>
  );
}

// ---------- Anniversary banner ----------
function AnniversaryBanner(){
  const profiles = useProfiles();
  const [dismissed, setDismissed] = useState(false);
  const ann = window.FT?.getAnniversaryContext?.();
  if(!ann || dismissed) return null;
  const label = ann.isToday
    ? `Happy anniversary — ${ann.years > 0 ? ann.years + ' year' + (ann.years===1?'':'s') + ' today ♡' : 'today ♡'}`
    : ann.isPast
    ? `Yesterday was your anniversary — hope it was lovely 💕`
    : `Anniversary in ${ann.diff} day${ann.diff===1?'':'s'} — year ${ann.years} 💕`;
  const prefill = ann.isToday
    ? `Happy ${ann.years} year${ann.years===1?'':'s'} my love ♡\n\n`
    : `Thinking about our anniversary coming up…\n\n`;
  return (
    <div onClick={()=>window.FTAPP?.openNote?.(prefill)} style={{marginBottom:10,padding:'10px 13px',borderRadius:12,background:'linear-gradient(135deg,color-mix(in oklab,var(--rose) 18%,transparent),color-mix(in oklab,var(--lavender) 14%,transparent))',border:'1px solid var(--border-accent)',cursor:'pointer',display:'flex',alignItems:'center',gap:10}}>
      <div style={{fontSize:22,flexShrink:0}}>💍</div>
      <div style={{flex:1,minWidth:0}}>
        <div style={{fontSize:10,color:'var(--rose)',textTransform:'uppercase',letterSpacing:'.15em',fontWeight:600,marginBottom:2}}>us ♡</div>
        <div style={{fontSize:12.5,color:'var(--text)',lineHeight:1.4}}>{label}</div>
      </div>
      <button onClick={(e)=>{e.stopPropagation(); setDismissed(true);}} aria-label="Dismiss" style={{background:'none',border:'none',color:'var(--text3)',fontSize:18,cursor:'pointer',padding:'0 4px',flexShrink:0}}>×</button>
    </div>
  );
}

// ---------- Holiday / first-day-of-season banner ----------
function HolidayBanner(){
  const hol = window.FT?.getHolidayContext?.();
  const cacheKey = hol ? 'ft-holiday-dismissed-' + hol.key : null;
  const [dismissed, setDismissed] = useState(() => {
    if(!cacheKey) return false;
    try{ return localStorage.getItem(cacheKey) === '1'; }catch(e){ return false; }
  });
  if(!hol || dismissed) return null;
  const label = hol.isToday
    ? `${hol.name} — today`
    : hol.isPast
    ? `${hol.name} — yesterday`
    : `${hol.name} — in ${hol.diff} day${hol.diff===1?'':'s'}`;
  const openAI = () => {
    // Fire a themed AI preset via the Ask AI sheet
    window.FTAPP?.openAI?.();
    // stash a pending prompt so AISheet can pick it up and send automatically
    try {
      window._pendingAIPrompt = `It's ${hol.isToday ? hol.name : (hol.isPast ? 'the day after '+hol.name : hol.name+' coming up soon')}. From our lists, pick 3 things that would be perfect for this occasion — a recipe, a movie, and an activity or date-night idea. Reference specific items from our lists by name and tell us why each fits.`;
    } catch(e){}
  };
  const dismiss = (e) => {
    e.stopPropagation();
    try{ localStorage.setItem(cacheKey, '1'); }catch(err){}
    setDismissed(true);
  };
  return (
    <div onClick={openAI} style={{marginBottom:10,padding:'10px 13px',borderRadius:12,background:'linear-gradient(135deg,rgba(242,185,136,.14),color-mix(in oklab,var(--rose) 12%,transparent))',border:'1px solid var(--border-accent)',cursor:'pointer',display:'flex',alignItems:'center',gap:10}}>
      <div style={{fontSize:22,flexShrink:0}}>{hol.emoji}</div>
      <div style={{flex:1,minWidth:0}}>
        <div style={{fontSize:10,color:'var(--rose)',textTransform:'uppercase',letterSpacing:'.15em',fontWeight:600,marginBottom:2}}>{hol.isSeasonFirst?'season':'holiday'}</div>
        <div style={{fontSize:12.5,color:'var(--text)',lineHeight:1.4}}>{label} — <span style={{color:'var(--lav)'}}>ideas →</span></div>
      </div>
      <button onClick={dismiss} aria-label="Dismiss" style={{background:'none',border:'none',color:'var(--text3)',fontSize:18,cursor:'pointer',padding:'0 4px',flexShrink:0}}>×</button>
    </div>
  );
}

// ---------- Monthly check-in banner ----------
function MonthlyCheckIn(){
  const now = new Date();
  const monthKey = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0');
  const day = now.getDate();
  const [dismissed, setDismissed] = useState(() => {
    try{ return localStorage.getItem('ft-checkin-dismissed') === monthKey; }catch(e){ return false; }
  });
  // Show only days 1-7 of the month (gentle window)
  if(day > 7) return null;
  if(dismissed) return null;

  const monthName = now.toLocaleDateString('en',{month:'long'});
  const prevMonth = new Date(now.getFullYear(), now.getMonth()-1, 1).toLocaleDateString('en',{month:'long'});

  const openCheckIn = () => {
    const prefill = `Our ${prevMonth} check-in ♡\n\n• What was the best moment this past month?\n  \n\n• What do we want more of in ${monthName}?\n  \n\n• Something new we want to try:\n  \n`;
    window.FTAPP?.openNote?.(prefill);
  };
  const dismiss = (e) => {
    e.stopPropagation();
    try{ localStorage.setItem('ft-checkin-dismissed', monthKey); }catch(err){}
    setDismissed(true);
  };

  return (
    <div onClick={openCheckIn} style={{marginBottom:14,padding:'11px 14px',borderRadius:12,background:'linear-gradient(135deg,color-mix(in oklab,var(--lavender) 15%,transparent),rgba(242,185,136,.10))',border:'1px solid var(--border-accent)',cursor:'pointer',position:'relative',display:'flex',alignItems:'center',gap:10}}>
      <div style={{fontSize:22,flexShrink:0}}>🌙</div>
      <div style={{flex:1,minWidth:0}}>
        <div style={{fontSize:10,color:'var(--lav)',textTransform:'uppercase',letterSpacing:'.15em',fontWeight:600,marginBottom:2}}>monthly check-in</div>
        <div style={{fontSize:12.5,color:'var(--text)',lineHeight:1.4}}>a quick reflection on {prevMonth} — <span style={{color:'var(--lav)'}}>tap to start →</span></div>
      </div>
      <button onClick={dismiss} aria-label="Dismiss" style={{background:'none',border:'none',color:'var(--text3)',fontSize:18,cursor:'pointer',padding:'0 4px',flexShrink:0}}>×</button>
    </div>
  );
}

// ---------- Category summary card ----------
function CategorySummary({ catKey, category }){
  const todayKey = new Date().toISOString().split('T')[0];
  const cacheKey = `ft-cat-summary-${catKey}`;
  const [summary, setSummary] = useState(() => {
    try{
      const saved = JSON.parse(localStorage.getItem(cacheKey) || 'null');
      if(saved && saved.day === todayKey && saved.count === (category?.items||[]).length) return saved.text;
    }catch(e){}
    return null;
  });
  const [loading, setLoading] = useState(false);
  const [expanded, setExpanded] = useState(false);

  // When category changes, re-read cache
  useEffect(() => {
    try{
      const saved = JSON.parse(localStorage.getItem(cacheKey) || 'null');
      if(saved && saved.day === todayKey && saved.count === (category?.items||[]).length){
        setSummary(saved.text);
      } else {
        setSummary(null);
        setExpanded(false);
      }
    }catch(e){ setSummary(null); }
  }, [catKey, (category?.items||[]).length]);

  const generate = async () => {
    if(loading || !category) return;
    setLoading(true);
    try{
      // Build a focused context for just this category
      const items = category.items.map(i => {
        const parts = [i.text];
        if(i.done) parts.push('(✓ done)');
        if(i.rating) parts.push(`(${i.rating}★)`);
        if(i.mood) parts.push(`[${i.mood}]`);
        if(i.tags?.length) parts.push(`[${i.tags.slice(0,3).join(',')}]`);
        return '- ' + parts.join(' ');
      }).join('\n');
      const prompt = `Here's our "${category.title}" list:\n\n${items}\n\nGive us a quick 3-4 sentence summary: total count, what we're leaning toward (genres/cuisines/vibes), how much is still pending vs done, and any interesting patterns. Warm & casual tone. End with one fun observation or nudge.`;
      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:`You're a warm, observant friend summarizing a couple's (${(FT.partners||['them']).join(' and ')}) list. Be specific, not generic.`,
          messages:[{role:'user', content: prompt}]
        })
      });
      const data = await r.json();
      const text = data?.content?.[0]?.text?.trim();
      if(text){
        setSummary(text);
        setExpanded(true);
        try{ localStorage.setItem(cacheKey, JSON.stringify({day:todayKey, count:category.items.length, text})); }catch(e){}
      }
    }catch(e){ console.warn('cat summary', e); }
    setLoading(false);
  };

  if(!category || category.items.length < 3) return null;

  const MD = window.FTUI && window.FTUI.Markdown;

  if(!summary && !loading){
    return (
      <button onClick={generate} style={{marginBottom:10,background:'linear-gradient(135deg,rgba(236,72,153,.08),rgba(167,139,250,.08))',border:'1px solid var(--border-accent)',borderRadius:11,padding:'8px 12px',color:'var(--text2)',fontSize:12,cursor:'pointer',fontFamily:'inherit',width:'100%',textAlign:'left'}}>
        ✨ summarize this list
      </button>
    );
  }

  return (
    <div style={{marginBottom:10,padding:'10px 12px',borderRadius:11,background:'linear-gradient(135deg,rgba(236,72,153,.08),rgba(167,139,250,.08))',border:'1px solid var(--border-accent)'}}>
      <div style={{fontSize:10,color:'var(--lav)',textTransform:'uppercase',letterSpacing:'.15em',fontWeight:600,marginBottom:4,display:'flex',alignItems:'center',justifyContent:'space-between'}}>
        <span>✨ list summary</span>
        <div style={{display:'flex',gap:8}}>
          <button onClick={()=>{try{localStorage.removeItem(cacheKey);}catch(e){} setSummary(null); generate();}} disabled={loading} style={{background:'none',border:'none',color:'var(--text3)',fontSize:12,cursor:'pointer',padding:2,fontFamily:'inherit',opacity:loading?.4:1}} title="Regenerate">↻</button>
          <button onClick={()=>setExpanded(e=>!e)} style={{background:'none',border:'none',color:'var(--text3)',fontSize:12,cursor:'pointer',padding:2,fontFamily:'inherit'}} title={expanded?'hide':'show'}>{expanded?'▴':'▾'}</button>
        </div>
      </div>
      {loading && !summary ? (
        <div style={{fontSize:12,color:'var(--text3)',fontStyle:'italic'}}>summarizing…</div>
      ) : expanded && summary ? (
        <div style={{fontSize:12.5,color:'var(--text)',lineHeight:1.5}}>{MD ? <MD text={summary}/> : summary}</div>
      ) : (
        <div style={{fontSize:12,color:'var(--text3)',fontStyle:'italic'}}>tap ▾ to read</div>
      )}
    </div>
  );
}

function HomeScreen({ me, onNavigate }){
  const d = useData();
  const activity = useActivity();
  const [tonight, setTonight] = useState(null);
  const [shuffling, setShuffling] = useState(false);
  const [weather, setWeather] = useState(null);
  const heroRef = useRef();

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const w = await window.FT?.getWeatherContext?.({ silent: true });
      if(!cancelled && w) setWeather(w);
    })();
    return () => { cancelled = true; };
  }, []);

  const buildWeightedCandidates = () => {
    if(!d) return [];
    const season = window.FT?.getSeasonContext?.()?.season;
    let out = [];
    Object.keys(d).forEach(k => {
      const wCat = weatherCatWeight(k, weather);
      d[k].items.filter(i => !i.done).forEach(it => {
        const wSeason = seasonFit(it, season);
        out.push({ cat: k, it, weight: wCat * wSeason });
      });
    });
    return out;
  };

  useEffect(() => {
    if(!d) return;
    const candidates = buildWeightedCandidates();
    if(!candidates.length) return;
    const pick = weightedPick(candidates);
    if(pick) setTonight(pick);
  }, [d, weather]);

  const shuffle = () => {
    if(!d || shuffling) return;
    const candidates = buildWeightedCandidates();
    if(!candidates.length) return;
    setShuffling(true);
    let count = 0;
    const timer = setInterval(() => {
      setTonight(weightedPick(candidates));
      count++;
      if(count >= 10){
        clearInterval(timer);
        setTonight(weightedPick(candidates));
        setShuffling(false);
      }
    }, 70);
  };

  const stats = useMemo(()=>{
    if(!d) return {total:0, done:0};
    let total=0, done=0;
    Object.values(d).forEach(c => { c.items.forEach(i => { total++; if(i.done) done++; }); });
    return {total, done};
  }, [d]);

  const greeting = useMemo(()=>{
    const h = new Date().getHours();
    if(h<5) return 'late night,';
    if(h<12) return 'morning,';
    if(h<17) return 'afternoon,';
    if(h<21) return 'evening,';
    return 'goodnight,';
  },[]);

  const seasonCtx = window.FT?.getSeasonContext?.();
  return (
    <div className="ft-screen">
      <div className="ft-screen-title">
        <span style={{marginRight:6}}>{seasonCtx?.emoji}</span>{greeting} <span style={{fontFamily:'Caveat',fontWeight:400}}>{me}</span>
      </div>
      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:10}}>
        <div className="ft-screen-sub" style={{marginBottom:0}}>here's what's waiting for us ♡</div>
        <WeatherChip/>
      </div>

      <div style={{marginTop:12}}>
        <InboxBanner me={me}/>
        <AnniversaryBanner/>
        <HolidayBanner/>
        <MonthlyCheckIn/>
      </div>

      {tonight && (
        <div className={`ft-hero ${shuffling?'shuffling':''}`} ref={heroRef}>
          <div className="ft-hero-eyebrow">tonight, maybe…</div>
          <h2 key={tonight.it.id} className="ft-hero-title">{tonight.it.text}</h2>
          <div className="ft-hero-meta">
            <span className="ft-chip accent">{catMeta(tonight.cat, d[tonight.cat]).label || tonight.cat}</span>
            {tonight.it.mood && <span className="ft-chip">{tonight.it.mood}</span>}
            {tonight.it.where && <span className="ft-chip">{tonight.it.where}</span>}
          </div>
          <div className="ft-hero-row">
            <Btn variant="solid" onClick={()=>onNavigate('list', tonight.cat, tonight.it.id)} disabled={shuffling}>Let's do it</Btn>
            <Btn variant="ghost" onClick={shuffle} disabled={shuffling}>{shuffling?'…':'Shuffle'}</Btn>
          </div>
        </div>
      )}

      <div className="ft-section-label" style={{marginTop:10}}>quick things</div>
      <div className="ft-qa">
        <button onClick={()=>window.FTAPP?.openThinking?.()}>
          <div className="ft-qa-ico rose"><Icon.heart/></div>
          <span>Thinking of you</span>
        </button>
        <button onClick={()=>window.FTAPP?.openNote?.()}>
          <div className="ft-qa-ico"><Icon.note/></div>
          <span>Leave a note</span>
        </button>
        <button onClick={()=>window.FTAPP?.openDateNight?.()}>
          <div className="ft-qa-ico gold"><Icon.dice/></div>
          <span>Date night</span>
        </button>
        <button onClick={()=>window.FTAPP?.openThisOrThat?.()}>
          <div className="ft-qa-ico lav"><Icon.bolt/></div>
          <span>This or That</span>
        </button>
        <button onClick={()=>window.FTAPP?.openSurprise?.()}>
          <div className="ft-qa-ico apricot"><Icon.gift/></div>
          <span>Surprise me</span>
        </button>
        <button onClick={()=>window.FTAPP?.openAI?.()}>
          <div className="ft-qa-ico teal"><Icon.sparkle/></div>
          <span>Ask AI</span>
        </button>
      </div>

      <DailyRec me={me} d={d}/>

      <div className="ft-section-label" style={{marginTop:18}}>progress</div>
      <Card hoverable={false}>
        <div style={{display:'flex',alignItems:'baseline',gap:8,marginBottom:8}}>
          <div style={{fontFamily:'Playfair Display,serif',fontSize:28,fontWeight:700,letterSpacing:'-.015em'}}>{stats.done}<span style={{color:'var(--text3)',fontWeight:400}}> of {stats.total}</span></div>
          <div style={{fontFamily:'Caveat,cursive',color:'var(--text2)',fontSize:15}}>adventures done</div>
        </div>
        <div style={{height:6,borderRadius:99,background:'rgba(255,255,255,.05)',overflow:'hidden',position:'relative'}}>
          <div className="ft-progbar-fill" style={{height:'100%',width:(stats.total?stats.done/stats.total*100:0)+'%',transition:'width .6s'}}/>
        </div>
      </Card>

      <div className="ft-section-label">
        between us
        <button onClick={()=>window.FTAPP?.openActivity?.()} style={{marginLeft:'auto',background:'none',border:'none',color:'var(--lavender)',fontSize:10.5,letterSpacing:'.08em',cursor:'pointer',padding:0,fontWeight:600}}>see all →</button>
      </div>
      <Card hoverable={false}>
        {activity.length === 0 ? (
          <div style={{color:'var(--text3)',fontSize:13,textAlign:'center',padding:'16px 0',fontFamily:'Caveat,cursive',fontSize:16}}>no activity yet — start checking things off ♡</div>
        ) : activity.slice(0,5).map((a,i) => (
          <div key={i} className="ft-activity-row" onClick={()=>window.FTAPP?.openActivity?.()} style={{cursor:'pointer'}}>
            <Avatar person={a.who} size={28}/>
            <div className="ft-activity-text"><b>{a.who}</b> {a.detail}</div>
            <div className="ft-activity-when">{timeAgo(a.ts)}</div>
          </div>
        ))}
      </Card>
    </div>
  );
}

// ---------- TALK ----------
const TALK_STYLES = [
  {k:'balanced', l:'🎲 balanced',  hint:'mix of intimate, silly, and big-picture'},
  {k:'playful',  l:'🪩 playful',   hint:'silly, funny, deliberately light questions'},
  {k:'deep',     l:'🌙 deep',      hint:'reflective, intimate, emotional questions'},
  {k:'future',   l:'🔭 future',    hint:'questions about dreams, plans, 10-years-from-now vibes'},
  {k:'memories', l:'📸 memories',  hint:'questions that dig into shared history and inside jokes'},
  {k:'hypothetical', l:'💭 hypothetical', hint:'what-if / would-you-rather / weird thought experiments'},
];

function TalkScreen({ me }){
  const d = useData();
  const todayKey = new Date().toISOString().split('T')[0];
  const [style, setStyle] = useState(() => {
    try{
      const saved = JSON.parse(localStorage.getItem('ft-talk-starter') || 'null');
      if(saved && saved.day === todayKey) return saved.style || 'balanced';
    }catch(e){}
    return 'balanced';
  });
  const [source, setSource] = useState(() => {
    try{ return localStorage.getItem('ft-talk-source') || 'lists'; }catch(e){ return 'lists'; }
  });
  const [data, setData] = useState(() => {
    try{
      const saved = JSON.parse(localStorage.getItem('ft-talk-starter') || 'null');
      if(saved && saved.day === todayKey) return saved;
    }catch(e){}
    return null;
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

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

  const generate = async (styleKey, sourceKey) => {
    if(loading) return;
    setLoading(true); setError(null);
    try{
      const styleObj = TALK_STYLES.find(s => s.k === styleKey) || TALK_STYLES[0];
      const countHint = styleKey === 'balanced'
        ? 'Return THREE questions — one intimate, one silly, one big-picture.'
        : 'Return THREE questions in this style. Each should feel distinct (different angles, not repetitive).';
      const useLists = sourceKey === 'lists';
      const listHint = useLists
        ? 'Where it makes sense, reference specific items from our lists by name (movies, recipes, activities we have saved). Mix in at least ONE question that references our lists.'
        : 'DO NOT reference our lists at all. Write timeless, universal couple questions — no mentions of specific movies, recipes, or activities we have. Focus on us as people: our relationship, feelings, memories, dreams, quirks.';
      const season = window.FT?.getSeasonContext?.();
      const holiday = window.FT?.getHolidayContext?.();
      const atmo = [];
      if(season) atmo.push(`It's ${season.month} (${season.season}).`);
      if(holiday) atmo.push(`${holiday.name} is ${holiday.isToday?'today':(holiday.isPast?'yesterday':'in '+holiday.diff+' days')}.`);
      const atmoHint = atmo.length ? ` CONTEXT: ${atmo.join(' ')} At least ONE question should lightly reference the season or holiday if it fits naturally (don't force it).` : '';
      const prompt = `${countHint} Style: ${styleObj.hint}. ${listHint}${atmoHint} Return as a JSON array in this exact shape: [{"vibe":"<one word>","q":"<question>"}]. Each question under 22 words, natural and specific (not generic). Return ONLY the JSON array, no prose.`;
      const ctx = useLists ? (window.FTAPP?.buildListContext?.(d) || '') : '';
      const userContent = useLists ? (ctx + '\n---\n\n' + prompt) : prompt;
      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:500,
          system:`You're a warm, thoughtful companion for a couple (${(FT.partners||['them']).join(' and ')}). You write conversation starters that feel natural and real — never generic. Always respond with clean JSON only.`,
          messages:[{role:'user', content: userContent}]
        })
      });
      const resp = await r.json();
      const text = resp?.content?.[0]?.text?.trim() || '';
      const match = text.match(/\[[\s\S]*\]/);
      if(!match) throw new Error('Could not parse questions.');
      const arr = JSON.parse(match[0]);
      const next = { day: todayKey, style: styleKey, source: sourceKey, questions: arr };
      setData(next);
      try{ localStorage.setItem('ft-talk-starter', JSON.stringify(next)); }catch(e){}
    }catch(e){ setError(e.message || 'Something went wrong.'); }
    setLoading(false);
  };

  useEffect(() => { if(!data && d) generate(style, source); }, [d]);

  const pickStyle = (k) => {
    setStyle(k);
    if(data?.style !== k) generate(k, source);
  };
  const pickSource = (k) => {
    setSource(k);
    if(data?.source !== k) generate(style, k);
  };

  const vibeIcon = { intimate:'💞', silly:'🪩', 'big-picture':'🔭', playful:'🪩', deep:'🌙', future:'🔭', memories:'📸', hypothetical:'💭' };
  const qs = data?.questions || [];

  return (
    <div className="ft-screen">
      <div className="ft-screen-title" style={{marginBottom:2}}>Talk</div>
      <div className="ft-screen-sub" style={{marginBottom:10}}>three questions for you two, every day</div>

      <div style={{display:'flex',gap:4,flexWrap:'wrap',marginBottom:6}}>
        <button onClick={()=>pickSource('lists')} disabled={loading} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:source==='lists'?'color-mix(in oklab,var(--lavender) 22%,transparent)':'var(--card)',border:`1px solid ${source==='lists'?'var(--border-accent)':'var(--border)'}`,color:source==='lists'?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit',opacity:loading?.5:1}}>
          🗂️ from our lists
        </button>
        <button onClick={()=>pickSource('general')} disabled={loading} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:source==='general'?'color-mix(in oklab,var(--lavender) 22%,transparent)':'var(--card)',border:`1px solid ${source==='general'?'var(--border-accent)':'var(--border)'}`,color:source==='general'?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit',opacity:loading?.5:1}}>
          💭 about us
        </button>
      </div>

      <div style={{display:'flex',gap:4,flexWrap:'wrap',marginBottom:10}}>
        {TALK_STYLES.map(s => (
          <button key={s.k} onClick={()=>pickStyle(s.k)} disabled={loading} style={{padding:'4px 9px',borderRadius:99,fontSize:11,background:style===s.k?'color-mix(in oklab,var(--rose) 22%,transparent)':'var(--card)',border:`1px solid ${style===s.k?'var(--border-accent)':'var(--border)'}`,color:style===s.k?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit',opacity:loading?.5:1}}>
            {s.l}
          </button>
        ))}
      </div>

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

      {loading && !qs.length ? (
        <Card hoverable={false}>
          <div style={{fontSize:13,color:'var(--text3)',fontStyle:'italic',textAlign:'center',padding:'16px 0'}}>finding questions for you…</div>
        </Card>
      ) : qs.length ? (
        <div style={{display:'flex',flexDirection:'column',gap:7}}>
          {qs.map((q,i) => (
            <button key={i} onClick={()=>window.FTAPP?.openNote?.(q.q + '\n\n')} style={{textAlign:'left',background:'linear-gradient(135deg,rgba(251,191,154,.06),color-mix(in oklab,var(--rose) 06%,transparent))',border:'1px solid var(--border-accent)',borderRadius:12,padding:'10px 12px',cursor:'pointer',fontFamily:'inherit',color:'var(--text)',display:'flex',gap:10,alignItems:'flex-start'}}>
              <div style={{fontSize:18,flexShrink:0,lineHeight:1,marginTop:1}}>{vibeIcon[q.vibe] || '💭'}</div>
              <div style={{flex:1,minWidth:0}}>
                <div style={{fontSize:9.5,color:'var(--rose)',textTransform:'uppercase',letterSpacing:'.12em',fontWeight:600,marginBottom:2}}>{q.vibe || 'question'} {i+1}</div>
                <div style={{fontSize:13.5,color:'var(--text)',lineHeight:1.4}}>{q.q}</div>
              </div>
            </button>
          ))}
        </div>
      ) : null}

      <button onClick={()=>{try{localStorage.removeItem('ft-talk-starter');}catch(e){} setData(null); generate(style, source);}} disabled={loading} style={{marginTop:10,width:'100%',padding:'9px',borderRadius:10,background:'var(--card)',border:'1px solid var(--border2)',color:'var(--text2)',fontSize:12,cursor:'pointer',fontFamily:'inherit',opacity:loading?.5:1}}>
        {loading ? 'thinking…' : '↻ try three new questions'}
      </button>

      <div className="ft-section-label" style={{marginTop:18,marginBottom:6}}>or try something else</div>
      <div style={{display:'flex',gap:7,flexDirection:'column'}}>
        <button onClick={()=>window.FTAPP?.openAI?.()} style={{display:'flex',alignItems:'center',gap:10,padding:'9px 12px',borderRadius:11,background:'var(--card)',border:'1px solid var(--border2)',color:'var(--text)',cursor:'pointer',fontFamily:'inherit',textAlign:'left'}}>
          <div style={{width:28,height:28,borderRadius:'50%',background:'rgba(139,184,180,.18)',color:'var(--teal,#8BB8B4)',display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0}}><Icon.sparkle/></div>
          <div style={{flex:1}}>
            <div style={{fontSize:13,fontWeight:600}}>Ask AI anything</div>
            <div style={{fontSize:11,color:'var(--text3)',marginTop:1}}>open-ended chat about your lists</div>
          </div>
          <div style={{color:'var(--text3)',fontSize:15}}>→</div>
        </button>
        <button onClick={()=>window.FTAPP?.openThisOrThat?.()} style={{display:'flex',alignItems:'center',gap:10,padding:'9px 12px',borderRadius:11,background:'var(--card)',border:'1px solid var(--border2)',color:'var(--text)',cursor:'pointer',fontFamily:'inherit',textAlign:'left'}}>
          <div style={{width:28,height:28,borderRadius:'50%',background:'color-mix(in oklab,var(--lavender) 18%,transparent)',color:'var(--lav)',display:'flex',alignItems:'center',justifyContent:'center',flexShrink:0}}><Icon.bolt/></div>
          <div style={{flex:1}}>
            <div style={{fontSize:13,fontWeight:600}}>This or That</div>
            <div style={{fontSize:11,color:'var(--text3)',marginTop:1}}>quick either-or prompts</div>
          </div>
          <div style={{color:'var(--text3)',fontSize:15}}>→</div>
        </button>
      </div>
    </div>
  );
}

// ---------- LIST ----------
function ListScreen({ me, focusCat, focusId, onCatChange }){
  const d = useData();
  const [cat, setCat] = useState(focusCat || null);
  const [q, setQ] = useState('');
  const [showDone, setShowDone] = useState(false);
  const [showArchive, setShowArchive] = useState(false);
  const [editMode, setEditMode] = useState(false);
  const [newListOpen, setNewListOpen] = useState(false);
  const [moodFilter, setMoodFilter] = useState(null);
  const [sortMode, setSortMode] = useState(() => localStorage.getItem('ft-sort-mode') || 'default');
  const scrollRef = useRef();

  useEffect(() => { try{ localStorage.setItem('ft-sort-mode', sortMode); }catch(e){} }, [sortMode]);
  useEffect(() => { if(focusCat) setCat(focusCat); }, [focusCat]);
  useEffect(() => { if(d && !cat) setCat(Object.keys(d)[0]); }, [d, cat]);
  // Notify parent (App) whenever cat changes so + button auto-targets the current list
  useEffect(() => { if(cat) onCatChange?.(cat); }, [cat, onCatChange]);
  useEffect(() => { setMoodFilter(null); setQ(''); }, [cat]);

  if(!d) return null;
  const keys = Object.keys(d);
  const active = cat && d[cat] ? d[cat] : null;

  // Archive threshold — done items completed > 30 days ago are hidden by default
  const ARCHIVE_MS = 30*24*60*60*1000;
  const now = Date.now();
  const isArchived = (i) => i.done && i.completedAt && (now - i.completedAt) > ARCHIVE_MS;

  // Derive moods/tags for smart filter (incomplete only — what's relevant now)
  const smartOptions = active ? (() => {
    const map = {};
    active.items.filter(i=>!i.done).forEach(i => {
      if(i.mood) map[i.mood] = (map[i.mood]||0)+1;
      (i.tags||[]).forEach(t => map[t] = (map[t]||0)+1);
      (i.moods||[]).forEach(m => map[m] = (map[m]||0)+1);
    });
    return Object.entries(map).filter(([,c])=>c>=1).sort((a,b)=>b[1]-a[1]).slice(0,6);
  })() : [];

  const filtered = active ? active.items.filter(i => {
    if(!showDone && i.done) return false;
    if(!showArchive && isArchived(i)) return false;
    if(moodFilter){
      const hay = [i.mood, ...(i.tags||[]), ...(i.moods||[])].filter(Boolean).join(' ').toLowerCase();
      if(!hay.includes(moodFilter.toLowerCase())) return false;
    }
    if(q){
      const s = q.toLowerCase();
      return (i.text||'').toLowerCase().includes(s) || (i.where||'').toLowerCase().includes(s) || (i.mood||'').toLowerCase().includes(s) || (i.tags||[]).some(t=>t.toLowerCase().includes(s));
    }
    return true;
  }) : [];

  // Apply sort mode (alpha / default)
  const sorted = sortMode === 'alpha'
    ? [...filtered].sort((a,b) => (a.text||'').localeCompare(b.text||'', undefined, {sensitivity:'base'}))
    : sortMode === 'alpha-desc'
    ? [...filtered].sort((a,b) => (b.text||'').localeCompare(a.text||'', undefined, {sensitivity:'base'}))
    : filtered;
  const pinned = sorted.filter(i => i.pinned);
  const regular = sorted.filter(i => !i.pinned);
  const stats = active ? (() => {
    let done=0; active.items.forEach(i => i.done && done++);
    return {done, total: active.items.length};
  })() : null;

  // reorder sheet state
  const [reorderOpen, setReorderOpen] = useState(false);

  return (
    <div className="ft-screen">
      <div style={{display:'flex',alignItems:'baseline',justifyContent:'space-between',marginBottom:4,gap:8}}>
        <div className="ft-screen-title">{active ? active.title.replace(/^[^\w]+\s*/,'') : 'Our List'}</div>
        <div style={{display:'flex',alignItems:'center',gap:10}}>
          {stats && <div style={{fontFamily:'Caveat,cursive',fontSize:17,color:'var(--text3)'}}>{stats.done}/{stats.total}</div>}
          <button onClick={()=>setEditMode(!editMode)} title="Edit lists" style={{width:28,height:28,borderRadius:'50%',background:editMode?'color-mix(in oklab,var(--rose) 18%,transparent)':'var(--card)',border:`1px solid ${editMode?'var(--border-accent)':'var(--border2)'}`,color:'var(--text2)',fontSize:13,display:'flex',alignItems:'center',justifyContent:'center'}}>{editMode?'✓':'✎'}</button>
        </div>
      </div>
      <div className="ft-screen-sub">{active?.emoji || '♡'} things to do together</div>

      <div className="ft-catrow-sticky">
        <div className="ft-catrow">
          {keys.map(k => {
            const m = catMeta(k, d[k]);
            return (
              <button
                key={k}
                className={`ft-catchip ${k===cat?'a':''} ${editMode?'edit':''}`}
                onClick={()=>{
                  if(editMode){ window.FTAPP?.editCategory?.(k); }
                  else setCat(k);
                }}
              >
                <span style={{marginRight:5}}>{d[k].emoji}</span>{m.label || d[k].title}
              </button>
            );
          })}
          <button className="ft-catchip new" onClick={()=>setNewListOpen(true)} title="New list">＋ new</button>
        </div>
      </div>

      <div className="ft-search" style={{position:'relative'}}>
        <Icon.search/>
        <input className="ft-input" placeholder="search this list…" value={q} onChange={e=>setQ(e.target.value)} autoCorrect="on" autoCapitalize="sentences" spellCheck="true" style={{paddingRight:42}}/>
        <button onClick={()=>window.FTAPP?.openSmartSearch?.(q)} title="Smart search with AI across all lists" style={{position:'absolute',right:6,top:'50%',transform:'translateY(-50%)',width:30,height:30,borderRadius:'50%',background:'linear-gradient(135deg,rgba(236,72,153,.18),rgba(167,139,250,.18))',border:'1px solid var(--border-accent)',color:'var(--text)',fontSize:14,cursor:'pointer',fontFamily:'inherit',display:'flex',alignItems:'center',justifyContent:'center'}}>✨</button>
      </div>

      {active && !editMode && <CategorySummary catKey={cat} category={active}/>}

      {editMode && (
        <div style={{padding:'10px 12px',marginBottom:10,borderRadius:12,background:'color-mix(in oklab,var(--lavender) 8%,transparent)',border:'1px solid var(--border-accent)',fontSize:12,color:'var(--text2)',lineHeight:1.5,display:'flex',alignItems:'center',justifyContent:'space-between',gap:8,flexWrap:'wrap'}}>
          <span><b style={{color:'var(--text)'}}>Edit mode</b> · tap a chip to rename/remove · ＋ adds a new list</span>
          <button onClick={()=>setReorderOpen(true)} className="ft-btn ft-btn-ghost ft-btn-sm">↕ reorder</button>
        </div>
      )}

      {smartOptions.length > 0 && !editMode && (
        <div style={{display:'flex',gap:6,overflowX:'auto',padding:'2px 2px 10px',scrollbarWidth:'none',WebkitOverflowScrolling:'touch'}}>
          <div style={{fontSize:10.5,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.15em',alignSelf:'center',marginRight:4,flexShrink:0}}>mood</div>
          {smartOptions.map(([m,count]) => (
            <button key={m} onClick={()=>setMoodFilter(moodFilter===m?null:m)}
              style={{flexShrink:0,padding:'5px 11px',borderRadius:99,fontSize:11.5,
                background:moodFilter===m?'color-mix(in oklab,var(--rose) 20%,transparent)':'var(--card)',
                border:`1px solid ${moodFilter===m?'var(--border-accent)':'var(--border)'}`,
                color:moodFilter===m?'var(--text)':'var(--text2)',cursor:'pointer',whiteSpace:'nowrap'}}>
              {m} <span style={{fontSize:9,color:'var(--text3)',marginLeft:2}}>{count}</span>
            </button>
          ))}
          {moodFilter && <button onClick={()=>setMoodFilter(null)} style={{flexShrink:0,padding:'5px 9px',borderRadius:99,fontSize:11,background:'transparent',border:'1px dashed var(--border)',color:'var(--text3)'}}>× clear</button>}
        </div>
      )}

      {active && active.items.length > 1 && !editMode && (
        <div style={{display:'flex',alignItems:'center',gap:6,fontSize:11,marginBottom:10,paddingLeft:2}}>
          <span style={{color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.12em'}}>sort</span>
          <button onClick={()=>setSortMode('default')} style={{padding:'3px 9px',borderRadius:99,background:sortMode==='default'?'color-mix(in oklab,var(--rose) 18%,transparent)':'transparent',border:`1px solid ${sortMode==='default'?'var(--border-accent)':'var(--border)'}`,color:sortMode==='default'?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit',fontSize:11}}>manual</button>
          <button onClick={()=>setSortMode('alpha')} style={{padding:'3px 9px',borderRadius:99,background:sortMode==='alpha'?'color-mix(in oklab,var(--rose) 18%,transparent)':'transparent',border:`1px solid ${sortMode==='alpha'?'var(--border-accent)':'var(--border)'}`,color:sortMode==='alpha'?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit',fontSize:11}}>A→Z</button>
          <button onClick={()=>setSortMode('alpha-desc')} style={{padding:'3px 9px',borderRadius:99,background:sortMode==='alpha-desc'?'color-mix(in oklab,var(--rose) 18%,transparent)':'transparent',border:`1px solid ${sortMode==='alpha-desc'?'var(--border-accent)':'var(--border)'}`,color:sortMode==='alpha-desc'?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit',fontSize:11}}>Z→A</button>
        </div>
      )}
      <div ref={scrollRef}>
        {pinned.length > 0 && <>
          <div className="ft-section-label">pinned</div>
          {pinned.map(it => <Item key={it.id} item={it} catKey={cat} me={me} highlight={it.id===focusId}/>)}
        </>}
        {regular.length > 0 && <>
          {pinned.length > 0 && <div className="ft-section-label">everything else</div>}
          {regular.map(it => <Item key={it.id} item={it} catKey={cat} me={me} highlight={it.id===focusId}/>)}
        </>}
        {filtered.length === 0 && <Empty glyph="♡ ♡ ♡">{q ? `nothing matches "${q}"` : 'this list is empty — tap + to add'}</Empty>}

        {active && active.items.some(i=>i.done) && (
          <button className="ft-btn ft-btn-ghost ft-btn-sm" style={{width:'100%',marginTop:14}} onClick={()=>setShowDone(!showDone)}>
            {showDone ? 'hide completed' : `show ${active.items.filter(i=>i.done && !isArchived(i)).length} completed`}
          </button>
        )}
        {showDone && active && active.items.some(isArchived) && (
          <button className="ft-btn ft-btn-ghost ft-btn-sm" style={{width:'100%',marginTop:8,opacity:.7}} onClick={()=>setShowArchive(!showArchive)}>
            {showArchive ? 'hide archive' : `📦 show ${active.items.filter(isArchived).length} archived (>30 days)`}
          </button>
        )}
      </div>

      <NewListModal open={newListOpen} onClose={()=>setNewListOpen(false)} onCreate={(title, emoji)=>{
        const k = FT.addCategory(title, emoji);
        if(k) setCat(k);
        setNewListOpen(false);
      }}/>
      <ReorderListsSheet open={reorderOpen} onClose={()=>setReorderOpen(false)} d={d}/>
    </div>
  );
}

function ReorderListsSheet({ open, onClose, d }){
  // Order = persistent list. Only re-set on commit (pointerup or arrow-tap).
  const [order, setOrder] = useState([]);
  // drag = {key, startIdx, dragY, targetIdx, stride} or null
  const [drag, setDrag] = useState(null);
  const rowRefs = useRef({});
  const dragRef = useRef(null); // mutable copy for handlers

  useEffect(() => { if(open && d) setOrder(Object.keys(d)); }, [open, d]);

  // Stop iOS Safari from hijacking the gesture as a page scroll while a drag is in progress.
  useEffect(() => {
    if(drag == null) return;
    const prevent = (e) => e.preventDefault();
    document.addEventListener('touchmove', prevent, { passive: false });
    return () => document.removeEventListener('touchmove', prevent);
  }, [drag]);

  const commit = (next) => {
    setOrder(next);
    FT.reorderCategories(next);
  };

  const move = (i, dir) => {
    const j = i + dir;
    if(j < 0 || j >= order.length) return;
    const next = [...order];
    [next[i], next[j]] = [next[j], next[i]];
    commit(next);
  };

  const onPointerDown = (e, key) => {
    e.preventDefault();
    const target = e.currentTarget;
    try { target.setPointerCapture(e.pointerId); } catch(_){}
    const startIdx = order.indexOf(key);
    // Measure stride (vertical distance between consecutive rows) by comparing
    // any two row tops — accounts for row height + flex gap.
    let stride = 50;
    const startEl = rowRefs.current[key];
    if(startEl){
      const sr = startEl.getBoundingClientRect();
      stride = sr.height + 6; // fallback assumes our gap=6
      for(const k of order){
        if(k === key) continue;
        const el = rowRefs.current[k];
        if(el){
          const r = el.getBoundingClientRect();
          const dist = Math.abs(r.top - sr.top);
          if(dist > 0){ stride = dist; break; }
        }
      }
    }
    dragRef.current = {
      pointerId: e.pointerId,
      startY: e.clientY,
      startIdx,
      key,
      stride,
      lastY: e.clientY,
      lastTargetIdx: startIdx,
      rafId: 0,
    };
    setDrag({ key, startIdx, dragY: 0, targetIdx: startIdx, stride });
    if(navigator.vibrate) navigator.vibrate(8);
  };

  const onPointerMove = (e) => {
    const dr = dragRef.current;
    if(!dr || dr.pointerId !== e.pointerId) return;
    dr.lastY = e.clientY;
    if(dr.rafId) return;
    dr.rafId = requestAnimationFrame(() => {
      const dr2 = dragRef.current;
      if(!dr2) return;
      dr2.rafId = 0;
      const dy = dr2.lastY - dr2.startY;
      let targetIdx = dr2.startIdx + Math.round(dy / dr2.stride);
      targetIdx = Math.max(0, Math.min(order.length - 1, targetIdx));
      if(targetIdx !== dr2.lastTargetIdx){
        dr2.lastTargetIdx = targetIdx;
        if(navigator.vibrate) navigator.vibrate(2);
      }
      setDrag(prev => prev ? { ...prev, dragY: dy, targetIdx } : prev);
    });
  };

  const onPointerUp = (e) => {
    const dr = dragRef.current;
    if(!dr || dr.pointerId !== e.pointerId) return;
    if(dr.rafId) cancelAnimationFrame(dr.rafId);
    const from = dr.startIdx;
    const dy = dr.lastY - dr.startY;
    let to = dr.startIdx + Math.round(dy / dr.stride);
    to = Math.max(0, Math.min(order.length - 1, to));
    dragRef.current = null;
    setDrag(null);
    if(from === to) return;
    const next = [...order];
    const [moved] = next.splice(from, 1);
    next.splice(to, 0, moved);
    commit(next);
    if(navigator.vibrate) navigator.vibrate(8);
  };

  if(!d) return null;

  return (
    <Sheet open={open} onClose={onClose} title="↕ Reorder lists">
      <div style={{fontSize:12,color:'var(--text3)',textAlign:'center',marginBottom:10}}>drag ⋮⋮ to reorder · 🗑 to delete · saves instantly</div>
      <div style={{display:'flex',flexDirection:'column',gap:6,paddingBottom:10,touchAction:'pan-y',position:'relative'}}>
        {order.map((k, idx) => {
          const c = d[k]; if(!c) return null;
          const isDragging = drag?.key === k;
          const onDelete = () => {
            const cleanTitle = (c.title||'').replace(/^[^\w]+\s*/,'') || 'this list';
            const n = c.items?.length || 0;
            const msg = n > 0
              ? `Delete "${cleanTitle}"? This removes the list and all ${n} item${n===1?'':'s'} on it. Can't undo.`
              : `Delete "${cleanTitle}"? Can't undo.`;
            if(window.confirm(msg)){
              FT.deleteCategory(k);
              setOrder(prev => prev.filter(x => x !== k));
            }
          };

          // Compute Y offset for this row.
          // - Dragging row tracks finger via dragY.
          // - Other rows shift by exactly one stride to make room.
          let translateY = 0;
          if(drag){
            if(isDragging){
              translateY = drag.dragY;
            } else {
              const { startIdx, targetIdx, stride } = drag;
              if(startIdx < targetIdx && idx > startIdx && idx <= targetIdx) translateY = -stride;
              else if(startIdx > targetIdx && idx < startIdx && idx >= targetIdx) translateY = stride;
            }
          }

          return (
            <div key={k}
              ref={el => { if(el) rowRefs.current[k] = el; }}
              style={{
                display:'flex',alignItems:'center',gap:6,padding:'6px 8px',
                background: isDragging ? 'color-mix(in oklab,var(--rose) 14%,transparent)' : 'var(--card)',
                border:`1px solid ${isDragging?'var(--border-accent)':'var(--border)'}`,
                borderRadius:11,
                boxShadow: isDragging ? '0 12px 28px rgba(0,0,0,.22)' : 'none',
                transform: `translate3d(0, ${translateY}px, 0)${isDragging ? ' scale(1.025)' : ''}`,
                transition: isDragging ? 'none' : 'transform .22s cubic-bezier(.2,.7,.3,1), background .18s ease, box-shadow .18s ease',
                opacity: isDragging ? .98 : 1,
                position: 'relative',
                zIndex: isDragging ? 10 : 1,
                willChange: drag ? 'transform' : 'auto',
              }}>
              <div
                onPointerDown={(e)=>onPointerDown(e, k)}
                onPointerMove={onPointerMove}
                onPointerUp={onPointerUp}
                onPointerCancel={onPointerUp}
                style={{
                  width:30,height:36,display:'flex',alignItems:'center',justifyContent:'center',
                  color:'var(--text3)',fontSize:15,lineHeight:1,
                  cursor:'grab',touchAction:'none',userSelect:'none',WebkitUserSelect:'none',
                  letterSpacing:'-1px'
                }}
                title="drag to reorder"
              >⋮⋮</div>
              <span style={{fontSize:16}}>{c.emoji}</span>
              <span style={{flex:1,fontFamily:'Playfair Display,serif',fontSize:14,fontWeight:600,color:'var(--text)',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{c.title?.replace(/^[^\w]+\s*/,'')}</span>
              <span style={{fontSize:11,color:'var(--text3)',marginRight:2}}>{c.items?.length||0}</span>
              <button onClick={()=>move(idx,-1)} disabled={idx===0 || drag!=null}
                style={{width:26,height:26,borderRadius:'50%',background:idx===0?'transparent':'var(--bg2)',border:`1px solid ${idx===0?'var(--border)':'var(--border2)'}`,color:idx===0?'var(--text3)':'var(--text)',fontSize:12,cursor:idx===0?'default':'pointer',opacity:idx===0?.35:1,padding:0}}>↑</button>
              <button onClick={()=>move(idx,1)} disabled={idx===order.length-1 || drag!=null}
                style={{width:26,height:26,borderRadius:'50%',background:idx===order.length-1?'transparent':'var(--bg2)',border:`1px solid ${idx===order.length-1?'var(--border)':'var(--border2)'}`,color:idx===order.length-1?'var(--text3)':'var(--text)',fontSize:12,cursor:idx===order.length-1?'default':'pointer',opacity:idx===order.length-1?.35:1,padding:0}}>↓</button>
              <button onClick={onDelete} title="delete list" aria-label={`Delete ${c.title?.replace(/^[^\w]+\s*/,'')||'list'}`}
                style={{width:26,height:26,borderRadius:'50%',background:'transparent',border:'1px solid var(--border2)',fontSize:12,cursor:'pointer',padding:0,marginLeft:2,lineHeight:1}}>🗑</button>
            </div>
          );
        })}
      </div>
    </Sheet>
  );
}

function NewListModal({ open, onClose, onCreate }){
  const [title, setTitle] = useState('');
  const [emoji, setEmoji] = useState('✨');
  const inputRef = useRef();
  useEffect(() => { if(open){ setTitle(''); setEmoji('✨'); setTimeout(()=>inputRef.current?.focus(),100);}}, [open]);
  if(!open) return null;
  const submit = () => {
    if(!title.trim()) return;
    onCreate(title.trim(), emoji);
  };
  const emojiOpts = ['✨','🎯','🍜','🍳','🎬','📺','📚','🎵','✈️','🎲','🌸','🎨','🏃','🧘','🍷','☕','🎂','🎁'];
  return (
    <div className="ft-modal-ov" onClick={onClose}>
      <div className="ft-modal" onClick={e=>e.stopPropagation()}>
        <div className="ft-modal-head"><h3>New list</h3><button className="ft-close-btn" onClick={onClose}><Icon.close/></button></div>
        <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 ref={inputRef} className="ft-input" placeholder="e.g. Hikes we want to do" value={title} onChange={e=>setTitle(e.target.value)} onKeyDown={e=>e.key==='Enter'&&submit()} autoCorrect="on" autoCapitalize="sentences" spellCheck="true"/>
        <Btn variant="solid" size="lg" style={{width:'100%',marginTop:14}} onClick={submit}>Create list</Btn>
      </div>
    </div>
  );
}

function SeasonTracker({ item, catKey }){
  const hasTracker = item.seasonCur || item.seasonTot || item.epCur || item.epTot;
  if(!hasTracker || item.done) return null;

  const bump = (field, dir) => {
    const copy = {...item};
    if(field==='epCur'){
      const epTot = copy.epTot || 0;
      const newVal = (copy.epCur||0) + dir;
      // roll into next season
      if(newVal > epTot && epTot && copy.seasonEps && (copy.seasonCur||0) < (copy.seasonTot||0)){
        copy.seasonCur = (copy.seasonCur||0) + 1;
        copy.epCur = 1;
        const nextS = copy.seasonEps.find(s => s.num === copy.seasonCur);
        if(nextS) copy.epTot = nextS.eps;
      } else if(newVal < 0 && (copy.seasonCur||0) > 1 && copy.seasonEps){
        copy.seasonCur = (copy.seasonCur||1) - 1;
        const prevS = copy.seasonEps.find(s => s.num === copy.seasonCur);
        if(prevS){ copy.epTot = prevS.eps; copy.epCur = prevS.eps; }
      } else {
        copy.epCur = Math.max(0, Math.min(epTot||999, newVal));
      }
    } else if(field==='seasonCur'){
      const newVal = Math.max(0, Math.min(copy.seasonTot||999, (copy.seasonCur||0)+dir));
      copy.seasonCur = newVal; copy.epCur = 0;
      if(copy.seasonEps){
        const s = copy.seasonEps.find(x => x.num===newVal);
        if(s) copy.epTot = s.eps;
      }
    }
    FT.updateItem(catKey, item.id, {seasonCur:copy.seasonCur, seasonTot:copy.seasonTot, epCur:copy.epCur, epTot:copy.epTot});
  };

  const sPct = item.seasonTot ? Math.min(100, Math.round((item.seasonCur||0)/item.seasonTot*100)) : 0;
  const ePct = item.epTot ? Math.min(100, Math.round((item.epCur||0)/item.epTot*100)) : 0;

  const BumpBtn = ({onClick, children}) => (
    <button onClick={(e)=>{e.stopPropagation(); onClick();}} style={{width:34,height:34,borderRadius:'50%',background:'var(--bg2)',border:'1px solid var(--border2)',color:'var(--text)',fontSize:18,fontWeight:500,cursor:'pointer',lineHeight:1,padding:0,display:'flex',alignItems:'center',justifyContent:'center',WebkitTapHighlightColor:'transparent',transition:'transform .1s, background .15s',touchAction:'manipulation'}} onMouseDown={e=>e.currentTarget.style.transform='scale(.9)'} onMouseUp={e=>e.currentTarget.style.transform=''} onMouseLeave={e=>e.currentTarget.style.transform=''} onTouchStart={e=>e.currentTarget.style.transform='scale(.9)'} onTouchEnd={e=>e.currentTarget.style.transform=''}>{children}</button>
  );

  return (
    <div style={{marginBottom:10,padding:'9px 11px',borderRadius:10,background:'color-mix(in oklab,var(--rose) 7%,transparent)',border:'1px solid var(--border)'}}>
      <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:6,fontSize:10.5,textTransform:'uppercase',letterSpacing:'.12em',color:'var(--text3)'}}>
        <span>📺 progress</span>
      </div>
      {item.seasonTot ? (
        <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:item.epTot?10:0}}>
          <div style={{flex:1}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',marginBottom:5,fontSize:12}}>
              <span style={{color:'var(--text2)',fontWeight:500}}>Season</span>
              <span style={{color:'var(--text)',fontWeight:600,fontVariantNumeric:'tabular-nums'}}>{item.seasonCur||0} <span style={{color:'var(--text3)',fontWeight:400}}>/ {item.seasonTot}</span></span>
            </div>
            <div style={{height:8,borderRadius:99,background:'var(--input-bg)',overflow:'hidden',boxShadow:'inset 0 1px 2px rgba(0,0,0,.08)'}}>
              <div style={{width:sPct+'%',height:'100%',background:'linear-gradient(90deg,#EC4899,#A78BFA)',transition:'width .35s cubic-bezier(.4,0,.2,1)',boxShadow:sPct>0?'0 0 8px color-mix(in oklab,var(--rose) 55%,transparent)':'none'}}/>
            </div>
          </div>
          <div style={{display:'flex',gap:6,flexShrink:0}}>
            <BumpBtn onClick={()=>bump('seasonCur',-1)}>−</BumpBtn>
            <BumpBtn onClick={()=>bump('seasonCur',1)}>+</BumpBtn>
          </div>
        </div>
      ) : null}
      {item.epTot ? (
        <div style={{display:'flex',alignItems:'center',gap:10}}>
          <div style={{flex:1}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',marginBottom:5,fontSize:12}}>
              <span style={{color:'var(--text2)',fontWeight:500}}>Episode</span>
              <span style={{color:'var(--text)',fontWeight:600,fontVariantNumeric:'tabular-nums'}}>{item.epCur||0} <span style={{color:'var(--text3)',fontWeight:400}}>/ {item.epTot}</span></span>
            </div>
            <div style={{height:8,borderRadius:99,background:'var(--input-bg)',overflow:'hidden',boxShadow:'inset 0 1px 2px rgba(0,0,0,.08)'}}>
              <div style={{width:ePct+'%',height:'100%',background:'linear-gradient(90deg,#EC4899,#A78BFA)',transition:'width .35s cubic-bezier(.4,0,.2,1)',boxShadow:ePct>0?'0 0 8px color-mix(in oklab,var(--rose) 55%,transparent)':'none'}}/>
            </div>
          </div>
          <div style={{display:'flex',gap:6,flexShrink:0}}>
            <BumpBtn onClick={()=>bump('epCur',-1)}>−</BumpBtn>
            <BumpBtn onClick={()=>bump('epCur',1)}>+</BumpBtn>
          </div>
        </div>
      ) : null}
    </div>
  );
}

function NudgeCard({ item }){
  const n = item.nudge;
  if(!n || typeof n !== 'object') return null;
  // Only render the card if it has meaningful extra info beyond what the main item card already shows.
  const hasRichText = !!(n.nudgeText || n.description || n.hours || n.rating);
  const hasNearby = !!(n.nearby && n.nearby.length);
  const hasFullAddress = !!(n.address && n.address.length > (n.neighborhood||'').length + 4);
  if(!hasRichText && !hasNearby && !hasFullAddress) return null;
  // Only include the photo+meta top row if the card has its own photo AND some meta.
  const showTop = (n.neighborhood || n.rating || n.hours) && (n.photo);
  return (
    <div className="ft-nc">
      {showTop && (
        <div className="ft-nc-top">
          {n.photo && <img className="ft-nc-photo" src={n.photo} alt="" onError={e=>e.target.style.display='none'}/>}
          <div className="ft-nc-info">
            {n.neighborhood && <div className="ft-nc-hood">📍 {n.neighborhood}</div>}
            {n.rating && <div className="ft-nc-rating">★ {n.rating}{n.reviewCount?` (${n.reviewCount})`:''}</div>}
            {n.hours && <div className="ft-nc-hours">🕘 {n.hours}</div>}
          </div>
        </div>
      )}
      {n.nudgeText && <div className="ft-nc-pull">"{n.nudgeText}"</div>}
      {n.description && !isSimilarText(n.description, n.nudgeText) && !isSimilarText(n.description, item.note) && <div className="ft-nc-desc">{n.description}</div>}
      {n.address && <div className="ft-nc-addr">📍 {n.address}</div>}
      {n.nearby && n.nearby.length > 0 && (
        <div className="ft-nc-near">
          <div className="ft-nc-near-title">Nearby spots</div>
          <div className="ft-nc-near-list">{n.nearby.join(' · ')}</div>
        </div>
      )}
      {item.link && (() => {
        let label = 'Open link →';
        try {
          const host = new URL(item.link).hostname.replace(/^www\./,'');
          if(/nudgetext\.com/i.test(host)) label = 'View on The Nudge →';
          else label = `Open on ${host} →`;
        } catch(e){}
        return <a className="ft-nc-link" href={item.link} target="_blank" rel="noopener noreferrer" onClick={e=>e.stopPropagation()}>{label}</a>;
      })()}
    </div>
  );
}

function Item({ item, catKey, me, highlight }){
  const profiles = useProfiles();
  const [expanded, setExpanded] = useState(false);
  const [descMore, setDescMore] = useState(false);
  const [flash, setFlash] = useState(false);
  const cbRef = useRef();
  const SeeMore = ({show}) => show ? (
    <button onClick={(e)=>{e.stopPropagation(); setDescMore(m=>!m);}} style={{background:'none',border:'none',color:'var(--lav)',fontSize:11.5,cursor:'pointer',padding:'2px 0',fontFamily:'inherit',fontWeight:500,marginTop:2}}>
      {descMore?'less ▴':'see more ▾'}
    </button>
  ) : null;

  const toggle = () => {
    FT.toggleDone(catKey, item.id, me);
    if(!item.done){ // was undone, now done
      setFlash(true);
      setTimeout(()=>setFlash(false), 700);
      window.FTAPP?.burst?.(cbRef.current);
    }
  };

  const poster = item.photo || item.tmdb_poster || item.tmdbPoster || item.rawg_poster || item.og_image || item.poster || item.nudge?.photo;
  const backdrop = item.photo || item.tmdb_backdrop || item.rawg_backdrop || item.nudge?.photo;
  const hasTmdb = !!(item.tmdb_poster || item.tmdb_backdrop || item.tmdb_rating || item.tmdb_year);
  const hasRawg = !hasTmdb && !!(item.rawg_poster || item.rawg_rating || item.rawg_year || item.rawg_metacritic);
  const hasOg = !!item.og_image && !hasTmdb && !hasRawg;
  const hasNudge = !hasTmdb && !hasRawg && !hasOg && !!(item.nudge && (item.nudge.photo || item.nudge.neighborhood || item.nudge.rating || item.nudge.description || item.nudge.address || item.nudge.nudgeText));
  const overview = item.tmdb_overview || item.rawg_overview || item.og_overview;

  useEffect(() => {
    if(highlight && cbRef.current){
      cbRef.current.scrollIntoView({block:'center', behavior:'smooth'});
    }
  }, [highlight]);

  const doDelete = () => { if(window.confirm(`Delete "${item.text}"?`)) FT.removeItem(catKey, item.id); };

  // Media-led card (movies / TV with TMDB data)
  if(hasTmdb){
    const openInfo = (e)=>{ e?.stopPropagation?.(); window.FTAPP?.moreInfo?.(catKey, item.id); };
    return (
      <SwipeToDelete onDelete={doDelete}><div className={`ft-mitem ${item.done?'done':''}`} style={highlight?{boxShadow:'0 0 0 2px var(--border-accent)'}:null}>
        {backdrop && <div className="ft-mitem-bd" style={{backgroundImage:`url(${backdrop})`}}/>}
        <div className="ft-mitem-inner">
          <div ref={cbRef} className={`ft-item-cb ${item.done?'checked':''} ${flash?'pop':''}`} onClick={toggle}><Icon.check/></div>
          {poster && (
            <div className="ft-mitem-poster ft-mitem-poster-tappable" onClick={openInfo} title="Tap for details">
              <img src={poster} alt=""/>
              <div className="ft-mitem-poster-hint">ℹ︎</div>
            </div>
          )}
          <div className="ft-mitem-body" onClick={()=>setExpanded(!expanded)}>
            <div className="ft-mitem-title">{item.text}</div>
            <div className="ft-mitem-meta">
              {item.tmdb_year && <span className="ft-mitem-year">{item.tmdb_year}</span>}
              {item.tmdb_rating && <span className="ft-mitem-rating">★ {item.tmdb_rating}</span>}
              {item.tmdb_type && <span className="ft-mitem-type">{item.tmdb_type==='tv'?'Series':'Movie'}</span>}
              <span className="ft-mitem-details-chip" onClick={openInfo}>
                ℹ︎ details
              </span>
            </div>
            {overview && <><div className="ft-mitem-ov" style={(expanded||descMore)?null:{WebkitLineClamp:2}}>{overview}</div><SeeMore show={overview.length>110}/></>}
            <div className="ft-item-meta" style={{marginTop:6}}>
              {item.mood && <span className="ft-badge ft-badge-mood">{item.mood}</span>}
              {(item.tags||[]).slice(0,2).map(t => <span key={t} className="ft-badge ft-badge-tag">{t}</span>)}
              {item.createdBy && <Avatar person={item.createdBy} size={20}/>}
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.addComment?.(catKey, item.id);}} style={{fontSize:11,color:item.comments?.length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:item.comments?.length?1:.6}} title="Add a comment">💬{item.comments?.length?' '+item.comments.length:''}</button>
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.openReact?.(catKey, item.id);}} style={{fontSize:11,color:Object.keys(item.reactions||{}).length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:Object.keys(item.reactions||{}).length?1:.6,marginLeft:-6}} title="React">{(() => { const r = item.reactions||{}; const keys = Object.keys(r); if(!keys.length) return '😊'; return keys[0] + ' ' + Object.values(r).reduce((n,arr)=>n+arr.length,0); })()}</button>
            </div>
            {expanded && <ItemExpanded item={item} catKey={catKey} me={me}/>}
          </div>
        </div>
      </div></SwipeToDelete>
    );
  }

  // Media-led card (video games with RAWG data)
  if(hasRawg){
    const openInfo = (e)=>{ e?.stopPropagation?.(); window.FTAPP?.moreInfo?.(catKey, item.id); };
    return (
      <SwipeToDelete onDelete={doDelete}><div className={`ft-mitem ${item.done?'done':''}`} style={highlight?{boxShadow:'0 0 0 2px var(--border-accent)'}:null}>
        {backdrop && <div className="ft-mitem-bd" style={{backgroundImage:`url(${backdrop})`}}/>}
        <div className="ft-mitem-inner">
          <div ref={cbRef} className={`ft-item-cb ${item.done?'checked':''} ${flash?'pop':''}`} onClick={toggle}><Icon.check/></div>
          {poster && (
            <div className="ft-mitem-poster ft-mitem-poster-tappable" onClick={openInfo} title="Tap for details">
              <img src={poster} alt=""/>
              <div className="ft-mitem-poster-hint">ℹ︎</div>
            </div>
          )}
          <div className="ft-mitem-body" onClick={()=>setExpanded(!expanded)}>
            <div className="ft-mitem-title">{item.text}</div>
            <div className="ft-mitem-meta">
              {item.rawg_year && <span className="ft-mitem-year">{item.rawg_year}</span>}
              {item.rawg_metacritic ? (
                <span className="ft-mitem-rating" style={{color: item.rawg_metacritic>=75?'var(--sage)':item.rawg_metacritic>=50?'var(--gold)':'var(--rose)'}}>
                  {item.rawg_metacritic} metacritic
                </span>
              ) : item.rawg_rating && (
                <span className="ft-mitem-rating">★ {item.rawg_rating}</span>
              )}
              <span className="ft-mitem-type" style={{background:'rgba(125,181,130,.18)',color:'var(--sage)'}}>Game</span>
              <span className="ft-mitem-details-chip" onClick={openInfo}>
                ℹ︎ details
              </span>
            </div>
            {item.rawg_platforms?.length > 0 && (
              <div style={{display:'flex',gap:4,flexWrap:'wrap',marginBottom:5,marginTop:2}}>
                {item.rawg_platforms.slice(0,4).map(p => (
                  <span key={p} style={{fontSize:9.5,padding:'1.5px 6px',borderRadius:99,background:'var(--card-strong)',color:'var(--text3)',letterSpacing:'.02em'}}>{p}</span>
                ))}
              </div>
            )}
            {overview && <><div className="ft-mitem-ov" style={(expanded||descMore)?null:{WebkitLineClamp:2}}>{overview}</div><SeeMore show={overview.length>110}/></>}
            <div className="ft-item-meta" style={{marginTop:6}}>
              {item.mood && <span className="ft-badge ft-badge-mood">{item.mood}</span>}
              {(item.tags||[]).slice(0,2).map(t => <span key={t} className="ft-badge ft-badge-tag">{t}</span>)}
              {item.createdBy && <Avatar person={item.createdBy} size={20}/>}
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.addComment?.(catKey, item.id);}} style={{fontSize:11,color:item.comments?.length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:item.comments?.length?1:.6}} title="Add a comment">💬{item.comments?.length?' '+item.comments.length:''}</button>
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.openReact?.(catKey, item.id);}} style={{fontSize:11,color:Object.keys(item.reactions||{}).length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:Object.keys(item.reactions||{}).length?1:.6,marginLeft:-6}} title="React">{(() => { const r = item.reactions||{}; const keys = Object.keys(r); if(!keys.length) return '😊'; return keys[0] + ' ' + Object.values(r).reduce((n,arr)=>n+arr.length,0); })()}</button>
            </div>
            {expanded && <ItemExpanded item={item} catKey={catKey} me={me}/>}
          </div>
        </div>
      </div></SwipeToDelete>
    );
  }

  // Nudge card — venues/activities from nudgetext.com with photo + neighborhood
  if(hasNudge){
    const n = item.nudge;
    const displayPhoto = item.photo || n.photo;
    return (
      <SwipeToDelete onDelete={doDelete}><div className={`ft-mitem ${item.done?'done':''}`} style={highlight?{boxShadow:'0 0 0 2px var(--border-accent)'}:null}>
        {displayPhoto && <div className="ft-mitem-bd" style={{backgroundImage:`url(${displayPhoto})`}}/>}
        <div className="ft-mitem-inner">
          <div ref={cbRef} className={`ft-item-cb ${item.done?'checked':''} ${flash?'pop':''}`} onClick={toggle}><Icon.check/></div>
          {displayPhoto && (
            <div className="ft-mitem-poster"><img src={displayPhoto} alt=""/></div>
          )}
          <div className="ft-mitem-body" onClick={()=>setExpanded(!expanded)}>
            <div className="ft-mitem-title">{item.text}</div>
            <div className="ft-mitem-meta">
              {n.neighborhood && <span className="ft-mitem-year">📍 {n.neighborhood}</span>}
              {n.rating && <span className="ft-mitem-rating">★ {n.rating}{n.reviewCount?` (${n.reviewCount})`:''}</span>}
              {n.hours && <span className="ft-mitem-type" style={{background:'rgba(125,181,130,.18)',color:'var(--sage)'}}>🕘 {n.hours}</span>}
            </div>
            {(() => {
              const blurb = n.nudgeText || n.description || item.note || '';
              if(!blurb) return null;
              const isQuote = !!n.nudgeText;
              return (
                <>
                  <div className="ft-mitem-ov" style={(expanded||descMore)?null:{WebkitLineClamp:2, ...(isQuote?{fontStyle:'italic'}:{})}}>
                    {isQuote ? `"${blurb}"` : blurb}
                  </div>
                  <SeeMore show={blurb.length>110}/>
                </>
              );
            })()}
            <div className="ft-item-meta" style={{marginTop:6}}>
              {item.mood && <span className="ft-badge ft-badge-mood">{item.mood}</span>}
              {(item.tags||[]).slice(0,2).map(t => <span key={t} className="ft-badge ft-badge-tag">{t}</span>)}
              {item.createdBy && <Avatar person={item.createdBy} size={20}/>}
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.addComment?.(catKey, item.id);}} style={{fontSize:11,color:item.comments?.length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:item.comments?.length?1:.6}} title="Add a comment">💬{item.comments?.length?' '+item.comments.length:''}</button>
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.openReact?.(catKey, item.id);}} style={{fontSize:11,color:Object.keys(item.reactions||{}).length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:Object.keys(item.reactions||{}).length?1:.6,marginLeft:-6}} title="React">{(() => { const r = item.reactions||{}; const keys = Object.keys(r); if(!keys.length) return '😊'; return keys[0] + ' ' + Object.values(r).reduce((n,arr)=>n+arr.length,0); })()}</button>
            </div>
            {expanded && <ItemExpanded item={item} catKey={catKey} me={me}/>}
          </div>
        </div>
      </div></SwipeToDelete>
    );
  }

  // Link-media card (recipes / articles with og_image, but no TMDB)
  if(hasOg){
    const ogPhoto = item.photo || item.og_image;
    return (
      <SwipeToDelete onDelete={doDelete}><div className={`ft-mitem ${item.done?'done':''}`} style={highlight?{boxShadow:'0 0 0 2px var(--border-accent)'}:null}>
        <div className="ft-mitem-bd" style={{backgroundImage:`url(${ogPhoto})`}}/>
        <div className="ft-mitem-inner">
          <div ref={cbRef} className={`ft-item-cb ${item.done?'checked':''} ${flash?'pop':''}`} onClick={toggle}><Icon.check/></div>
          <div className="ft-mitem-poster"><img src={ogPhoto} alt=""/></div>
          <div className="ft-mitem-body" onClick={()=>setExpanded(!expanded)}>
            <div className="ft-mitem-title">{item.text}</div>
            <div className="ft-mitem-meta">
              {item.link && <span className="ft-mitem-type" style={{textTransform:'lowercase'}}>{(() => { try{ return new URL(item.link).hostname.replace(/^www\./,''); }catch(e){ return 'link'; }})()}</span>}
            </div>
            {(() => {
              // Prefer user-written note; fall back to og_overview. Show both if meaningfully different.
              const note = item.note;
              const haveNote = !!note;
              const haveOv = !!overview && !isSimilarText(overview, note);
              if(!haveNote && !haveOv) return null;
              const primary = haveNote ? note : overview;
              const secondary = haveNote && haveOv ? overview : null;
              return (
                <>
                  <div className="ft-mitem-ov" style={(expanded||descMore)?null:{WebkitLineClamp:2}}>{primary}</div>
                  {(expanded||descMore) && secondary && <div className="ft-mitem-ov" style={{marginTop:4,opacity:.7,fontSize:'0.95em'}}>{secondary}</div>}
                  <SeeMore show={(primary?.length||0)>110 || !!secondary}/>
                </>
              );
            })()}
            <div className="ft-item-meta" style={{marginTop:6}}>
              {item.mood && <span className="ft-badge ft-badge-mood">{item.mood}</span>}
              {(item.tags||[]).slice(0,2).map(t => <span key={t} className="ft-badge ft-badge-tag">{t}</span>)}
              {item.createdBy && <Avatar person={item.createdBy} size={20}/>}
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.addComment?.(catKey, item.id);}} style={{fontSize:11,color:item.comments?.length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:item.comments?.length?1:.6}} title="Add a comment">💬{item.comments?.length?' '+item.comments.length:''}</button>
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.openReact?.(catKey, item.id);}} style={{fontSize:11,color:Object.keys(item.reactions||{}).length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:Object.keys(item.reactions||{}).length?1:.6,marginLeft:-6}} title="React">{(() => { const r = item.reactions||{}; const keys = Object.keys(r); if(!keys.length) return '😊'; return keys[0] + ' ' + Object.values(r).reduce((n,arr)=>n+arr.length,0); })()}</button>
            </div>
            {expanded && <ItemExpanded item={item} catKey={catKey} me={me}/>}
          </div>
        </div>
      </div></SwipeToDelete>
    );
  }

  // Nudge card (activities / places with nudgetext.com data) — photo + neighborhood inline
  if(hasNudge){
    const n = item.nudge;
    const openLink = (e)=>{ if(item.link){ e?.stopPropagation?.(); const a=document.createElement('a'); a.href=item.link; a.target='_blank'; a.rel='noopener noreferrer'; document.body.appendChild(a); a.click(); a.remove(); }};
    const displayPhoto = item.photo || n.photo;
    return (
      <SwipeToDelete onDelete={doDelete}><div className={`ft-mitem ${item.done?'done':''}`} style={highlight?{boxShadow:'0 0 0 2px var(--border-accent)'}:null}>
        {displayPhoto && <div className="ft-mitem-bd" style={{backgroundImage:`url(${displayPhoto})`}}/>}
        <div className="ft-mitem-inner">
          <div ref={cbRef} className={`ft-item-cb ${item.done?'checked':''} ${flash?'pop':''}`} onClick={toggle}><Icon.check/></div>
          {displayPhoto && (
            <div className="ft-mitem-poster ft-mitem-poster-tappable" onClick={openLink} title={item.link?'Open on The Nudge':''}>
              <img src={displayPhoto} alt=""/>
              {item.link && <div className="ft-mitem-poster-hint">↗</div>}
            </div>
          )}
          <div className="ft-mitem-body" onClick={()=>setExpanded(!expanded)}>
            <div className="ft-mitem-title">{item.text}</div>
            <div className="ft-mitem-meta">
              {n.neighborhood && <span className="ft-mitem-year">📍 {n.neighborhood}</span>}
              {n.rating && <span className="ft-mitem-rating">★ {n.rating}{n.reviewCount?` (${n.reviewCount})`:''}</span>}
              {n.hours && <span className="ft-mitem-type" style={{background:'rgba(125,181,130,.18)',color:'var(--sage)'}}>🕘 {n.hours}</span>}
            </div>
            {(() => {
              const blurb = n.nudgeText || n.description || item.note || '';
              if(!blurb) return null;
              const isQuote = !!n.nudgeText;
              return (
                <>
                  <div className="ft-mitem-ov" style={(expanded||descMore)?null:{WebkitLineClamp:2, ...(isQuote?{fontStyle:'italic'}:{})}}>
                    {isQuote ? `"${blurb}"` : blurb}
                  </div>
                  <SeeMore show={blurb.length>110}/>
                </>
              );
            })()}
            <div className="ft-item-meta" style={{marginTop:6}}>
              {item.mood && <span className="ft-badge ft-badge-mood">{item.mood}</span>}
              {(item.tags||[]).slice(0,2).map(t => <span key={t} className="ft-badge ft-badge-tag">{t}</span>)}
              {(n.tags||n.autoTags||[]).slice(0,3).map(t => <span key={t} className="ft-badge ft-badge-tag">{t}</span>)}
              {item.createdBy && <Avatar person={item.createdBy} size={20}/>}
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.addComment?.(catKey, item.id);}} style={{fontSize:11,color:item.comments?.length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:item.comments?.length?1:.6}} title="Add a comment">💬{item.comments?.length?' '+item.comments.length:''}</button>
              <button onClick={(e)=>{e.stopPropagation(); window.FTAPP?.openReact?.(catKey, item.id);}} style={{fontSize:11,color:Object.keys(item.reactions||{}).length?'var(--text2)':'var(--text3)',background:'none',border:'none',cursor:'pointer',padding:'2px 4px',fontFamily:'inherit',lineHeight:1,opacity:Object.keys(item.reactions||{}).length?1:.6,marginLeft:-6}} title="React">{(() => { const r = item.reactions||{}; const keys = Object.keys(r); if(!keys.length) return '😊'; return keys[0] + ' ' + Object.values(r).reduce((n,arr)=>n+arr.length,0); })()}</button>
            </div>
            {expanded && <ItemExpanded item={item} catKey={catKey} me={me}/>}
          </div>
        </div>
      </div></SwipeToDelete>
    );
  }

  return (
    <SwipeToDelete onDelete={doDelete}><div className={`ft-item ${item.done?'done':''}`} style={highlight?{boxShadow:'0 0 0 2px var(--border-accent)'}:null}>
      <div ref={cbRef} className={`ft-item-cb ${item.done?'checked':''} ${flash?'pop':''}`} onClick={toggle}>
        <Icon.check/>
      </div>
      {poster && <div className="ft-item-poster"><img src={poster} alt=""/></div>}
      <div className="ft-item-body" onClick={()=>setExpanded(!expanded)}>
        <div className="ft-item-text">{item.text}</div>
        {item.note && (
          <div style={{fontSize:12,color:'var(--text3)',lineHeight:1.4,marginTop:2,display:'-webkit-box',WebkitLineClamp: expanded?'unset':2,WebkitBoxOrient:'vertical',overflow:'hidden'}}>
            {item.note}
          </div>
        )}
        <div className="ft-item-meta">
          {item.status && <span className="ft-badge ft-badge-status">{item.status}</span>}
          {item.mood && <span className="ft-badge ft-badge-mood">{item.mood}</span>}
          {(item.tags||[]).slice(0,2).map(t => <span key={t} className="ft-badge ft-badge-tag">{t}</span>)}
          {item.createdBy && <Avatar person={item.createdBy} size={20}/>}
          {item.comments?.length > 0 && <span style={{fontSize:11,color:'var(--text3)'}}>💬 {item.comments.length}</span>}
        </div>
        {expanded && <ItemExpanded item={item} catKey={catKey} me={me}/>}
      </div>
    </div></SwipeToDelete>
  );
}

function StarRating({ item, catKey, me }){
  if(!me) return null;
  const ratings = item.ratings || {};
  const names = Object.keys(ratings).filter(n => ratings[n]);
  const myKey = me.toLowerCase();
  const myVal = ratings[myKey] || 0;
  const setMy = (v) => window.FT?.setRating?.(catKey, item.id, me, v === myVal ? 0 : v);
  return (
    <div style={{display:'flex',alignItems:'center',gap:10,marginBottom:10,padding:'7px 11px',borderRadius:10,background:'rgba(242,185,136,.08)',border:'1px solid var(--border)'}}>
      <div style={{fontSize:11,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.1em',flexShrink:0}}>ratings</div>
      <div style={{display:'flex',gap:1,alignItems:'center'}}>
        {[1,2,3,4,5].map(n => (
          <button key={n} onClick={(e)=>{e.stopPropagation(); setMy(n);}} style={{background:'none',border:'none',padding:'2px 1px',cursor:'pointer',fontSize:18,lineHeight:1,color:n<=myVal?'var(--gold)':'var(--text3)',opacity:n<=myVal?1:.4,transition:'opacity .15s'}}>★</button>
        ))}
      </div>
      <div style={{flex:1,display:'flex',gap:8,justifyContent:'flex-end',fontSize:11,color:'var(--text3)'}}>
        {names.filter(n => n !== myKey).map(n => (
          <span key={n} style={{whiteSpace:'nowrap'}}><span style={{textTransform:'capitalize',marginRight:3}}>{n}:</span><span style={{color:'var(--gold)'}}>{'★'.repeat(ratings[n])}</span><span style={{opacity:.3}}>{'★'.repeat(5-ratings[n])}</span></span>
        ))}
      </div>
    </div>
  );
}

function ItemReactions({ item, catKey, me }){
  const reactions = item.reactions || {};
  const activeEmojis = Object.keys(reactions).filter(e => reactions[e]?.length);
  if(!activeEmojis.length) return null;
  const toggle = (emoji) => { window.FT?.toggleItemReaction?.(catKey, item.id, emoji, me); };
  return (
    <div style={{display:'flex',alignItems:'center',gap:6,flexWrap:'wrap',marginBottom:8}}>
      {activeEmojis.map(emoji => {
        const users = reactions[emoji];
        const mine = users.includes(me);
        return (
          <button key={emoji} onClick={(e)=>{e.stopPropagation(); toggle(emoji);}} style={{background:mine?'color-mix(in oklab,var(--rose) 18%,transparent)':'var(--card)',border:`1px solid ${mine?'var(--border-accent)':'var(--border)'}`,borderRadius:99,padding:'3px 9px',fontSize:12,cursor:'pointer',display:'flex',alignItems:'center',gap:4,fontFamily:'inherit',color:'var(--text)'}}>
            <span>{emoji}</span><span style={{fontSize:11,color:'var(--text3)'}}>{users.length}</span>
          </button>
        );
      })}
    </div>
  );
}

function ItemExpanded({ item, catKey, me }){
  const metaBits = [];
  if(item.where) metaBits.push({icon:'📍', text:item.where});
  if(item.status) metaBits.push({icon:'⏱', text:item.status});
  if(item.done && item.completedBy && item.completedAt){
    metaBits.push({icon:'✓', text:`${item.completedBy} · ${timeAgo(item.completedAt)}`});
  } else if(item.createdAt){
    metaBits.push({icon:'✎', text:`added ${timeAgo(item.createdAt)}${item.createdBy?' by '+item.createdBy:''}`, muted:true});
  }
  return (
    <div style={{marginTop:10,paddingTop:10,borderTop:'1px solid var(--border)'}}>
      {(() => {
        // Skip showing note if it's the same as what the nudge/tmdb/rawg card already shows
        if(!item.note) return null;
        const n = item.nudge || {};
        const competing = [n.description, n.nudgeText, item.tmdb_overview, item.rawg_overview, item.og_overview];
        if(competing.some(t => isSimilarText(item.note, t))) return null;
        return <div style={{fontSize:12.5,color:'var(--text2)',lineHeight:1.5,marginBottom:8}}>{item.note}</div>;
      })()}
      {metaBits.length > 0 && (
        <div style={{display:'flex',flexWrap:'wrap',gap:8,marginBottom:8,fontSize:11,color:'var(--text3)'}}>
          {metaBits.map((b,i) => (
            <span key={i} style={{display:'inline-flex',alignItems:'center',gap:4,opacity:b.muted?.6:1}}>
              <span>{b.icon}</span>
              <span>{b.text}</span>
            </span>
          ))}
        </div>
      )}
      <ItemReactions item={item} catKey={catKey} me={me}/>
      <StarRating item={item} catKey={catKey} me={me}/>
      <SeasonTracker item={item} catKey={catKey}/>
      <NudgeCard item={item}/>
      {item.comments?.length > 0 && (
        <div className="ft-comments">
          {item.comments.map(c => {
            const reactions = c.reactions || {};
            const activeEmojis = Object.keys(reactions).filter(e => reactions[e]?.length);
            const mine = c.by === me;
            return (
              <div key={c.id} className={`ft-comment ${mine?'mine':''}`}>
                <div className="ft-comment-avatar"><Avatar person={c.by} size={24}/></div>
                <div className="ft-comment-body">
                  <div className="ft-comment-bubble">
                    <div className="ft-comment-who">{c.by}</div>
                    <div className="ft-comment-text">{c.text}</div>
                  </div>
                  <div className="ft-comment-reactions">
                    {activeEmojis.map(e => {
                      const iReacted = reactions[e].includes(me);
                      return (
                        <button
                          key={e}
                          onClick={()=>FT.toggleCommentReaction(catKey, item.id, c.id, e, me)}
                          className={`ft-reaction ${iReacted?'mine':''}`}
                        >
                          <span className="ft-reaction-emoji">{e}</span>
                          <span className="ft-reaction-count">{reactions[e].length}</span>
                        </button>
                      );
                    })}
                    <button
                      className="ft-reaction-add"
                      onClick={(ev)=>{
                        ev.stopPropagation();
                        const menu = ev.currentTarget.nextElementSibling;
                        if(menu) menu.style.display = menu.style.display==='flex'?'none':'flex';
                      }}
                    >＋</button>
                    <div className="ft-reaction-picker">
                      {['🥰','❤️','😭','😍','🤣','✨'].map(e => (
                        <button
                          key={e}
                          onClick={(ev)=>{
                            ev.stopPropagation();
                            FT.toggleCommentReaction(catKey, item.id, c.id, e, me);
                            ev.currentTarget.parentElement.style.display='none';
                          }}
                        >{e}</button>
                      ))}
                    </div>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      )}
      <div style={{display:'flex',gap:6,marginTop:8,flexWrap:'wrap'}}>
        <button className="ft-btn ft-btn-ghost ft-btn-sm" onClick={()=>window.FTAPP?.editItem?.(catKey, item.id)}>✏️ edit</button>
        <button className="ft-btn ft-btn-ghost ft-btn-sm" onClick={()=>window.FTAPP?.addComment?.(catKey, item.id)}>💬 comment</button>
        <button className="ft-btn ft-btn-ghost ft-btn-sm" onClick={()=>FT.updateItem(catKey,item.id,{pinned:!item.pinned})}>{item.pinned?'📌 unpin':'📌 pin'}</button>
        {(() => {
          const hasAnyImage = !!(item.photo || item.tmdb_poster || item.rawg_poster || item.og_image || item.nudge?.photo);
          const lockedToTmdbRawg = !!(item.tmdb_poster || item.rawg_poster);
          if(!hasAnyImage){
            return <button className="ft-btn ft-btn-ghost ft-btn-sm" onClick={()=>window.FTAPP?.addPhoto?.(catKey, item.id)}>🖼 add photo</button>;
          }
          if(!lockedToTmdbRawg){
            return <button className="ft-btn ft-btn-ghost ft-btn-sm" onClick={()=>window.FTAPP?.addPhoto?.(catKey, item.id)}>🖼 change photo</button>;
          }
          return null;
        })()}
        {(item.tmdb_type || ['movies','tv','shows'].includes(catKey)) && (
          <button className="ft-btn ft-btn-ghost ft-btn-sm" onClick={()=>window.FTAPP?.pickTmdb?.(catKey, item.id)}>🎬 re-match</button>
        )}
        {(item.where || item.nudge?.address || item.nudge?.neighborhood) && (
          <button className="ft-btn ft-btn-ghost ft-btn-sm" onClick={(e)=>{
            e.stopPropagation();
            const q = encodeURIComponent([item.text, item.where, item.nudge?.address, item.nudge?.neighborhood].filter(Boolean).join(' '));
            window.open(`https://www.google.com/maps/search/?api=1&query=${q}`, '_blank');
          }}>📍 maps</button>
        )}
        {item.link && (
          <button className="ft-btn ft-btn-ghost ft-btn-sm" onClick={(e)=>{
            e.stopPropagation();
            window.open(item.link, '_blank');
          }}>🔗 link</button>
        )}
      </div>
    </div>
  );
}

// ---------- MEMORIES ----------
function MemoriesScreen(){
  const d = useData();
  const [filter, setFilter] = useState('all'); // 'all' | 'favorites'
  const [detailCtx, setDetailCtx] = useState(null); // {cat, id} | null
  if(!d) return null;

  const memories = [];
  Object.entries(d).forEach(([k, cat]) => {
    cat.items.filter(i => i.done).forEach(i => {
      const ts = i.completedAt || i.lastDone || i.completedTs || 0;
      memories.push({cat:k, catTitle:cat.title, emoji:cat.emoji, ...i, completedAt: ts});
    });
  });
  memories.sort((a,b) => (b.completedAt||0) - (a.completedAt||0));

  const favCount = memories.filter(m => m.favorite).length;
  const shown = filter === 'favorites' ? memories.filter(m => m.favorite) : memories;

  // group by month (undated items go into "earlier")
  const groups = {};
  shown.forEach(m => {
    let key;
    if(m.completedAt){
      const dt = new Date(m.completedAt);
      key = dt.toLocaleDateString('en', {month:'long', year:'numeric'});
    } else {
      key = 'earlier ♡';
    }
    (groups[key] = groups[key] || []).push(m);
  });

  return (
    <div className="ft-screen">
      <div className="ft-screen-title">Memories</div>
      <div className="ft-screen-sub">things we did together ♡</div>

      {memories.length > 0 && (
        <div style={{display:'flex',gap:5,marginTop:12,marginBottom:4}}>
          <button onClick={()=>setFilter('all')} style={{padding:'5px 12px',borderRadius:99,fontSize:11.5,background:filter==='all'?'color-mix(in oklab,var(--rose) 22%,transparent)':'var(--card)',border:`1px solid ${filter==='all'?'var(--border-accent)':'var(--border)'}`,color:filter==='all'?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>
            all <span style={{color:'var(--text3)',marginLeft:2}}>{memories.length}</span>
          </button>
          <button onClick={()=>setFilter('favorites')} style={{padding:'5px 12px',borderRadius:99,fontSize:11.5,background:filter==='favorites'?'color-mix(in oklab,var(--rose) 22%,transparent)':'var(--card)',border:`1px solid ${filter==='favorites'?'var(--border-accent)':'var(--border)'}`,color:filter==='favorites'?'var(--text)':'var(--text3)',cursor:'pointer',fontFamily:'inherit'}}>
            ♥ favorites <span style={{color:'var(--text3)',marginLeft:2}}>{favCount}</span>
          </button>
        </div>
      )}

      {memories.length === 0 ? (
        <Empty glyph="♡">nothing here yet — check something off and it'll land here</Empty>
      ) : shown.length === 0 ? (
        <div style={{textAlign:'center',color:'var(--text3)',padding:30,fontFamily:'Caveat,cursive',fontSize:17}}>no favorites yet — tap the ♡ on any memory to save it</div>
      ) : Object.entries(groups).map(([month, items]) => (
        <div key={month}>
          <div className="ft-mem-month">{month} <span className="count">{items.length}</span></div>
          {items.map(m => <Memory key={m.id} m={m} onOpen={()=>setDetailCtx({cat:m.cat, id:m.id})}/>)}
        </div>
      ))}

      <MemoryDetailSheet ctx={detailCtx} onClose={()=>setDetailCtx(null)}/>
    </div>
  );
}

function Memory({ m, onOpen }){
  const poster = m.photo || m.tmdb_poster || m.tmdbPoster || m.poster;
  const dt = m.completedAt ? new Date(m.completedAt) : null;
  const dateStr = dt ? dt.toLocaleDateString('en', {weekday:'short', month:'short', day:'numeric'}) : '♡';
  const toggleFav = (e) => {
    e.stopPropagation();
    window.FT?.updateItem?.(m.cat, m.id, { favorite: !m.favorite });
  };
  return (
    <div className="ft-mem" onClick={onOpen} style={{cursor:'pointer',position:'relative'}}>
      {poster ? (
        <div className="ft-mem-img"><img src={poster} alt=""/></div>
      ) : (
        <div className="ft-mem-placeholder">{m.emoji || '♡'}</div>
      )}
      <div className="ft-mem-body">
        <div className="ft-mem-date">{dateStr}</div>
        <div className="ft-mem-title" style={{paddingRight:28}}>{m.text}</div>
        {m.note && <div style={{fontSize:12.5,color:'var(--text2)',lineHeight:1.4,marginBottom:6}}>{m.note}</div>}
        {(m.completedBy || m.comments?.length) ? (
          <div className="ft-mem-who">
            {m.completedBy && <><Avatar person={m.completedBy} size={20}/><span>{m.completedBy} checked it off</span></>}
            {m.comments?.length > 0 && <span style={{marginLeft:'auto',color:'var(--text3)'}}>💬 {m.comments.length}</span>}
          </div>
        ) : null}
      </div>
      <button onClick={toggleFav} aria-label={m.favorite?'Unfavorite':'Favorite'} title={m.favorite?'Unfavorite':'Favorite'}
        className={`ft-mem-fav ${m.favorite?'on':''}`}>
        <span style={{position:'relative',top:m.favorite?0:-1}}>{m.favorite ? '♥' : '♡'}</span>
      </button>
    </div>
  );
}

function MemoryDetailSheet({ ctx, onClose }){
  const d = useData();
  const [reflectText, setReflectText] = useState('');
  const [editingReflect, setEditingReflect] = useState(false);

  const item = ctx && d && d[ctx.cat] ? d[ctx.cat].items.find(i => i.id === ctx.id) : null;
  const cat = ctx && d ? d[ctx.cat] : null;

  useEffect(() => {
    if(ctx){ setReflectText(''); setEditingReflect(false); }
  }, [ctx]);

  if(!ctx || !item) return null;

  const dt = item.completedAt ? new Date(item.completedAt) : null;
  const dateStr = dt ? dt.toLocaleDateString('en', {weekday:'long', month:'long', day:'numeric', year:'numeric'}) : 'date unknown';
  const poster = item.photo || item.tmdb_poster || item.tmdbPoster || item.rawg_poster || item.og_image || item.nudge?.photo;
  const ratings = item.ratings || {};
  const ratedBy = Object.keys(ratings).filter(n => ratings[n]);

  const saveReflection = () => {
    const t = reflectText.trim();
    if(!t) return;
    window.FT?.updateItem?.(ctx.cat, ctx.id, { retrospective: t, retrospectiveAt: Date.now() });
    setEditingReflect(false);
    setReflectText('');
  };
  const clearReflection = () => {
    window.FT?.updateItem?.(ctx.cat, ctx.id, { retrospective: null, retrospectiveAt: null });
  };

  return (
    <Sheet open={!!ctx} onClose={onClose} title={item.text}>
      {poster && (
        <div style={{borderRadius:14,overflow:'hidden',marginBottom:12,aspectRatio:'16/9',background:'var(--card)'}}>
          <img src={poster} alt="" style={{width:'100%',height:'100%',objectFit:'cover'}}/>
        </div>
      )}

      <div style={{display:'flex',alignItems:'center',gap:8,fontSize:11,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.1em',marginBottom:6}}>
        <span>{cat?.emoji} {(cat?.title||'').replace(/^[^\w]+\s*/,'')}</span>
        {item.favorite && <span style={{color:'var(--rose)'}}>· ♥ favorite</span>}
      </div>
      <div style={{fontSize:12.5,color:'var(--text2)',marginBottom:12}}>
        ✓ {dateStr}{item.completedBy ? ` · ${item.completedBy} checked it off` : ''}
      </div>

      {item.note && <div style={{fontSize:13,color:'var(--text2)',lineHeight:1.55,marginBottom:12,padding:10,borderRadius:10,background:'var(--card)',border:'1px solid var(--border)'}}>{item.note}</div>}

      {ratedBy.length > 0 && (
        <div style={{marginBottom:12}}>
          <div style={{fontSize:10.5,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.12em',marginBottom:6}}>ratings</div>
          <div style={{display:'flex',gap:10,flexWrap:'wrap',fontSize:12.5}}>
            {ratedBy.map(n => (
              <span key={n}><span style={{textTransform:'capitalize',color:'var(--text2)',marginRight:4}}>{n}:</span><span style={{color:'var(--gold)'}}>{'★'.repeat(ratings[n])}</span><span style={{opacity:.3}}>{'★'.repeat(5-ratings[n])}</span></span>
            ))}
          </div>
        </div>
      )}

      {item.comments?.length > 0 && (
        <div style={{marginBottom:12}}>
          <div style={{fontSize:10.5,color:'var(--text3)',textTransform:'uppercase',letterSpacing:'.12em',marginBottom:6}}>comments</div>
          <div style={{display:'flex',flexDirection:'column',gap:6}}>
            {item.comments.map(c => (
              <div key={c.id} style={{padding:'8px 11px',borderRadius:10,background:'var(--card)',border:'1px solid var(--border)'}}>
                <div style={{fontSize:11,color:'var(--text3)',marginBottom:3}}>{c.by}</div>
                <div style={{fontSize:12.5,color:'var(--text)',lineHeight:1.4}}>{c.text}</div>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Do it again — clones to active list, keeps memory */}
      {(() => {
        const me = localStorage.getItem('ft-v2-me') || '';
        const doItAgain = () => {
          const extras = {};
          // Copy portable fields (not status, ratings, comments, reactions, retrospective)
          ['note','link','tags','mood','moods','where','photo','og_image','og_title','og_overview','tmdb_id','tmdb_poster','tmdb_backdrop','tmdb_overview','tmdb_rating','tmdb_year','tmdb_type','rawg_id','rawg_slug','rawg_poster','rawg_backdrop','rawg_overview','rawg_rating','rawg_year','nudge'].forEach(k => {
            if(item[k] != null) extras[k] = item[k];
          });
          // Mark skip flags so enrichers don't re-fetch
          if(item.tmdb_id) extras.tmdb_skip = true;
          if(item.rawg_id) extras.rawg_skip = true;
          if(item.og_image) extras.og_skip = true;
          window.FT?.addItem?.(ctx.cat, item.text, extras, me);
          onClose();
          setTimeout(() => window.FTAPP?.navigate?.('list', ctx.cat), 250);
        };
        return (
          <button onClick={doItAgain} style={{width:'100%',marginTop:12,padding:'11px',borderRadius:11,background:'linear-gradient(135deg,rgba(242,185,136,.15),color-mix(in oklab,var(--rose) 12%,transparent))',border:'1px solid var(--border-accent)',color:'var(--text)',fontSize:13,cursor:'pointer',fontFamily:'inherit',fontWeight:500}}>
            🔁 do it again
          </button>
        );
      })()}

      {/* Retrospective reflection */}
      <div style={{marginTop:14,padding:12,borderRadius:12,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)'}}>
        <div style={{fontSize:10.5,color:'var(--rose)',textTransform:'uppercase',letterSpacing:'.12em',fontWeight:600,marginBottom:6}}>✨ how was it?</div>
        {item.retrospective && !editingReflect ? (
          <div>
            <div style={{fontSize:13,color:'var(--text)',lineHeight:1.55,marginBottom:6,fontStyle:'italic'}}>"{item.retrospective}"</div>
            {item.retrospectiveAt && <div style={{fontSize:10.5,color:'var(--text3)',marginBottom:6}}>added {new Date(item.retrospectiveAt).toLocaleDateString('en',{month:'short',day:'numeric',year:'numeric'})}</div>}
            <div style={{display:'flex',gap:6}}>
              <button onClick={()=>{setEditingReflect(true); setReflectText(item.retrospective);}} className="ft-btn ft-btn-ghost ft-btn-sm">✏️ edit</button>
              <button onClick={clearReflection} className="ft-btn ft-btn-ghost ft-btn-sm">× remove</button>
            </div>
          </div>
        ) : editingReflect || !item.retrospective ? (
          <div>
            <textarea className="ft-textarea" placeholder="Looking back — how was it actually? Favorite part? Would you do it again?" value={reflectText} onChange={e=>setReflectText(e.target.value)} style={{minHeight:70,fontSize:13}} autoCorrect="on" autoCapitalize="sentences"/>
            <div style={{display:'flex',gap:6,marginTop:8}}>
              <Btn variant="solid" size="sm" onClick={saveReflection} disabled={!reflectText.trim()}>save reflection</Btn>
              {editingReflect && <Btn variant="ghost" size="sm" onClick={()=>{setEditingReflect(false); setReflectText('');}}>cancel</Btn>}
            </div>
          </div>
        ) : null}
      </div>
    </Sheet>
  );
}

// ---------- US ----------
function AvatarCropper({ open, onClose, file, onSave, person, onRemove, hasPhoto }){
  const [src, setSrc] = useState(null);
  const [baseScale, setBaseScale] = useState(1); // natural→display scale at zoom=1 (cover)
  const [minZoom, setMinZoom] = useState(0.5);   // how far user can zoom out (contain)
  const [zoom, setZoom] = useState(1);
  const [offsetX, setOffsetX] = useState(0);
  const [offsetY, setOffsetY] = useState(0);
  const [natDim, setNatDim] = useState({w:0, h:0});
  const imgRef = useRef();
  const dragState = useRef(null);

  useEffect(() => {
    if(!file){ setSrc(null); return; }
    // Bake EXIF rotation into pixels so naturalWidth/Height match visual orientation
    (async () => {
      try{
        const bitmap = await createImageBitmap(file, { imageOrientation: 'from-image' });
        const cvs = document.createElement('canvas');
        cvs.width = bitmap.width;
        cvs.height = bitmap.height;
        cvs.getContext('2d').drawImage(bitmap, 0, 0);
        bitmap.close?.();
        setSrc(cvs.toDataURL('image/jpeg', 0.92));
      }catch(e){
        // fallback: read file as-is (may have EXIF issues on old browsers)
        const reader = new FileReader();
        reader.onload = () => setSrc(reader.result);
        reader.readAsDataURL(file);
      }
    })();
    setZoom(1); setOffsetX(0); setOffsetY(0); setMinZoom(0.5); setBaseScale(1); setNatDim({w:0,h:0});
  }, [file]);

  const SIZE = 260;

  const onImgLoad = () => {
    const img = imgRef.current;
    if(!img) return;
    const natW = img.naturalWidth, natH = img.naturalHeight;
    if(!natW || !natH) return;
    // zoom=1 means "cover" (fills circle). zoom=contain/cover means "contain" (whole photo visible).
    const cover = Math.max(SIZE/natW, SIZE/natH);
    const contain = Math.min(SIZE/natW, SIZE/natH);
    setBaseScale(cover);
    setMinZoom(contain / cover); // always ≤1
    setNatDim({w:natW, h:natH});
    setZoom(1);
    setOffsetX(0); setOffsetY(0);
  };

  const onPointerDown = (e) => {
    e.preventDefault();
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch(_){}
    dragState.current = { x: e.clientX, y: e.clientY, ox: offsetX, oy: offsetY };
  };
  const onPointerMove = (e) => {
    if(!dragState.current) return;
    const dx = e.clientX - dragState.current.x;
    const dy = e.clientY - dragState.current.y;
    setOffsetX(dragState.current.ox + dx);
    setOffsetY(dragState.current.oy + dy);
  };
  const onPointerUp = () => { dragState.current = null; };

  const save = () => {
    if(!imgRef.current || !src) return;
    const img = imgRef.current;
    const natW = img.naturalWidth, natH = img.naturalHeight;
    const effRatio = baseScale * zoom;
    const displayW = natW * effRatio;
    const displayH = natH * effRatio;
    const imgLeft = SIZE/2 - displayW/2 + offsetX;
    const imgTop = SIZE/2 - displayH/2 + offsetY;
    // crop area (SIZE×SIZE) in image coords
    const cropLeftOnImg = (0 - imgLeft) / effRatio;
    const cropTopOnImg  = (0 - imgTop) / effRatio;
    const cropSizeOnImg = SIZE / effRatio;

    const outSize = 240;
    const cvs = document.createElement('canvas');
    cvs.width = cvs.height = outSize;
    const ctx = cvs.getContext('2d');
    // If zooming below contain, the image doesn't cover the canvas — fill bg first
    ctx.fillStyle = '#1a1619';
    ctx.fillRect(0, 0, outSize, outSize);
    // Clamp src rect to image bounds; translate dest accordingly
    const sx = Math.max(0, cropLeftOnImg);
    const sy = Math.max(0, cropTopOnImg);
    const sx2 = Math.min(natW, cropLeftOnImg + cropSizeOnImg);
    const sy2 = Math.min(natH, cropTopOnImg + cropSizeOnImg);
    const sw = sx2 - sx;
    const sh = sy2 - sy;
    if(sw > 0 && sh > 0){
      const dx = ((sx - cropLeftOnImg) / cropSizeOnImg) * outSize;
      const dy = ((sy - cropTopOnImg) / cropSizeOnImg) * outSize;
      const dw = (sw / cropSizeOnImg) * outSize;
      const dh = (sh / cropSizeOnImg) * outSize;
      ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
    }
    const dataURL = cvs.toDataURL('image/jpeg', 0.85);
    onSave(dataURL);
    onClose();
  };

  // Computed display dims (in pixels, absolute)
  const displayW = natDim.w * baseScale * zoom;
  const displayH = natDim.h * baseScale * zoom;

  if(!open) return null;
  return (
    <Modal open={open} onClose={onClose} title={`Adjust ${person}'s photo`}>
      {src && (
        <>
          <div style={{position:'relative',width:SIZE,height:SIZE,margin:'0 auto',borderRadius:'50%',overflow:'hidden',background:'#1a1619',touchAction:'none',cursor:'grab',userSelect:'none'}}
            onPointerDown={onPointerDown}
            onPointerMove={onPointerMove}
            onPointerUp={onPointerUp}
            onPointerCancel={onPointerUp}>
            <img ref={imgRef} src={src} alt="" draggable={false} onLoad={onImgLoad}
              style={{
                position:'absolute',
                left: (SIZE - displayW)/2 + offsetX + 'px',
                top:  (SIZE - displayH)/2 + offsetY + 'px',
                width: displayW + 'px',
                height: displayH + 'px',
                pointerEvents:'none',
                display: natDim.w ? 'block' : 'none',
              }}/>
            <div style={{position:'absolute',inset:0,boxShadow:'0 0 0 4000px rgba(0,0,0,.35)',borderRadius:'50%',pointerEvents:'none'}}/>
          </div>
          <div style={{margin:'18px 0 6px',display:'flex',alignItems:'center',gap:10}}>
            <button onClick={()=>setZoom(Math.max(minZoom, zoom-0.1))} style={{width:30,height:30,borderRadius:'50%',border:'1px solid var(--border)',background:'var(--card)',color:'var(--text)',fontSize:16,fontWeight:600,cursor:'pointer',display:'grid',placeItems:'center'}}>−</button>
            <input type="range" min={minZoom} max="3" step="0.01" value={zoom} onChange={e=>setZoom(parseFloat(e.target.value))} style={{flex:1,accentColor:'var(--rose, #E8A5C0)'}}/>
            <button onClick={()=>setZoom(Math.min(3, zoom+0.1))} style={{width:30,height:30,borderRadius:'50%',border:'1px solid var(--border)',background:'var(--card)',color:'var(--text)',fontSize:16,fontWeight:600,cursor:'pointer',display:'grid',placeItems:'center'}}>+</button>
          </div>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14,fontSize:11,color:'var(--text3)'}}>
            <button onClick={()=>{ setZoom(minZoom); setOffsetX(0); setOffsetY(0); }} style={{background:'transparent',border:'none',color:'var(--lavender)',fontSize:11,cursor:'pointer',padding:0,fontFamily:'inherit',textDecoration:'underline'}}>fit whole photo</button>
            <span>drag to reposition</span>
            <button onClick={()=>{ setZoom(1); setOffsetX(0); setOffsetY(0); }} style={{background:'transparent',border:'none',color:'var(--lavender)',fontSize:11,cursor:'pointer',padding:0,fontFamily:'inherit',textDecoration:'underline'}}>fill circle</button>
          </div>
          <div style={{display:'flex',gap:8}}>
            <Btn variant="solid" size="md" style={{flex:1}} onClick={save}>Save photo</Btn>
            <Btn variant="ghost" size="md" onClick={onClose}>Cancel</Btn>
          </div>
          {hasPhoto && onRemove && (
            <div style={{marginTop:12,paddingTop:12,borderTop:'1px dashed var(--border)',textAlign:'center'}}>
              <button onClick={()=>{ if(confirm(`Remove ${person}'s photo?`)){ onRemove(); onClose(); } }} style={{background:'transparent',border:'none',color:'var(--rose)',fontSize:12,cursor:'pointer',padding:'6px 12px',fontFamily:'inherit'}}>Remove current photo</button>
            </div>
          )}
        </>
      )}
    </Modal>
  );
}

function UsScreen({ me, onSwitchUser }){
  const profiles = useProfiles();
  const d = useData();
  const activity = useActivity();
  const partners = usePartners();
  const [p0, p1] = partners;
  const k0 = FT.normKey(p0||'');
  const k1 = FT.normKey(p1||'');
  const [streaks, setStreaks] = useState({});

  useEffect(() => {
    const unsub = window.FT.db.collection('lists').doc('streaks_v2').onSnapshot(doc => {
      if(doc.exists) setStreaks(doc.data());
    });
    return unsub;
  }, []);

  const stats = useMemo(()=>{
    if(!d) return {total:0,done:0,byL:0,byO:0,withNotes:0,commented:0};
    let total=0, done=0, byL=0, byO=0, withNotes=0, commented=0;
    Object.values(d).forEach(c => c.items.forEach(i => {
      total++;
      if(i.done) done++;
      const cb = FT.normKey(i.completedBy||'');
      if(cb && cb === k0) byL++;
      else if(cb && cb === k1) byO++;
      if(i.note) withNotes++;
      if(i.comments?.length) commented++;
    }));
    return {total, done, byL, byO, withNotes, commented};
  }, [d, k0, k1]);

  // Couple-wide achievements (non-game)
  const achievements = useMemo(() => {
    const a = (ico, title, sub, unlocked) => ({ico, title, sub, unlocked});
    const items = [];
    items.push(a('🌱','First step','check off your first thing together',stats.done>=1));
    items.push(a('✨','Ten-down','10 things completed',stats.done>=10));
    items.push(a('🎉','Fifty-fifty','50 things together',stats.done>=50));
    items.push(a('💯','Century','100 adventures done',stats.done>=100));
    items.push(a('🎯','Balanced pair','both of you checked off 5+',stats.byL>=5 && stats.byO>=5));
    items.push(a('💬','Chatty','a comment on 5 items',stats.commented>=5));
    items.push(a('📝','Note-taker','5 items with notes',stats.withNotes>=5));
    items.push(a('🔥','On fire','3-day check-off streak',(streaks[k0]?.count>=3)||(streaks[k1]?.count>=3)));
    items.push(a('🚀','Week warrior','7-day check-off streak',(streaks[k0]?.count>=7)||(streaks[k1]?.count>=7)));
    return items;
  }, [stats, streaks, k0, k1]);
  const unlockedCount = achievements.filter(a=>a.unlocked).length;

  const [cropCtx, setCropCtx] = useState(null); // { person, file }
  const [streaksOpen, setStreaksOpen] = useState(() => { try { return localStorage.getItem('ft-us-streaks-open') !== '0'; } catch(e){ return true; } });
  const [achOpen, setAchOpen] = useState(() => { try { return localStorage.getItem('ft-us-ach-open') !== '0'; } catch(e){ return true; } });
  useEffect(() => { try { localStorage.setItem('ft-us-streaks-open', streaksOpen?'1':'0'); } catch(e){} }, [streaksOpen]);
  useEffect(() => { try { localStorage.setItem('ft-us-ach-open', achOpen?'1':'0'); } catch(e){} }, [achOpen]);

  const onFile = (person, e) => {
    const file = e.target.files?.[0];
    if(!file) return;
    setCropCtx({ person, file });
    e.target.value = ''; // reset so same file re-selects
  };

  const removePhoto = (person) => {
    if(!confirm(`Remove ${person}'s photo?`)) return;
    FT.saveProfile(person, { photo: null, name: person });
  };

  const saveCrop = (dataURL) => {
    if(!cropCtx) return;
    FT.saveProfile(cropCtx.person, { photo: dataURL, name: cropCtx.person });
  };

  if(!p0 || !p1) return <div className="ft-screen"><div className="ft-screen-title">Us</div></div>;

  const PartnerCard = ({ name, count }) => {
    const k = FT.normKey(name);
    return (
      <div className="ft-profile-name">
        <label className="ft-av-upload">
          <Avatar person={name} size={68} ring={FT.normKey(me)===k}/>
          <span className="ft-av-badge"><Icon.camera/></span>
          <input type="file" accept="image/*" onChange={e=>onFile(name,e)}/>
        </label>
        <div className="ft-profile-name-text">{name}</div>
        <div className="ft-profile-name-role">{count} checked off</div>
        {profiles?.[k]?.photo && (
          <button onClick={()=>removePhoto(name)} className="ft-remove-photo-btn">× remove photo</button>
        )}
      </div>
    );
  };

  return (
    <div className="ft-screen">
      <div className="ft-screen-title">Us</div>
      <div className="ft-screen-sub">{p0} <span style={{color:'var(--text3)'}}>&</span> {p1}</div>

      <Card hoverable={false} className="ft-profile-card">
        <div className="ft-profile-names">
          <PartnerCard name={p0} count={stats.byL}/>
          <div className="ft-profile-amp">&</div>
          <PartnerCard name={p1} count={stats.byO}/>
        </div>
        <div style={{marginTop:8,display:'flex',justifyContent:'center'}}>
          <Squiggle width={80} color="var(--rose)"/>
        </div>
        <div style={{marginTop:14,fontSize:12,color:'var(--text2)'}}>
          you're signed in as <b style={{color:'var(--text)'}}>{me}</b> · <button onClick={onSwitchUser} style={{color:'var(--rose)',fontSize:12,padding:0}}>switch</button>
        </div>
      </Card>

      <div className="ft-section-label">our numbers</div>
      <div className="ft-stat-row">
        <div className="ft-stat"><div className="ft-stat-n">{stats.done}</div><div className="ft-stat-l">Done</div></div>
        <div className="ft-stat"><div className="ft-stat-n">{stats.total - stats.done}</div><div className="ft-stat-l">To go</div></div>
        <div className="ft-stat"><div className="ft-stat-n">{activity.length}</div><div className="ft-stat-l">Moments</div></div>
      </div>

      <div onClick={()=>setStreaksOpen(o=>!o)} className="ft-section-label" style={{cursor:'pointer',userSelect:'none'}} role="button" tabIndex={0}>
        daily check-off streaks
        <span style={{color:'var(--text3)',fontWeight:400,fontSize:11,textTransform:'none',letterSpacing:'normal',marginLeft:6}}>{streaksOpen?'▴':'▾'}</span>
      </div>
      {streaksOpen && (
        <div className="ft-streak-row">
          <div className="ft-streak-card">
            <Avatar person={p0} size={36}/>
            <div className="ft-streak-flame">🔥</div>
            <div className="ft-streak-n">{streaks[k0]?.count||0}</div>
            <div className="ft-streak-l">{(streaks[k0]?.count||0)===1?'day':'days'} {streaks[k0]?.best ? `· best ${streaks[k0].best}`:''}</div>
          </div>
          <div className="ft-streak-card">
            <Avatar person={p1} size={36}/>
            <div className="ft-streak-flame">🔥</div>
            <div className="ft-streak-n">{streaks[k1]?.count||0}</div>
            <div className="ft-streak-l">{(streaks[k1]?.count||0)===1?'day':'days'} {streaks[k1]?.best ? `· best ${streaks[k1].best}`:''}</div>
          </div>
        </div>
      )}

      <div onClick={()=>setAchOpen(o=>!o)} className="ft-section-label" style={{cursor:'pointer',userSelect:'none'}} role="button" tabIndex={0}>
        couple achievements
        <span style={{color:'var(--text3)',fontWeight:400,textTransform:'none',letterSpacing:'normal',marginLeft:6}}>{unlockedCount}/{achievements.length}</span>
        <span style={{color:'var(--text3)',fontWeight:400,fontSize:11,textTransform:'none',letterSpacing:'normal',marginLeft:6}}>{achOpen?'▴':'▾'}</span>
      </div>
      {achOpen && (
        <div className="ft-ach-grid">
          {achievements.map((a,i) => (
            <div key={i} className={`ft-ach ${a.unlocked?'on':''}`} title={a.sub}>
              <div className="ft-ach-ico">{a.unlocked ? a.ico : '🔒'}</div>
              <div className="ft-ach-ttl">{a.title}</div>
              <div className="ft-ach-sub">{a.sub}</div>
            </div>
          ))}
        </div>
      )}

      <div className="ft-section-label">also here</div>
      <div style={{display:'grid',gap:8}}>
        <Card onClick={()=>window.FTAPP?.openNotesHistory?.()}><div style={{display:'flex',alignItems:'center',gap:12}}><div style={{fontSize:22}}>💌</div><div><div style={{fontWeight:600,fontSize:14}}>Notes we've left</div><div style={{fontSize:12,color:'var(--text3)'}}>Little love notes, saved</div></div></div></Card>
        <Card onClick={()=>window.FTAPP?.openFeedback?.()}><div style={{display:'flex',alignItems:'center',gap:12}}><div style={{fontSize:22}}>📨</div><div><div style={{fontWeight:600,fontSize:14}}>Send Lauren a note</div><div style={{fontSize:12,color:'var(--text3)'}}>Bugs, ideas, wishes — straight to her phone</div></div></div></Card>
        <Card onClick={()=>window.FTAPP?.openSettings?.()}><div style={{display:'flex',alignItems:'center',gap:12}}><div style={{fontSize:22}}>⚙️</div><div><div style={{fontWeight:600,fontSize:14}}>Settings</div><div style={{fontSize:12,color:'var(--text3)'}}>Notifications, theme, sync</div></div></div></Card>
      </div>
      <AvatarCropper
        open={!!cropCtx}
        file={cropCtx?.file}
        person={cropCtx?.person}
        hasPhoto={!!(profiles?.[cropCtx?.person?.toLowerCase?.().replace('é','e')]?.photo)}
        onRemove={()=>FT.saveProfile(cropCtx.person, { photo: null, name: cropCtx.person })}
        onClose={()=>setCropCtx(null)}
        onSave={saveCrop}
      />
    </div>
  );
}

// ---------- PLAY (check-off streaks + achievements) ----------
function PlayScreen({ me }){
  const [streaks, setStreaks] = useState({});
  const d = useData();
  const partners = FT.usePartners ? FT.usePartners() : [];
  const [p0, p1] = partners;
  const k0 = (p0||'').toLowerCase().replace('é','e');
  const k1 = (p1||'').toLowerCase().replace('é','e');

  useEffect(() => {
    const unsub = window.FT.db.collection('lists').doc('streaks_v2').onSnapshot(doc => {
      if(doc.exists) setStreaks(doc.data());
    });
    return unsub;
  }, []);

  // Achievements from the full data
  const achievements = useMemo(() => {
    if(!d) return [];
    const items = [];
    let total=0, doneCount=0, by0=0, by1=0, withNotes=0, commented=0;
    Object.values(d).forEach(c => c.items.forEach(i => {
      total++;
      if(i.done){ doneCount++; }
      const cb = (i.completedBy||'').toLowerCase().replace('é','e');
      if(cb && cb === k0) by0++;
      else if(cb && cb === k1) by1++;
      if(i.note) withNotes++;
      if(i.comments?.length) commented++;
    }));
    const a = (ico, title, sub, unlocked) => ({ico, title, sub, unlocked});
    items.push(a('🌱','First step','check off your first thing together',doneCount>=1));
    items.push(a('✨','Ten-down','10 things completed',doneCount>=10));
    items.push(a('🎉','Fifty-fifty','50 things together',doneCount>=50));
    items.push(a('💯','Century','100 adventures done',doneCount>=100));
    items.push(a('🎯','Balanced pair','both of you checked off 5+',by0>=5&&by1>=5));
    items.push(a('💬','Chatty','a comment on 5 items',commented>=5));
    items.push(a('📝','Note-taker','5 items with notes',withNotes>=5));
    items.push(a('🔥','On fire','3-day streak',(streaks[k0]?.count>=3)||(streaks[k1]?.count>=3)));
    items.push(a('🚀','Week warrior','7-day streak',(streaks[k0]?.count>=7)||(streaks[k1]?.count>=7)));
    return items;
  }, [d, streaks, k0, k1]);
  const unlockedCount = achievements.filter(a=>a.unlocked).length;

  if(!p0 || !p1) return <div className="ft-screen"><div className="ft-screen-title">Play</div></div>;

  return (
    <div className="ft-screen">
      <div className="ft-screen-title">Play</div>
      <div className="ft-screen-sub">the streaks, the wins</div>

      {/* Check-off streaks */}
      <div className="ft-section-label">check-off streaks</div>
      <div className="ft-streak-row">
        <div className="ft-streak-card">
          <Avatar person={p0} size={36}/>
          <div className="ft-streak-flame">🔥</div>
          <div className="ft-streak-n">{streaks[k0]?.count||0}</div>
          <div className="ft-streak-l">days {streaks[k0]?.best ? `· best ${streaks[k0].best}`:''}</div>
        </div>
        <div className="ft-streak-card">
          <Avatar person={p1} size={36}/>
          <div className="ft-streak-flame">🔥</div>
          <div className="ft-streak-n">{streaks[k1]?.count||0}</div>
          <div className="ft-streak-l">days {streaks[k1]?.best ? `· best ${streaks[k1].best}`:''}</div>
        </div>
      </div>

      {/* Achievements */}
      <div className="ft-section-label">achievements <span style={{color:'var(--text3)',fontWeight:400,textTransform:'none',letterSpacing:'normal',marginLeft:6}}>{unlockedCount}/{achievements.length}</span></div>
      <div className="ft-ach-grid">
        {achievements.map((a,i) => (
          <div key={i} className={`ft-ach ${a.unlocked?'on':''}`} title={a.sub}>
            <div className="ft-ach-ico">{a.unlocked ? a.ico : '🔒'}</div>
            <div className="ft-ach-ttl">{a.title}</div>
            <div className="ft-ach-sub">{a.sub}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

window.FTScreens = { HomeScreen, ListScreen, TalkScreen, MemoriesScreen, UsScreen, PlayScreen, CAT_META, catMeta, timeAgo };
