/* Nutri Movie Night — UI (browse · cards · lists · liked · watched + sheets).
   Discovery powered by TMDb (window.TMDB). Library/credentials live in the Store.
   Excludes watched + disliked from discovery by default. Keeps added/imported/watched
   dates. Links watched movies to Diary memories by date (never modifies diary text).

   Components read the Store directly via useStore() (same pattern as the other screens);
   sheet opening + toasts are passed down from NutriShell. */

const MOVIE_ACCENT = '#C25A4E';
const MOVIE_STATUS_META = {
  new:       { label: 'New',        emoji: '🎬', color: 'var(--muted)' },
  liked:     { label: 'Liked',      emoji: '❤️', color: '#C2554E' },
  disliked:  { label: 'Disliked',   emoji: '👎', color: 'var(--muted)' },
  watchlist: { label: 'Watchlist',  emoji: '🔖', color: '#5784D8' },
};
const MOVIE_SORTS = [
  { id: 'popularity', label: 'Popular',  tmdb: 'popularity.desc' },
  { id: 'rating',     label: 'Top rated',tmdb: 'vote_average.desc' },
  { id: 'release',    label: 'Newest',   tmdb: 'primary_release_date.desc' },
  { id: 'title',      label: 'A–Z',      tmdb: 'original_title.asc' },
];

/* ── pure helpers ─────────────────────────────────────────────────────── */
function movieIsWatched(m) { return !!(m && m.watchedDate); }
function moviePrimaryDate(m) {
  return (m && (m.watchedDate || m.sourceAddedDateFromCSV || m.importedDate || m.addedDate)) || '';
}
function _normTitle(t) { return (t || '').toString().toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); }
// userRating may be legacy 1–10 (IMDb CSV) or new 1–5 → always returns a 1–5 star value (0 = unrated).
function movieStarValue(m) {
  const r = m && (typeof m === 'object' ? m.userRating : m);
  if (r == null || r === '') return 0;
  const n = +r; if (isNaN(n)) return 0;
  return n > 5 ? Math.max(1, Math.min(5, Math.round(n / 2))) : Math.max(1, Math.min(5, Math.round(n)));
}
function movieIsLiked(m) { return !!(m && (m.liked || movieStarValue(m) === 5)); }   // explicit OR 5-star
function movieIsDisliked(m) { return !!(m && m.disliked); }
// Build a lookup index over the library: by tmdbId, imdbId, and normalized title+year.
function movieLibIndex(movies) {
  const byTmdb = {}, byImdb = {}, byTY = {};
  (movies || []).forEach(m => {
    if (m.tmdbId != null) byTmdb[m.tmdbId] = m;
    if (m.imdbId) byImdb[m.imdbId] = m;
    const nt = _normTitle(m.title); if (nt && m.year != null) byTY[nt + '|' + m.year] = m;
  });
  return { byTmdb, byImdb, byTY };
}
// Resolve a meta/library object to its library record (tmdbId → imdbId → title+year ±1).
function resolveLibMovie(meta, idx) {
  if (!meta || !idx) return null;
  if (meta.id && String(meta.id).slice(0, 4) === 'mov_' && (meta.userStatus !== undefined || meta.addedDate !== undefined)) return meta;
  if (meta.tmdbId != null && idx.byTmdb[meta.tmdbId]) return idx.byTmdb[meta.tmdbId];
  if (meta.imdbId && idx.byImdb[meta.imdbId]) return idx.byImdb[meta.imdbId];
  const nt = _normTitle(meta.title); if (!nt) return null;
  for (const y of [meta.year, meta.year - 1, meta.year + 1]) { if (y != null && idx.byTY[nt + '|' + y]) return idx.byTY[nt + '|' + y]; }
  return null;
}
function _mdy(iso) {
  if (!iso) return '';
  try { return new Date(iso + 'T00:00:00').toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); }
  catch (_) { return iso; }
}
function _ratingStr(r) { return r == null ? '–' : (+r).toFixed(1); }
function _compactNum(n) {
  if (n == null) return '';
  if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
  if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'k';
  return String(n);
}
// Real IMDb rating (from OMDb) if present — kept SEPARATE from tmdbRating + userRating.
function movieImdbRating(m) { return (m && m.imdbRating != null) ? m.imdbRating : null; }

/* ── central recommendation-exclusion set (spec §3) ───────────────────────
   ONE place that decides which movies are hidden from discovery — used by Browse,
   Cards, For You, Best of Year, Similar and search. Default = watched + disliked.
   Returns key sets (tmdbId / imdbId / normalized title+year ±1) so a candidate is
   matched even when it has no tmdbId (e.g. a CSV-imported watched movie). */
function getExcludedMovieKeys(movies, opts) {
  const o = opts || {};
  const tmdb = new Set(), imdb = new Set(), ty = new Set();
  (movies || []).forEach(m => {
    const excluded = (m.watchedDate && !o.includeWatched)
      || (m.disliked && !o.includeDisliked)
      || (o.excludeLiked && movieIsLiked(m))                       // Cards: don't re-show liked
      || (o.excludeWatchlist && m.watchlist && !m.watchedDate);    // Cards: don't re-show watchlisted
    if (!excluded) return;
    if (m.tmdbId != null) tmdb.add(m.tmdbId);
    if (m.imdbId) imdb.add(m.imdbId);
    const nt = _normTitle(m.title);
    if (nt && m.year != null) { ty.add(nt + '|' + m.year); ty.add(nt + '|' + (m.year - 1)); ty.add(nt + '|' + (m.year + 1)); }
  });
  return { tmdb, imdb, ty };
}
/* ── Cards session (persists across tab switches within a session) ─────────
   Keeps the candidate queue, the current position, AND a set of movies already
   acted on this session so swiping never re-shows them (spec §6/§7). Module-level
   so it survives the component unmounting when you switch bottom tabs. */
const _cardSession = { deck: null, acted: null };
function _movieKeysOf(m) {
  const ks = [];
  if (m.tmdbId != null) ks.push('t:' + m.tmdbId);
  if (m.imdbId) ks.push('i:' + m.imdbId);
  const nt = _normTitle(m.title); if (nt && m.year != null) ks.push('n:' + nt + '|' + m.year);
  return ks;
}
function _cardActedSet() { if (!_cardSession.acted) _cardSession.acted = new Set(); return _cardSession.acted; }
function _isCardActed(m) { const s = _cardActedSet(); return _movieKeysOf(m).some(k => s.has(k)); }
function _markCardActed(m) { const s = _cardActedSet(); _movieKeysOf(m).forEach(k => s.add(k)); }
function resetCardSession() { _cardSession.deck = null; _cardSession.acted = new Set(); }
function movieKeyExcluded(meta, keys) {
  if (!meta || !keys) return false;
  if (meta.tmdbId != null && keys.tmdb.has(meta.tmdbId)) return true;
  if (meta.imdbId && keys.imdb.has(meta.imdbId)) return true;
  const nt = _normTitle(meta.title);
  if (nt && meta.year != null && keys.ty.has(nt + '|' + meta.year)) return true;
  return false;
}

/* ── tiny module cache so switching tabs doesn't refetch everything (spec §4) ──
   Keyed results with a TTL. Browse/For You/Cards/Best/Similar read cache first and
   fetch in the background only when stale or on an explicit Refresh. */
const _MOVIE_CACHE_TTL = 10 * 60 * 1000; // 10 min
const _movieCache = {};
function movieCacheGet(key, maxAge) {
  const e = _movieCache[key];
  if (!e) return null;
  if (Date.now() - e.at > (maxAge != null ? maxAge : _MOVIE_CACHE_TTL)) return null;
  return e.val;
}
function movieCacheSet(key, val) { _movieCache[key] = { at: Date.now(), val }; return val; }
function movieCacheDelete(key) { delete _movieCache[key]; }
function movieCacheClear() { Object.keys(_movieCache).forEach(k => delete _movieCache[k]); if (typeof resetCardSession === 'function') resetCardSession(); }

/* ── movie-specific icons ─────────────────────────────────────────────── */
const FilmIcon  = ({size}) => <Icon size={size}><rect x="2.5" y="4" width="19" height="16" rx="2"/><path d="M7 4v16M17 4v16M2.5 9h4.5M2.5 15h4.5M17 9h4.5M17 15h4.5"/></Icon>;
const HeartIcon = ({size, fill}) => <svg width={size} height={size} viewBox="0 0 24 24" fill={fill || 'none'} stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 1 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8z"/></svg>;
const ThumbDownIcon = ({size, fill}) => <svg width={size} height={size} viewBox="0 0 24 24" fill={fill || 'none'} stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M17 2h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2M10 22l-1-7H4a2 2 0 0 1-2-2.3l1.2-7A2 2 0 0 1 5.2 2H14a2 2 0 0 1 2 2v8l-6 10z"/></svg>;
const BookmarkIcon = ({size, fill}) => <svg width={size} height={size} viewBox="0 0 24 24" fill={fill || 'none'} stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M6 3h12a1 1 0 0 1 1 1v17l-7-4-7 4V4a1 1 0 0 1 1-1z"/></svg>;
const EyeIcon   = ({size}) => <Icon size={size}><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></Icon>;
const StarIconM = ({size, fill}) => <svg width={size} height={size} viewBox="0 0 24 24" fill={fill || 'none'} stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 15.1 8.6 22 9.3 16.8 14 18.3 21 12 17.3 5.7 21 7.2 14 2 9.3 8.9 8.6 12 2"/></svg>;
const ListPlusIcon = ({size}) => <Icon size={size}><path d="M3 6h11M3 12h8M3 18h8M16 15h6M19 12v6"/></Icon>;
const TrashIconM = ({size}) => <Icon size={size}><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></Icon>;
const PlayIcon  = ({size}) => <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>;

/* ── poster resolution — one place all surfaces build a poster URL ────────
   Prefers a stored posterUrl, else builds one from posterPath (fixes imported /
   liked / watchlist movies that have a path but no pre-built URL), else a cached
   URL, else null → graceful placeholder. */
function moviePosterUrl(movie) {
  if (!movie) return null;
  if (movie.posterUrl) return movie.posterUrl;
  if (movie.posterPath && window.TMDB && TMDB.posterUrl) return TMDB.posterUrl(movie.posterPath);
  if (movie.cachedPosterUrl) return movie.cachedPosterUrl;
  return null;
}
/* ── poster with graceful placeholder ─────────────────────────────────── */
function MoviePoster({ movie, w, h, radius }) {
  const [err, setErr] = React.useState(false);
  const url = moviePosterUrl(movie);
  React.useEffect(() => { setErr(false); }, [url]);
  const style = {
    width: w || '100%', height: h || 'auto', aspectRatio: (w && h) ? undefined : '2/3',
    borderRadius: radius != null ? radius : 12, objectFit: 'cover', display: 'block',
    background: 'var(--surface-3)', flexShrink: 0,
  };
  if (url && !err) {
    return <img src={url} alt={(movie && movie.title) || ''} loading="lazy" onError={() => setErr(true)} style={style}/>;
  }
  return (
    <div style={{ ...style, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
      gap: 6, color: 'var(--muted-2)', border: '1px solid var(--border)', padding: 8, textAlign: 'center' }}>
      <FilmIcon size={Math.min(34, (typeof w === 'number' ? w : 90) * 0.34)}/>
      <span style={{ fontSize: 10, color: 'var(--muted)', lineHeight: 1.2, maxHeight: 28, overflow: 'hidden' }}>
        {(movie && movie.title) || 'No poster'}
      </span>
    </div>
  );
}

/* ── Firebase save/sync status banner (spec §16) — only shows on error/offline ── */
function MovieSyncBanner() {
  const store = useStore();
  const status = store.syncStatus && store.syncStatus();
  if (status !== 'error' && status !== 'offline') return null;
  const offline = status === 'offline';
  return (
    <div style={{ margin: '10px 14px 0', padding: '9px 12px', borderRadius: 12,
      background: offline ? 'var(--surface-2)' : 'rgba(194,85,78,0.12)',
      border: '1px solid ' + (offline ? 'var(--border-2)' : 'rgba(194,85,78,0.4)'),
      display: 'flex', alignItems: 'center', gap: 10, fontSize: 12.5 }}>
      <span style={{ flex: 1, color: offline ? 'var(--muted)' : '#C2554E', fontWeight: 600, lineHeight: 1.4 }}>
        {offline ? '⚠ Offline — saved on this device, will sync when you reconnect.' : '⚠ Could not save. Please try again.'}
      </span>
      {!offline && <button onClick={() => store.retryMovieSync()} style={subtleBtn}>Retry</button>}
    </div>
  );
}

/* ── connect-TMDb gate ────────────────────────────────────────────────── */
function MovieConnectPrompt({ onOpenSettings }) {
  return (
    <div style={{ padding: 16 }}>
      <div style={{ ...nutriStyles.card, padding: '26px 18px', textAlign: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
        <div style={{ fontSize: 34 }}>🎬</div>
        <div style={{ fontWeight: 700, fontSize: 16 }}>Connect TMDb to discover movies</div>
        <div style={{ fontSize: 12.5, color: 'var(--muted)', maxWidth: 280, lineHeight: 1.5 }}>
          Movie Night is powered by The Movie Database. Add your free API key or Read Access Token to search,
          browse and get recommendations. Your liked & watched lists work offline.
        </div>
        <button onClick={onOpenSettings} style={primaryBtn}>Open Movie Night Settings</button>
        <div style={{ fontSize: 10.5, color: 'var(--muted-2)', marginTop: 4, maxWidth: 280 }}>{TMDB.ATTRIBUTION}</div>
      </div>
    </div>
  );
}

/* ── reusable movie-link picker (used by Diary) — optional, searches the
   library (watched/liked/lists/all) first, then TMDb if connected ──────── */
function MovieLinkField({ value, onChange, label }) {
  const store = useStore();
  const movies = store.movies();
  const selected = value ? movies.find(m => m.id === value) : null;
  const [open, setOpen] = React.useState(false);
  const [q, setQ] = React.useState('');
  const [tmdbResults, setTmdbResults] = React.useState([]);
  const [searching, setSearching] = React.useState(false);
  const timer = React.useRef(0);
  const hasCreds = TMDB.hasCredentials();
  const ql = q.trim().toLowerCase();
  const libMatches = ql ? movies.filter(m => (m.title || '').toLowerCase().includes(ql)).slice(0, 8) : [];

  React.useEffect(() => {
    clearTimeout(timer.current);
    if (!open || !ql || !hasCreds) { setTmdbResults([]); return; }
    timer.current = setTimeout(() => {
      setSearching(true);
      TMDB.search(q.trim()).then(r => {
        const libTmdb = new Set(movies.filter(m => m.tmdbId != null).map(m => m.tmdbId));
        setTmdbResults((r.results || []).filter(x => !libTmdb.has(x.tmdbId)).slice(0, 8));
      }).catch(() => setTmdbResults([])).finally(() => setSearching(false));
    }, 350);
    return () => clearTimeout(timer.current);
  }, [q, open, hasCreds]);

  function pickLib(m) { onChange(m.id); setOpen(false); setQ(''); }
  function pickTmdb(meta) { const id = store.ensureMovie(meta, 'diary'); onChange(id); setOpen(false); setQ(''); }

  const lbl = label || 'Movie for this day';
  if (selected) {
    return (
      <div>
        <div style={miniLabel}>{lbl}</div>
        <div style={{ display: 'flex', gap: 10, alignItems: 'center', ...nutriStyles.card, padding: 8 }}>
          <MoviePoster movie={selected} w={40}/>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontWeight: 600, fontSize: 13.5 }}>{selected.title}</div>
            <div style={{ fontSize: 11.5, color: 'var(--muted)' }}>{selected.year || ''}{selected.watchedDate ? ' · ✓ watched' : (selected.watchlist ? ' · planned' : '')}{movieIsLiked(selected) ? ' · ❤️' : ''}</div>
          </div>
          <button onClick={() => onChange(null)} style={chipBtn}>Remove</button>
        </div>
      </div>
    );
  }
  return (
    <div>
      <div style={miniLabel}>{lbl} <span style={{ textTransform: 'none', fontWeight: 400, color: 'var(--muted-2)' }}>· optional</span></div>
      {!open ? (
        <button onClick={() => setOpen(true)} style={{ ...subtleBtn, display: 'inline-flex', alignItems: 'center', gap: 6 }}>🎬 Link a movie</button>
      ) : (
        <div style={{ ...nutriStyles.card, padding: 10 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 10, padding: '7px 10px', marginBottom: 8 }}>
            <SearchIcon size={15}/>
            <input autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder="Search your movies or TMDb…" style={{ flex: 1, border: 0, background: 'transparent', outline: 'none', font: 'inherit', fontSize: 13.5, color: 'var(--text)' }}/>
            <button onClick={() => { setOpen(false); setQ(''); }} style={{ background: 'none', border: 0, color: 'var(--muted)', cursor: 'pointer' }}><XIcon size={14}/></button>
          </div>
          {libMatches.length > 0 && <div style={pickHdr}>Your movies</div>}
          {libMatches.map(m => (
            <button key={m.id} onClick={() => pickLib(m)} style={pickRow}>
              <MoviePoster movie={m} w={30}/>
              <span style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 600 }}>{m.title}<span style={{ color: 'var(--muted)', fontWeight: 400 }}>{m.year ? ' · ' + m.year : ''}</span>{m.watchedDate ? ' ✓' : ''}{movieIsLiked(m) ? ' ❤️' : ''}</span>
            </button>
          ))}
          {hasCreds && tmdbResults.length > 0 && <div style={pickHdr}>TMDb</div>}
          {hasCreds && tmdbResults.map(m => (
            <button key={'t' + m.tmdbId} onClick={() => pickTmdb(m)} style={pickRow}>
              <MoviePoster movie={m} w={30}/>
              <span style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 600 }}>{m.title}<span style={{ color: 'var(--muted)', fontWeight: 400 }}>{m.year ? ' · ' + m.year : ''}</span></span>
            </button>
          ))}
          {searching && <div style={{ fontSize: 12, color: 'var(--muted)', padding: '4px 2px' }}>Searching…</div>}
          {!searching && ql && libMatches.length === 0 && tmdbResults.length === 0 && (
            <div style={{ fontSize: 12, color: 'var(--muted)', padding: '4px 2px' }}>No matches{!hasCreds ? ' — connect TMDb in Movie Night to search online.' : '.'}</div>
          )}
        </div>
      )}
    </div>
  );
}
const pickRow = { width: '100%', display: 'flex', alignItems: 'center', gap: 8, padding: '6px 4px', background: 'transparent', border: 0, borderRadius: 8, cursor: 'pointer', font: 'inherit', color: 'var(--text)', textAlign: 'left' };
const pickHdr = { fontSize: 10.5, color: 'var(--muted)', margin: '4px 0', textTransform: 'uppercase', letterSpacing: '0.04em' };

/* ── interactive 1–5 star rating ──────────────────────────────────────── */
function MovieStars({ value, onChange, size }) {
  const v = movieStarValue(value);   // 1–5 (interprets legacy 1–10); 0 = unrated
  const s = size || 22;
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
      {[1,2,3,4,5].map(i => {
        const filled = i <= v;
        return (
          <button key={i} onClick={() => onChange(v === i ? null : i)} style={{
            background: 'none', border: 0, padding: 1, cursor: 'pointer',
            color: filled ? '#E89B3C' : 'var(--muted-2)', lineHeight: 0,
          }} aria-label={'Rate ' + i}>
            <StarIconM size={s} fill={filled ? '#E89B3C' : 'none'}/>
          </button>
        );
      })}
      {v > 0 && (
        <span style={{ fontSize: 12, color: 'var(--muted)', marginLeft: 6 }}>{v}/5
          <button onClick={() => onChange(null)} style={{ background: 'none', border: 0, color: 'var(--muted-2)', cursor: 'pointer', fontSize: 12, marginLeft: 4 }}>clear</button>
        </span>
      )}
    </div>
  );
}

/* ── compact action buttons (like / dislike / watched / list) ─────────────
   Like/dislike are independent flags; watched is watchedDate. All go through the
   Store so every tab + the assistant stay in sync. */
function MovieActions({ meta, source, onPickList, flashToast }) {
  const store = useStore();
  const idx = movieLibIndex(store.movies());
  const lib = resolveLibMovie(meta, idx);
  const liked = movieIsLiked(lib);
  const disliked = movieIsDisliked(lib);
  const watched = movieIsWatched(lib);

  function ensure() { return (lib && lib.id) || store.ensureMovie(meta, source || 'browse'); }
  function like()    { store.toggleMovieLiked(ensure());    flashToast && flashToast(liked ? 'Like removed' : 'Liked ❤️'); }
  function dislike() { store.toggleMovieDisliked(ensure()); flashToast && flashToast(disliked ? 'Cleared' : 'Nope — hidden from picks'); }
  function watchNow(){ const id = ensure(); if (watched) { store.unmarkWatched(id); flashToast && flashToast('Removed from watched'); } else { store.markWatched(id); flashToast && flashToast('Marked watched ✓'); } }
  function pickList() { onPickList && onPickList(ensure()); }

  const btn = (active, activeColor) => ({
    flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
    padding: '8px 0', borderRadius: 12, cursor: 'pointer', font: 'inherit', fontSize: 10, fontWeight: 600,
    background: active ? activeColor : 'var(--surface-2)',
    color: active ? '#fff' : 'var(--text)',
    border: '1px solid ' + (active ? activeColor : 'var(--border)'),
    transition: 'background .12s, color .12s, border-color .12s',
  });
  return (
    <div style={{ display: 'flex', gap: 6 }}>
      <button onClick={like} style={btn(liked, '#C2554E')}>
        <HeartIcon size={17} fill={liked ? '#fff' : 'none'}/>{liked ? 'Liked' : 'Like'}
      </button>
      <button onClick={dislike} style={btn(disliked, 'var(--muted)')}>
        <ThumbDownIcon size={17} fill={disliked ? '#fff' : 'none'}/>Nope
      </button>
      <button onClick={watchNow} style={btn(watched, '#4FA862')}>
        {watched ? <CheckIcon size={17}/> : <EyeIcon size={17}/>}{watched ? 'Watched' : 'Watch'}
      </button>
      <button onClick={pickList} style={btn(false)}>
        <ListPlusIcon size={17}/>List
      </button>
    </div>
  );
}

/* ── IMDb-only rating badge for movie CARDS (spec §2) ──────────────────────
   The user wants visible cards to rely on IMDb rating, never TMDb. Shows the real
   IMDb rating when available, else "IMDb —". TMDb is only ever shown inside Movie
   Details (its own fallback section), never on a card. */
function MovieImdbBadge({ m, dash }) {
  const imdb = movieImdbRating(m);
  if (imdb != null) return <span style={{ color: '#E0A911', fontWeight: 700 }}>IMDb {_ratingStr(imdb)}</span>;
  return dash === false ? null : <span style={{ color: 'var(--muted-2)' }}>IMDb —</span>;
}
/* ── rating line for feed cards (Browse / Best of Year / Similar / Search) ──
   IMDb rating only (+ My rating separately) — no TMDb on the card. */
function MovieRatingLine({ meta, lib, size }) {
  const star = movieStarValue(lib);
  const fs = size || 11.5;
  const watched = movieIsWatched(lib);
  // prefer the library record's IMDb rating, else the meta's (both may be null on discovery).
  const m = (lib && lib.imdbRating != null) ? lib : (meta && meta.imdbRating != null ? meta : (lib || meta));
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', fontSize: fs, color: 'var(--muted)' }}>
      {meta && meta.year ? <span>{meta.year}</span> : null}
      <MovieImdbBadge m={m}/>
      {star > 0
        ? <span style={{ color: '#E89B3C', fontWeight: 600 }}>My {star}/5</span>
        : (watched ? <span style={{ color: 'var(--muted-2)' }}>My: not rated</span> : null)}
      {watched && <span style={{ color: '#4FA862', fontWeight: 600 }}>✓ Watched</span>}
      {movieIsLiked(lib) && <span style={{ color: '#C2554E', fontWeight: 600 }}>❤️ Liked</span>}
    </div>
  );
}
// Compact inline rating chips (IMDb only [+ My rating]) for list rows.
function MovieRatingChips({ m, mine }) {
  const star = movieStarValue(m);
  return (
    <>
      <MovieImdbBadge m={m}/>
      {mine !== false && star > 0 && <span style={{ color: '#E89B3C', fontWeight: 600 }}>My {star}/5</span>}
    </>
  );
}

/* ── feed card (Browse / Best of Year / Similar / Search) ─────────────── */
function MovieFeedCard({ meta, source, onOpen, onPickList, flashToast, why }) {
  const store = useStore();
  const idx = movieLibIndex(store.movies());
  const lib = resolveLibMovie(meta, idx);
  return (
    <div style={{ ...nutriStyles.card, padding: 10, marginBottom: 12 }}>
      <div style={{ display: 'flex', gap: 12 }}>
        <div onClick={() => onOpen(meta)} style={{ cursor: 'pointer', width: 92, flexShrink: 0 }}>
          <MoviePoster movie={meta} w={92}/>
        </div>
        <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
          <div onClick={() => onOpen(meta)} style={{ cursor: 'pointer', flex: 1 }}>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
              <span style={{ fontWeight: 700, fontSize: 14.5, lineHeight: 1.25 }}>{meta.title}</span>
            </div>
            <div style={{ margin: '3px 0 4px' }}><MovieRatingLine meta={meta} lib={lib}/></div>
            {(why || (meta._why)) && (
              <div style={{ fontSize: 10.5, color: 'var(--accent)', fontWeight: 600, marginBottom: 4 }}>{why || ('Because: ' + meta._why)}</div>
            )}
            {meta.genres && meta.genres.length > 0 && (
              <div style={{ fontSize: 11, color: 'var(--muted-2)', marginBottom: 4 }}>{meta.genres.slice(0,3).join(' · ')}</div>
            )}
            <div style={{ fontSize: 12, color: 'var(--text-2)', lineHeight: 1.4, display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
              {meta.overview || 'No overview available.'}
            </div>
          </div>
          <div style={{ marginTop: 8 }}>
            <MovieActions meta={meta} source={source} onPickList={onPickList} flashToast={flashToast} onOpenDetail={() => onOpen(meta)}/>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ── filter bar (genres / year / rating / lang / sort + exclude toggle) ── */
function MovieFilterBar({ genreMap, filters, setFilters, sort, setSort }) {
  const [open, setOpen] = React.useState(false);
  const genreEntries = Object.entries(genreMap || {});
  const activeCount = (filters.genreIds.length ? 1 : 0) + (filters.year ? 1 : 0) + (filters.voteGte ? 1 : 0) + (filters.lang ? 1 : 0);
  const years = []; const ty = new Date().getFullYear();
  for (let y = ty; y >= 1950; y--) years.push(y);
  return (
    <div style={{ marginBottom: 10 }}>
      <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
        <button onClick={() => setOpen(o => !o)} style={{
          ...pill(open || activeCount > 0), display: 'inline-flex', alignItems: 'center', gap: 5,
        }}>⚙︎ Filters{activeCount > 0 ? ' · ' + activeCount : ''}</button>
        <div style={{ display: 'flex', gap: 5, overflowX: 'auto', flex: 1 }} className="nutri-scroll-x">
          {MOVIE_SORTS.map(s => (
            <button key={s.id} onClick={() => setSort(s.id)} style={pill(sort === s.id)}>{s.label}</button>
          ))}
        </div>
      </div>
      {open && (
        <div style={{ ...nutriStyles.card, padding: 12, marginTop: 8, display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div>
            <div style={miniLabel}>Genres</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {genreEntries.length === 0 && <span style={{ fontSize: 12, color: 'var(--muted)' }}>Genres load once TMDb is connected.</span>}
              {genreEntries.map(([id, name]) => {
                const on = filters.genreIds.indexOf(+id) >= 0;
                return (
                  <button key={id} onClick={() => setFilters(f => ({ ...f, genreIds: on ? f.genreIds.filter(g => g !== +id) : [...f.genreIds, +id] }))}
                    style={pill(on)}>{name}</button>
                );
              })}
            </div>
          </div>
          <div style={{ display: 'flex', gap: 10 }}>
            <label style={{ flex: 1 }}>
              <div style={miniLabel}>Year</div>
              <select value={filters.year || ''} onChange={e => setFilters(f => ({ ...f, year: e.target.value }))} style={inputStyle}>
                <option value="">Any</option>
                {years.map(y => <option key={y} value={y}>{y}</option>)}
              </select>
            </label>
            <label style={{ flex: 1 }}>
              <div style={miniLabel}>Min rating</div>
              <select value={filters.voteGte || ''} onChange={e => setFilters(f => ({ ...f, voteGte: e.target.value }))} style={inputStyle}>
                <option value="">Any</option>
                {[5,6,7,7.5,8,8.5].map(v => <option key={v} value={v}>★ {v}+</option>)}
              </select>
            </label>
          </div>
          <div style={{ display: 'flex', gap: 10 }}>
            <label style={{ flex: 1 }}>
              <div style={miniLabel}>Language</div>
              <select value={filters.lang || ''} onChange={e => setFilters(f => ({ ...f, lang: e.target.value }))} style={inputStyle}>
                <option value="">Any</option>
                <option value="en">English</option><option value="ar">Arabic</option>
                <option value="fr">French</option><option value="es">Spanish</option>
                <option value="ja">Japanese</option><option value="ko">Korean</option>
                <option value="hi">Hindi</option><option value="de">German</option>
              </select>
            </label>
            <label style={{ flex: 1 }}>
              <div style={miniLabel}>Max runtime</div>
              <select value={filters.runtimeLte || ''} onChange={e => setFilters(f => ({ ...f, runtimeLte: e.target.value }))} style={inputStyle}>
                <option value="">Any</option>
                {[90,100,120,150,180].map(v => <option key={v} value={v}>≤ {v} min</option>)}
              </select>
            </label>
          </div>
          <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: 'var(--text)', cursor: 'pointer' }}>
            <input type="checkbox" checked={filters.excludeSeen} onChange={e => setFilters(f => ({ ...f, excludeSeen: e.target.checked }))}/>
            Hide watched &amp; disliked
          </label>
          <button onClick={() => setFilters(f => ({ genreIds: [], year: '', voteGte: '', lang: '', runtimeLte: '', excludeSeen: f.excludeSeen }))}
            style={{ ...subtleBtn, alignSelf: 'flex-start' }}>Clear filters</button>
        </div>
      )}
    </div>
  );
}

/* ── recommendations engine ───────────────────────────────────────────────
   Taste signals (strongest→weakest): liked / 5★ → 4★ → other watched. CSV-only
   seeds (imdbId, no tmdbId) are resolved via TMDb /find (capped). Excludes watched
   + disliked by robust title+year matching so CSV-watched movies never leak in. */
function _movieSeedScore(m) {
  if (m.disliked) return 0;
  const star = movieStarValue(m);
  if (movieIsLiked(m)) return 3;     // explicit like or 5★
  if (star === 4) return 2;
  if (m.watchedDate) return 1;
  return 0;
}
// ── variety helpers ──
function _shuffle(arr) { const a = (arr || []).slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const t = a[i]; a[i] = a[j]; a[j] = t; } return a; }
function _pickOne(arr) { return (arr && arr.length) ? arr[Math.floor(Math.random() * arr.length)] : null; }
function _rPage(max) { return 1 + Math.floor(Math.random() * (max || 3)); }
// Group library into seed buckets (excludes disliked).
function _seedBuckets(lib) {
  const liked = [], four = [], watched = [];
  (lib || []).forEach(m => {
    if (m.disliked) return;
    if (movieIsLiked(m)) liked.push(m);
    else if (movieStarValue(m) === 4) four.push(m);
    else if (m.watchedDate) watched.push(m);
  });
  return { liked, four, watched };
}
// Resolve tmdbId for imdb-only seeds, capped to avoid hammering the API.
async function _seedsWithTmdb(seeds, cap) {
  const out = []; let used = 0;
  for (const s of seeds) {
    if (s.tmdbId != null) { out.push({ ...s, _seedTitle: s.title }); }
    else if (s.imdbId && used < (cap || 8)) {
      used++;
      try { const f = await TMDB.findByImdb(s.imdbId); if (f && f.tmdbId != null) out.push({ ...s, tmdbId: f.tmdbId, _seedTitle: s.title }); } catch (_) {}
    }
    if (out.length >= 10) break;
  }
  return out;
}

// Sectioned "For You" — randomized each call (rotates seeds + TMDb pages) for variety.
// Network calls run in PARALLEL so the deck/feed appears fast (spec §4/§5). Uses the
// central exclusion keys so watched/disliked never leak in (spec §3).
async function buildMovieForYou(store) {
  const lib = store.movies();
  const keys = getExcludedMovieKeys(lib);
  const placed = new Set();
  const sections = [];
  function addSection(title, movies, why) {
    const uniq = [];
    (movies || []).forEach(r => {
      if (!r || r.tmdbId == null || placed.has(r.tmdbId) || movieKeyExcluded(r, keys)) return;
      placed.add(r.tmdbId); uniq.push(why ? { ...r, _why: why } : r);
    });
    if (uniq.length >= 3) sections.push({ title, movies: uniq.slice(0, 18) });
    else uniq.forEach(r => placed.delete(r.tmdbId));
  }

  const b = _seedBuckets(lib);
  // Random rotation: shuffle each bucket, take a few, resolve tmdbIds.
  const candidates = [..._shuffle(b.liked).slice(0, 4), ..._shuffle(b.four).slice(0, 2), ..._shuffle(b.watched).slice(0, 4)];
  const seeds = await _seedsWithTmdb(candidates, 8);
  const likeSeeds  = seeds.filter(s => movieIsLiked(s));
  const fourSeeds  = seeds.filter(s => !movieIsLiked(s) && movieStarValue(s) === 4);
  const fiveSeeds  = seeds.filter(s => movieStarValue(s) === 5);
  const watchSeeds = seeds.filter(s => s.watchedDate && !movieIsLiked(s) && movieStarValue(s) !== 4);

  const becauseSeeds = _shuffle([...likeSeeds, ...fourSeeds]).slice(0, 2); // rotates which movie appears
  const fiveSeed = _pickOne(fiveSeeds);
  const wSeed = _pickOne(watchSeeds);

  // Fire every TMDb call in parallel, then assemble sections in priority order.
  const becausePromises = becauseSeeds.map(s =>
    TMDB.recommendations(s.tmdbId, _rPage()).catch(() => []).then(async r => {
      if (r.length < 4) r = r.concat(await TMDB.similar(s.tmdbId).catch(() => []));
      return { seed: s, results: r };
    }));
  const [becauseResults, fiveSim, watchRec, topR, trend] = await Promise.all([
    Promise.all(becausePromises),
    fiveSeed ? TMDB.similar(fiveSeed.tmdbId, _rPage()).catch(() => []) : Promise.resolve([]),
    wSeed ? TMDB.recommendations(wSeed.tmdbId, _rPage()).catch(() => []) : Promise.resolve([]),
    TMDB.topRated(_rPage()).catch(() => []),
    TMDB.trending(Math.random() < 0.5 ? 'week' : 'day', _rPage()).catch(() => []),
  ]);

  becauseResults.forEach(({ seed, results }) =>
    addSection('Because you liked ' + (seed._seedTitle || seed.title), _shuffle(results), 'recommended from ' + (seed._seedTitle || seed.title)));
  if (fiveSeed) addSection('Similar to your 5★ movies', _shuffle(fiveSim), 'similar to your 5★ ' + (fiveSeed._seedTitle || fiveSeed.title));
  if (wSeed) addSection('Based on your watched history', _shuffle(watchRec), 'based on ' + (wSeed._seedTitle || wSeed.title));
  addSection("Great movies you haven't watched", topR, 'highly rated on TMDb');
  addSection('Popular right now', trend, 'trending now');
  if (!sections.length) addSection('Popular movies', await TMDB.popular(_rPage()).catch(() => []), 'popular on TMDb');

  return { sections, seeded: (likeSeeds.length + fourSeeds.length + fiveSeeds.length + watchSeeds.length) > 0 };
}

// Flat ranked recommendations (used by the Cards deck) — derived from the same engine.
async function buildMovieRecommendations(store) {
  const r = await buildMovieForYou(store);
  const seen = new Set(); const list = [];
  r.sections.forEach(sec => sec.movies.forEach(m => { if (!seen.has(m.tmdbId)) { seen.add(m.tmdbId); list.push(m); } }));
  list.sort((a, b) => ((b.tmdbRating || 0) - (a.tmdbRating || 0)) || ((b.popularity || 0) - (a.popularity || 0)));
  return { list, seeded: r.seeded };
}

/* ── BROWSE tab — modes: For You · Browse · Best of Year · Similar (+ search) ── */
const MOVIE_BROWSE_MODES = [['foryou','✨ For You'],['browse','🍿 Browse'],['best','🏆 Best of Year'],['similar','🎬 Similar']];
function MoviesBrowse({ onOpenMovie, onOpenSettings, onAddManual, onImportCsv, onPickList, flashToast }) {
  const store = useStore();
  const hasCreds = TMDB.hasCredentials();
  const [mode, setMode] = React.useState('foryou'); // foryou | browse | best | similar | search
  const [query, setQuery] = React.useState('');
  const [submitted, setSubmitted] = React.useState('');
  const [genreMap, setGenreMap] = React.useState(TMDB.genreMapSync());
  const [filters, setFilters] = React.useState({ genreIds: [], year: '', voteGte: '', lang: '', runtimeLte: '', excludeSeen: true });
  const [sort, setSort] = React.useState('popularity');
  const [results, setResults] = React.useState([]);
  const [page, setPage] = React.useState(1);
  const [totalPages, setTotalPages] = React.useState(1);
  const [loading, setLoading] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [forYou, setForYou] = React.useState(() => movieCacheGet('foryou') || null); // { sections, seeded } — cached across tab switches
  const [refreshNonce, setRefreshNonce] = React.useState(0);
  const reqRef = React.useRef(0);

  React.useEffect(() => { if (hasCreds) TMDB.genres().then(setGenreMap).catch(() => {}); }, [hasCreds]);

  async function load(reset) {
    if (!hasCreds || mode === 'best' || mode === 'similar') return; // sub-modes self-load
    const my = ++reqRef.current;
    setLoading(true); setErr(null);
    try {
      if (mode === 'foryou') {
        // Use the cached For You feed when available (instant on tab switch); Refresh clears it.
        const cached = movieCacheGet('foryou');
        if (cached) { setForYou(cached); setResults([]); return; }
        const r = await buildMovieForYou(store);
        if (my !== reqRef.current) return;
        movieCacheSet('foryou', r);
        setForYou(r); setResults([]);
      } else if (mode === 'search' && submitted.trim()) {
        const nextPage = reset ? 1 : page + 1;
        const r = await TMDB.search(submitted.trim(), nextPage);
        if (my !== reqRef.current) return;
        setResults(prev => reset ? r.results : prev.concat(r.results));
        setPage(nextPage); setTotalPages(r.totalPages || 1);
      } else { // browse — general discover feed (no "Because you liked")
        const sortMeta = MOVIE_SORTS.find(s => s.id === sort) || MOVIE_SORTS[0];
        // Refresh rotates the starting page for variety.
        const startPage = reset ? (1 + (refreshNonce % 4)) : page + 1;
        const r = await TMDB.discover({
          genreIds: filters.genreIds, year: filters.year, voteGte: filters.voteGte,
          lang: filters.lang, runtimeLte: filters.runtimeLte, sortBy: sortMeta.tmdb, page: startPage,
        });
        if (my !== reqRef.current) return;
        setResults(prev => reset ? r.results : prev.concat(r.results));
        setPage(startPage); setTotalPages(r.totalPages || 1);
      }
    } catch (e) {
      if (my === reqRef.current) setErr(e.message || 'Could not load movies.');
    } finally {
      if (my === reqRef.current) setLoading(false);
    }
  }

  // Reload on mode / search / filters / sort / refresh / library-size change.
  const libLen = store.movies().length;
  React.useEffect(() => { setResults([]); setPage(1); load(true); /* eslint-disable-next-line */ },
    [mode, submitted, sort, refreshNonce, JSON.stringify(filters.genreIds), filters.year, filters.voteGte, filters.lang, filters.runtimeLte, hasCreds]);

  // Use the central exclusion set (tmdbId/imdbId/title+year) — consistent with Cards/For You.
  const _browseExclKeys = React.useMemo(() => getExcludedMovieKeys(store.movies()), [store.movies().length, store.movies().filter(m=>m.watchedDate||m.disliked).length]);
  const visible = results.filter(m => filters.excludeSeen ? !movieKeyExcluded(m, _browseExclKeys) : true);
  const forYouSections = (forYou && forYou.sections || []).map(sec => ({
    title: sec.title,
    movies: filters.excludeSeen ? sec.movies.filter(m => !movieKeyExcluded(m, _browseExclKeys)) : sec.movies,
  })).filter(sec => sec.movies.length > 0);
  const canPaginate = (mode === 'browse' || mode === 'search') && page < totalPages;
  const flatMode = mode === 'browse' || mode === 'search';

  if (!hasCreds) return <MovieConnectPrompt onOpenSettings={onOpenSettings}/>;

  return (
    <div style={{ padding: 14 }}>
      {/* search row */}
      <form onSubmit={e => { e.preventDefault(); if (query.trim()) { setMode('search'); setSubmitted(query); } }} style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
        <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 8, background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 12, padding: '8px 12px' }}>
          <SearchIcon size={16}/>
          <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search movies on TMDb…"
            style={{ flex: 1, border: 0, background: 'transparent', outline: 'none', font: 'inherit', fontSize: 14, color: 'var(--text)' }}/>
          {query && <button type="button" onClick={() => { setQuery(''); if (mode === 'search') { setSubmitted(''); setMode('foryou'); } }} style={{ background: 'none', border: 0, color: 'var(--muted)', cursor: 'pointer' }}><XIcon size={15}/></button>}
        </div>
        <button type="button" onClick={onAddManual} title="Add manually" style={{ ...iconSquare }}>＋</button>
      </form>

      {/* mode pills */}
      <div style={{ display: 'flex', gap: 6, marginBottom: 10, overflowX: 'auto' }} className="nutri-scroll-x">
        {MOVIE_BROWSE_MODES.map(([id, label]) => (
          <button key={id} onClick={() => { if (mode === 'search') setSubmitted(''); setMode(id); }} style={pill(mode === id)}>{label}</button>
        ))}
        {mode === 'search' && <button style={pill(true)}>🔎 Results</button>}
      </div>

      {/* Best of Year / Similar are self-contained sub-views */}
      {mode === 'best'    && <MoviesBestOfYear onOpenMovie={onOpenMovie} onPickList={onPickList} flashToast={flashToast}/>}
      {mode === 'similar' && <MoviesSimilar onOpenMovie={onOpenMovie} onPickList={onPickList} flashToast={flashToast}/>}

      {flatMode && <MovieFilterBar genreMap={genreMap} filters={filters} setFilters={setFilters} sort={sort} setSort={setSort}/>}

      {mode === 'foryou' && (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 }}>
          <div style={{ fontSize: 11.5, color: 'var(--muted)', flex: 1, minWidth: 0 }}>
            {forYou && forYou.seeded ? 'Tuned to your taste — Refresh for fresh picks.' : 'Popular picks — like or rate movies 5★ to personalise.'}
          </div>
          <button onClick={() => { movieCacheDelete('foryou'); setRefreshNonce(n => n + 1); }} style={subtleBtn}>↻ Refresh</button>
        </div>
      )}
      {mode === 'browse' && (
        <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
          <button onClick={() => setRefreshNonce(n => n + 1)} style={subtleBtn}>↻ Shuffle</button>
        </div>
      )}

      {(mode === 'foryou' || flatMode) && err && (
        <div style={{ ...nutriStyles.card, padding: 14, marginBottom: 12, borderColor: 'rgba(194,90,78,0.4)' }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: '#C2554E', marginBottom: 4 }}>Couldn’t load</div>
          <div style={{ fontSize: 12.5, color: 'var(--muted)' }}>{err}</div>
          <button onClick={() => load(true)} style={{ ...subtleBtn, marginTop: 8 }}>Retry</button>
        </div>
      )}

      {mode === 'foryou' && (
        <>
          {forYouSections.map(sec => (
            <MovieRail key={sec.title} title={sec.title} movies={sec.movies} onOpen={onOpenMovie}/>
          ))}
          {!loading && forYouSections.length === 0 && !err && (
            <EmptyState icon="🍿" title="Building your picks" subtitle="Like a few movies or rate watched ones 5★, then Refresh to personalise For You."/>
          )}
        </>
      )}

      {flatMode && (
        <>
          {visible.map(m => (
            <MovieFeedCard key={m.tmdbId} meta={m} source={mode === 'search' ? 'search' : 'browse'}
              onOpen={onOpenMovie} onPickList={onPickList} flashToast={flashToast}/>
          ))}
          {!loading && visible.length === 0 && !err && (
            <EmptyState icon="🍿" title="Nothing to show" subtitle={mode === 'search' ? 'No matches — try another title.' : 'Adjust filters or enable “include watched”.'}/>
          )}
          {!loading && canPaginate && visible.length > 0 && (
            <button onClick={() => load(false)} style={{ ...subtleBtn, width: '100%', marginTop: 4 }}>Load more</button>
          )}
        </>
      )}

      {loading && (mode === 'foryou' || flatMode) && <div style={{ textAlign: 'center', padding: 20, color: 'var(--muted)', fontSize: 13 }}>Loading…</div>}

      {mode !== 'similar' && (
        <div style={{ marginTop: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <button onClick={onImportCsv} style={{ ...subtleBtn }}>⬆︎ Import CSV</button>
          <button onClick={onOpenSettings} style={{ ...subtleBtn }}>⚙︎ Settings</button>
        </div>
      )}
    </div>
  );
}

/* ── Best of Year (mode within Browse) ────────────────────────────────── */
function MoviesBestOfYear({ onOpenMovie, onPickList, flashToast }) {
  const store = useStore();
  const nowYear = new Date().getFullYear();
  const [year, setYear] = React.useState(nowYear);
  const [bySort, setBySort] = React.useState('rating'); // rating | popular
  const [includeWatched, setIncludeWatched] = React.useState(false);
  const [results, setResults] = React.useState([]);
  const [page, setPage] = React.useState(1);
  const [totalPages, setTotalPages] = React.useState(1);
  const [loading, setLoading] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const reqRef = React.useRef(0);
  const years = []; for (let y = nowYear; y >= 1970; y--) years.push(y);

  async function load(reset) {
    const my = ++reqRef.current;
    const nextPage = reset ? 1 : page + 1;
    const sig = 'best:' + year + ':' + bySort;
    if (reset) { // reuse the cached first page on return (no refetch)
      const cached = movieCacheGet(sig);
      if (cached) { setResults(cached.results); setPage(cached.page); setTotalPages(cached.totalPages); return; }
    }
    setLoading(true); setErr(null);
    try {
      const r = await TMDB.discover({
        year,
        sortBy: bySort === 'rating' ? 'vote_average.desc' : 'popularity.desc',
        minVotes: bySort === 'rating' ? 300 : 100,   // vote-count floor for a quality balance
        page: nextPage,
      });
      if (my !== reqRef.current) return;
      setResults(prev => reset ? r.results : prev.concat(r.results));
      setPage(nextPage); setTotalPages(r.totalPages || 1);
      if (reset) movieCacheSet(sig, { results: r.results, page: nextPage, totalPages: r.totalPages || 1 });
    } catch (e) { if (my === reqRef.current) setErr(e.message || 'Could not load.'); }
    finally { if (my === reqRef.current) setLoading(false); }
  }
  React.useEffect(() => { setResults([]); setPage(1); load(true); /* eslint-disable-next-line */ }, [year, bySort]);

  const keys = getExcludedMovieKeys(store.movies());
  const visible = results.filter(m => includeWatched ? true : !movieKeyExcluded(m, keys));

  return (
    <div>
      {/* year strip */}
      <div style={{ display: 'flex', gap: 6, overflowX: 'auto', marginBottom: 8 }} className="nutri-scroll-x">
        {years.slice(0, 12).map(y => (
          <button key={y} onClick={() => setYear(y)} style={pill(year === y)}>{y}</button>
        ))}
        <select value={year} onChange={e => setYear(+e.target.value)} style={{ ...inputStyle, width: 'auto', flexShrink: 0 }}>
          {years.map(y => <option key={y} value={y}>{y}</option>)}
        </select>
      </div>
      <div style={{ display: 'flex', gap: 6, alignItems: 'center', marginBottom: 10, flexWrap: 'wrap' }}>
        <button onClick={() => setBySort('rating')} style={pill(bySort === 'rating')}>Top rated</button>
        <button onClick={() => setBySort('popular')} style={pill(bySort === 'popular')}>Popular</button>
        <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text)', cursor: 'pointer', marginLeft: 'auto' }}>
          <input type="checkbox" checked={includeWatched} onChange={e => setIncludeWatched(e.target.checked)}/> Include watched
        </label>
      </div>
      <div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 8 }}>Best of <strong style={{ color: 'var(--text)' }}>{year}</strong> you haven’t seen</div>

      {err && <div style={{ ...nutriStyles.card, padding: 14, marginBottom: 12 }}><div style={{ fontSize: 12.5, color: 'var(--muted)' }}>{err}</div><button onClick={() => load(true)} style={{ ...subtleBtn, marginTop: 8 }}>Retry</button></div>}
      {visible.map(m => (
        <MovieFeedCard key={m.tmdbId} meta={m} source="browse" onOpen={onOpenMovie} onPickList={onPickList} flashToast={flashToast}/>
      ))}
      {loading && <div style={{ textAlign: 'center', padding: 20, color: 'var(--muted)', fontSize: 13 }}>Loading…</div>}
      {!loading && visible.length === 0 && !err && (
        <EmptyState icon="🏆" title={'Nothing for ' + year} subtitle="Try another year, or enable “include watched”."/>
      )}
      {!loading && page < totalPages && visible.length > 0 && (
        <button onClick={() => load(false)} style={{ ...subtleBtn, width: '100%', marginTop: 4 }}>Load more</button>
      )}
    </div>
  );
}

/* ── Similar Movies (mode within Browse) — seed 1+ movies, merge recs ──── */
function MoviesSimilar({ onOpenMovie, onPickList, flashToast }) {
  const store = useStore();
  const [seeds, setSeeds] = React.useState([]); // [{tmdbId,title,year,imdbId,posterUrl}]
  const [yearFrom, setYearFrom] = React.useState('');
  const [yearTo, setYearTo] = React.useState('');
  const [genreId, setGenreId] = React.useState('');
  const [minRating, setMinRating] = React.useState('');
  const [lang, setLang] = React.useState('');
  const [includeWatched, setIncludeWatched] = React.useState(false);
  const [results, setResults] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [genreMap, setGenreMap] = React.useState(TMDB.genreMapSync());
  const reqRef = React.useRef(0);

  React.useEffect(() => { TMDB.genres().then(setGenreMap).catch(() => {}); }, []);

  function addSeed(meta) {
    if (seeds.some(s => s.tmdbId === meta.tmdbId && meta.tmdbId != null)) return;
    setSeeds(s => [...s, meta]);
  }
  function removeSeed(i) { setSeeds(s => s.filter((_, n) => n !== i)); }

  async function run() {
    const my = ++reqRef.current;
    if (!seeds.length) { setResults([]); return; }
    setLoading(true); setErr(null);
    try {
      // resolve tmdbIds (CSV seeds may be imdb-only)
      const resolved = await _seedsWithTmdb(seeds, 6);
      const pool = {};
      for (const s of resolved) {
        const [recs, sim] = await Promise.all([
          TMDB.recommendations(s.tmdbId).catch(() => []),
          TMDB.similar(s.tmdbId).catch(() => []),
        ]);
        recs.concat(sim).forEach(r => {
          if (!r || r.tmdbId == null) return;
          if (resolved.some(x => x.tmdbId === r.tmdbId)) return; // not the seeds themselves
          if (!pool[r.tmdbId]) { pool[r.tmdbId] = { ...r, _score: 1, _whyIds: [s._seedTitle || s.title] }; }
          else { pool[r.tmdbId]._score += 1; if (pool[r.tmdbId]._whyIds.indexOf(s._seedTitle || s.title) < 0) pool[r.tmdbId]._whyIds.push(s._seedTitle || s.title); }
        });
      }
      if (my !== reqRef.current) return;
      let list = Object.values(pool);
      list.sort((a, b) => (b._score - a._score) || ((b.tmdbRating || 0) - (a.tmdbRating || 0)) || ((b.popularity || 0) - (a.popularity || 0)));
      setResults(list);
    } catch (e) { if (my === reqRef.current) setErr(e.message || 'Could not load.'); }
    finally { if (my === reqRef.current) setLoading(false); }
  }
  React.useEffect(() => { run(); /* eslint-disable-next-line */ }, [JSON.stringify(seeds.map(s => s.tmdbId || s.imdbId || s.title))]);

  const _simExclKeys = getExcludedMovieKeys(store.movies());
  const visible = results.filter(m => {
    if (!includeWatched && movieKeyExcluded(m, _simExclKeys)) return false;
    if (yearFrom && (m.year == null || m.year < +yearFrom)) return false;
    if (yearTo && (m.year == null || m.year > +yearTo)) return false;
    if (genreId && (m.genreIds || []).indexOf(+genreId) < 0) return false;
    if (minRating && (m.tmdbRating == null || m.tmdbRating < +minRating)) return false;
    if (lang && m.originalLanguage !== lang) return false;
    return true;
  });

  return (
    <div>
      {/* selected seeds */}
      <div style={{ ...nutriStyles.card, padding: 10, marginBottom: 10 }}>
        <div style={miniLabel}>Find movies similar to</div>
        {seeds.length > 0 && (
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 8 }}>
            {seeds.map((s, i) => (
              <span key={(s.tmdbId || s.imdbId || s.title) + ':' + i} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: 'var(--surface-2)', border: '1px solid var(--border-2)', borderRadius: 999, padding: '4px 6px 4px 4px', fontSize: 12, fontWeight: 600 }}>
                <MoviePoster movie={s} w={22} radius={5}/>
                {s.title}{s.year ? ' (' + s.year + ')' : ''}
                <button onClick={() => removeSeed(i)} style={{ background: 'none', border: 0, color: 'var(--muted)', cursor: 'pointer', fontSize: 13, lineHeight: 1 }}>✕</button>
              </span>
            ))}
          </div>
        )}
        <_SimilarSeedSearch onPick={addSeed}/>
      </div>

      {/* filters */}
      {seeds.length > 0 && (
        <div style={{ ...nutriStyles.card, padding: 10, marginBottom: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
          <div style={{ display: 'flex', gap: 8 }}>
            <label style={{ flex: 1 }}><div style={miniLabel}>From year</div>
              <input type="number" inputMode="numeric" placeholder="any" value={yearFrom} onChange={e => setYearFrom(e.target.value)} style={inputStyle}/></label>
            <label style={{ flex: 1 }}><div style={miniLabel}>To year</div>
              <input type="number" inputMode="numeric" placeholder="any" value={yearTo} onChange={e => setYearTo(e.target.value)} style={inputStyle}/></label>
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <label style={{ flex: 1 }}><div style={miniLabel}>Genre</div>
              <select value={genreId} onChange={e => setGenreId(e.target.value)} style={inputStyle}>
                <option value="">Any</option>
                {Object.entries(genreMap).map(([id, name]) => <option key={id} value={id}>{name}</option>)}
              </select></label>
            <label style={{ flex: 1 }}><div style={miniLabel}>Min ★</div>
              <select value={minRating} onChange={e => setMinRating(e.target.value)} style={inputStyle}>
                <option value="">Any</option>{[5,6,7,7.5,8].map(v => <option key={v} value={v}>★ {v}+</option>)}
              </select></label>
          </div>
          <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: 'var(--text)', cursor: 'pointer' }}>
            <input type="checkbox" checked={includeWatched} onChange={e => setIncludeWatched(e.target.checked)}/> Include watched
          </label>
        </div>
      )}

      {seeds.length === 0 && <EmptyState icon="🎬" title="Pick a movie (or a few)" subtitle="Search above to add seed movies — we’ll merge similar picks from TMDb."/>}
      {err && <div style={{ ...nutriStyles.card, padding: 14, marginBottom: 12 }}><div style={{ fontSize: 12.5, color: 'var(--muted)' }}>{err}</div></div>}
      {loading && <div style={{ textAlign: 'center', padding: 20, color: 'var(--muted)', fontSize: 13 }}>Finding similar movies…</div>}
      {!loading && seeds.length > 0 && (
        <>
          <div style={{ fontSize: 12, color: 'var(--muted)', margin: '2px 0 8px' }}>{visible.length} matches</div>
          {visible.map(m => (
            <MovieFeedCard key={m.tmdbId} meta={m} source="browse" onOpen={onOpenMovie} onPickList={onPickList} flashToast={flashToast}
              why={m._whyIds && m._whyIds.length ? ('Because: ' + m._whyIds.slice(0, 3).join(', ')) : null}/>
          ))}
          {visible.length === 0 && <EmptyState icon="🍿" title="No matches" subtitle="Loosen the filters or add another seed movie."/>}
        </>
      )}
    </div>
  );
}
// Compact seed search for the Similar tool (library + TMDb).
function _SimilarSeedSearch({ onPick }) {
  const store = useStore();
  const [q, setQ] = React.useState('');
  const [tmdb, setTmdb] = React.useState([]);
  const [searching, setSearching] = React.useState(false);
  const timer = React.useRef(0);
  const ql = q.trim().toLowerCase();
  const movies = store.movies();
  const lib = ql ? movies.filter(m => (m.title || '').toLowerCase().includes(ql)).slice(0, 6) : [];
  React.useEffect(() => {
    clearTimeout(timer.current);
    if (!ql || !TMDB.hasCredentials()) { setTmdb([]); return; }
    timer.current = setTimeout(() => {
      setSearching(true);
      TMDB.search(q.trim()).then(r => {
        const libT = new Set(movies.filter(m => m.tmdbId != null).map(m => m.tmdbId));
        setTmdb((r.results || []).filter(x => !libT.has(x.tmdbId)).slice(0, 6));
      }).catch(() => setTmdb([])).finally(() => setSearching(false));
    }, 350);
    return () => clearTimeout(timer.current);
  }, [q]);
  function pick(meta) { onPick(meta); setQ(''); setTmdb([]); }
  return (
    <div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 10, padding: '7px 10px' }}>
        <SearchIcon size={15}/>
        <input value={q} onChange={e => setQ(e.target.value)} placeholder="Add a movie…" style={{ flex: 1, border: 0, background: 'transparent', outline: 'none', font: 'inherit', fontSize: 13.5, color: 'var(--text)' }}/>
        {q && <button onClick={() => setQ('')} style={{ background: 'none', border: 0, color: 'var(--muted)', cursor: 'pointer' }}><XIcon size={14}/></button>}
      </div>
      {(lib.length > 0 || tmdb.length > 0) && (
        <div style={{ marginTop: 6 }}>
          {lib.map(m => (
            <button key={m.id} onClick={() => pick(m)} style={pickRow}><MoviePoster movie={m} w={28}/><span style={{ flex: 1, fontSize: 13, fontWeight: 600 }}>{m.title}<span style={{ color: 'var(--muted)', fontWeight: 400 }}>{m.year ? ' · ' + m.year : ''}</span>{m.watchedDate ? ' ✓' : ''}</span></button>
          ))}
          {tmdb.map(m => (
            <button key={'t' + m.tmdbId} onClick={() => pick(m)} style={pickRow}><MoviePoster movie={m} w={28}/><span style={{ flex: 1, fontSize: 13, fontWeight: 600 }}>{m.title}<span style={{ color: 'var(--muted)', fontWeight: 400 }}>{m.year ? ' · ' + m.year : ''}</span></span></button>
          ))}
        </div>
      )}
      {searching && <div style={{ fontSize: 12, color: 'var(--muted)', padding: '4px 2px' }}>Searching…</div>}
    </div>
  );
}

/* ── CARDS tab (swipe deck + visible buttons) ─────────────────────────── */
function MoviesCards({ onOpenMovie, onOpenSettings, onPickList, flashToast }) {
  const store = useStore();
  const hasCreds = TMDB.hasCredentials();
  // Cards hide watched + disliked + liked + watchlisted + anything acted on this session.
  function freshKeys() { return getExcludedMovieKeys(store.movies(), { excludeLiked: true, excludeWatchlist: true }); }
  function filterDeck(list) { const keys = freshKeys(); return (list || []).filter(m => m && m.tmdbId != null && !movieKeyExcluded(m, keys) && !_isCardActed(m)); }

  // The deck is a QUEUE persisted in the module-level session, so leaving + returning to
  // Cards resumes from the same card (spec §6); the front of the queue is the current card.
  const [deck, setDeck] = React.useState(() => _cardSession.deck ? filterDeck(_cardSession.deck) : []);
  const [loading, setLoading] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [drag, setDrag] = React.useState({ dx: 0, dy: 0, active: false });
  const startRef = React.useRef({ x: 0, y: 0, t: 0 });
  const lastTapRef = React.useRef(0);
  const loadingRef = React.useRef(false);

  function persist(nd) { _cardSession.deck = nd; }

  async function loadDeck() {
    if (!hasCreds || loadingRef.current) return; // guard against overlapping loads (no endless spinner)
    loadingRef.current = true;
    setLoading(true); setErr(null);
    try {
      const r = await buildMovieRecommendations(store);
      const cards = filterDeck(r.list).slice(0, 40);
      setDeck(cards); persist(cards);
    } catch (e) { setErr(e.message || 'Could not load.'); }
    finally { loadingRef.current = false; setLoading(false); }
  }
  // Only fetch when the session queue is empty — tab switches resume the saved queue.
  React.useEffect(() => { if (hasCreds && !deck.length) loadDeck(); /* eslint-disable-next-line */ }, [hasCreds]);

  const current = deck[0];
  const next = deck[1];

  // Remove the current card from the queue (and persist) → the next card moves to the front.
  function dropFront() { setDrag({ dx: 0, dy: 0, active: false }); setDeck(d => { const nd = d.slice(1); persist(nd); return nd; }); }
  function act(kind) {
    if (!current) return;
    if (kind === 'list') {
      // Swipe up / List button → open the Save-to (Watchlist + lists) picker. Advance only on a
      // successful add; on cancel keep the same card (and drop a just-created empty record).
      const existing = store.findMovie(current);
      const id = store.ensureMovie(current, 'cards');
      const wasNew = !existing;
      const card = current;
      setDrag({ dx: 0, dy: 0, active: false }); // snap card back to center while the picker is open
      onPickList && onPickList(id, { onResolve: (added) => {
        if (added) { _markCardActed(card); dropFront(); return; }
        if (wasNew) {
          const m = store.movies().find(x => x.id === id);
          if (m && !m.watchedDate && !m.liked && !m.disliked && !m.watchlist && (!m.lists || !m.lists.length) && m.userRating == null) store.deleteMovie(id);
        }
      }});
      return;
    }
    const id = store.ensureMovie(current, 'cards');
    if (kind === 'like') { store.setMovieStatus(id, 'liked'); flashToast && flashToast('Liked ❤️'); }
    else if (kind === 'dislike') { store.setMovieStatus(id, 'disliked'); flashToast && flashToast('Nope'); }
    _markCardActed(current);
    dropFront();
  }
  function onDown(e) {
    if (!current) return;
    const p = e.touches ? e.touches[0] : e;
    startRef.current = { x: p.clientX, y: p.clientY, t: Date.now() };
    setDrag({ dx: 0, dy: 0, active: true });
  }
  function onMove(e) {
    if (!drag.active) return;
    const p = e.touches ? e.touches[0] : e;
    setDrag({ dx: p.clientX - startRef.current.x, dy: p.clientY - startRef.current.y, active: true });
  }
  function onUp() {
    if (!drag.active) return;
    const { dx, dy } = drag;
    const dist = Math.abs(dx) + Math.abs(dy);
    const dt = Date.now() - startRef.current.t;
    if (dist < 8 && dt < 300) { // tap / double-tap
      const now = Date.now();
      if (now - lastTapRef.current < 280) { act('like'); lastTapRef.current = 0; }
      else { lastTapRef.current = now; setDrag({ dx: 0, dy: 0, active: false }); setTimeout(() => { if (lastTapRef.current && Date.now() - lastTapRef.current >= 270) { lastTapRef.current = 0; onOpenMovie(current); } }, 290); }
      return;
    }
    if (dx > 110) return act('like');
    if (dx < -110) return act('dislike');
    if (dy < -110) { act('list'); setDrag({ dx: 0, dy: 0, active: false }); return; }
    setDrag({ dx: 0, dy: 0, active: false }); // snap back
  }

  if (!hasCreds) return <MovieConnectPrompt onOpenSettings={onOpenSettings}/>;

  return (
    <div style={{ padding: 14, height: '100%', display: 'flex', flexDirection: 'column' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
        <div style={{ fontWeight: 700, fontSize: 15 }}>Swipe to discover</div>
        <button onClick={loadDeck} style={subtleBtn}>↻ Refresh</button>
      </div>

      {err && <div style={{ ...nutriStyles.card, padding: 14, marginBottom: 10 }}><div style={{ fontSize: 12.5, color: 'var(--muted)' }}>{err}</div><button onClick={loadDeck} style={{ ...subtleBtn, marginTop: 8 }}>Retry</button></div>}
      {loading && <div style={{ textAlign: 'center', padding: 40, color: 'var(--muted)' }}>Shuffling…</div>}

      {!loading && !current && !err && (
        <div style={{ marginTop: 20 }}>
          <EmptyState icon="🎉" title="That’s the stack!" subtitle="You’ve gone through the deck. Refresh for more or check your lists."/>
          <button onClick={loadDeck} style={{ ...primaryBtn, width: '100%', marginTop: 12 }}>Load more</button>
        </div>
      )}

      {current && (
        <div style={{ position: 'relative', flex: 1, minHeight: 380, marginBottom: 12 }}>
          {next && (
            <div style={{ position: 'absolute', inset: 0, transform: 'scale(0.95) translateY(10px)', opacity: 0.6 }}>
              <MovieSwipeCard meta={next}/>
            </div>
          )}
          <div
            onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={() => drag.active && onUp()}
            onTouchStart={onDown} onTouchMove={onMove} onTouchEnd={onUp}
            style={{
              position: 'absolute', inset: 0, touchAction: 'none', cursor: 'grab',
              transform: `translate(${drag.dx}px, ${drag.dy}px) rotate(${drag.dx * 0.04}deg)`,
              transition: drag.active ? 'none' : 'transform .25s cubic-bezier(.2,.7,.2,1)',
            }}>
            <MovieSwipeCard meta={current} dx={drag.dx} dy={drag.dy} onInfo={() => onOpenMovie(current)}/>
          </div>
        </div>
      )}

      {current && (
        <div style={{ display: 'flex', gap: 10, justifyContent: 'center', alignItems: 'center', paddingBottom: 4 }}>
          <button onClick={() => act('dislike')} style={circleBtn('var(--muted)')} aria-label="Dislike"><ThumbDownIcon size={22}/></button>
          <button onClick={() => act('list')} style={circleBtn('#5784D8', 46)} aria-label="Add to list"><ListPlusIcon size={20}/></button>
          <button onClick={() => onOpenMovie(current)} style={circleBtn('var(--text)', 46)} aria-label="Details"><EyeIcon size={20}/></button>
          <button onClick={() => act('like')} style={circleBtn('#C2554E')} aria-label="Like"><HeartIcon size={22} fill="#fff"/></button>
        </div>
      )}
      <div style={{ textAlign: 'center', fontSize: 11, color: 'var(--muted-2)', marginTop: 8 }}>
        Swipe → like · ← nope · ↑ list · tap = details · double-tap = like
      </div>
    </div>
  );
}
function MovieSwipeCard({ meta, dx, dy, onInfo }) {
  const likeOp = dx > 20 ? Math.min(1, dx / 110) : 0;
  const nopeOp = dx < -20 ? Math.min(1, -dx / 110) : 0;
  const listOp = dy < -20 ? Math.min(1, -dy / 110) : 0;
  return (
    <div style={{ position: 'relative', width: '100%', height: '100%', borderRadius: 20, overflow: 'hidden', background: 'var(--surface-3)', border: '1px solid var(--border-2)', boxShadow: 'var(--shadow-lg)' }}>
      {moviePosterUrl(meta)
        ? <img src={moviePosterUrl(meta)} alt={meta.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} draggable={false}/>
        : <div style={{ width: '100%', height: '100%', display: 'grid', placeItems: 'center', color: 'var(--muted-2)' }}><FilmIcon size={64}/></div>}
      {/* swipe hint badges */}
      <Badge text="LIKE" color="#4FA862" op={likeOp} pos={{ top: 18, left: 16, rotate: -14 }}/>
      <Badge text="NOPE" color="#C2554E" op={nopeOp} pos={{ top: 18, right: 16, rotate: 14 }}/>
      <Badge text="＋ LIST" color="#5784D8" op={listOp} pos={{ top: 18, left: '50%', translateX: '-50%' }}/>
      <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '40px 16px 16px', background: 'linear-gradient(to top, rgba(0,0,0,0.85), transparent)', color: '#fff' }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
          <span style={{ fontWeight: 700, fontSize: 19, lineHeight: 1.2 }}>{meta.title}</span>
          {meta.year && <span style={{ fontSize: 13, opacity: 0.85 }}>{meta.year}</span>}
        </div>
        <div style={{ display: 'flex', gap: 10, alignItems: 'center', margin: '4px 0 6px', fontSize: 12.5 }}>
          {movieImdbRating(meta) != null
            ? <span style={{ color: '#FFD27A', fontWeight: 700 }}>IMDb {_ratingStr(movieImdbRating(meta))}</span>
            : <span style={{ color: 'rgba(255,255,255,0.6)' }}>IMDb —</span>}
          {meta.genres && <span style={{ opacity: 0.85 }}>{meta.genres.slice(0, 3).join(' · ')}</span>}
        </div>
        <div style={{ fontSize: 12.5, lineHeight: 1.4, opacity: 0.92, display: '-webkit-box', WebkitLineClamp: 3, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{meta.overview}</div>
      </div>
    </div>
  );
}
function Badge({ text, color, op, pos }) {
  const p = pos || {};
  return (
    <div style={{
      position: 'absolute', top: p.top, left: p.left, right: p.right,
      transform: `${p.translateX ? 'translateX(' + p.translateX + ') ' : ''}rotate(${p.rotate || 0}deg)`,
      opacity: op, transition: 'opacity .08s', padding: '4px 12px', borderRadius: 8,
      border: '3px solid ' + color, color, fontWeight: 800, fontSize: 18, letterSpacing: '0.05em',
      background: 'rgba(0,0,0,0.25)', pointerEvents: 'none',
    }}>{text}</div>
  );
}

/* ── LIKED tab — explicitly liked OR rated 5★ ─────────────────────────── */
function MoviesLiked({ onOpenMovie, onPickList, flashToast }) {
  const store = useStore();
  const sel = useMovieSelection();
  const liked = store.movies().filter(movieIsLiked)
    .sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
  return (
    <div style={{ padding: 14, paddingBottom: sel.selecting ? 80 : 14 }}>
      {!sel.selecting && <DailyRatingQueue onOpenMovie={onOpenMovie} flashToast={flashToast} title="⭐ Rate 5 watched movies"/>}
      {liked.length === 0 ? (
        <EmptyState icon="❤️" title="No liked movies yet" subtitle="Like movies from Browse or Cards, or rate a watched movie 5★, and they’ll collect here."/>
      ) : (
        <>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
            <div style={{ fontSize: 12.5, color: 'var(--muted)' }}>{liked.length} liked <span style={{ color: 'var(--muted-2)' }}>· includes your 5★ movies</span></div>
            {!sel.selecting && <button onClick={sel.start} style={subtleBtn}>Select</button>}
          </div>
          {liked.map(m => <MovieLikedRow key={m.id} m={m} store={store} onOpen={onOpenMovie} onPickList={onPickList} flashToast={flashToast} sel={sel}/>)}
        </>
      )}
      {sel.selecting && <MovieBulkBar sel={sel} context={{ type: 'liked' }} flashToast={flashToast}/>}
    </div>
  );
}
function MovieLikedRow({ m, store, onOpen, onPickList, flashToast, sel }) {
  const watched = movieIsWatched(m);
  const star = movieStarValue(m);
  const selecting = sel && sel.selecting;
  const picked = selecting && sel.has(m.id);
  function toggleWatched() {
    if (watched) { store.unmarkWatched(m.id); flashToast && flashToast('Removed from watched'); }
    else { store.markWatched(m.id); flashToast && flashToast('Marked watched ✓'); }
  }
  function removeLike() {
    store.updateMovie(m.id, { liked: false });
    flashToast && flashToast(star === 5 ? 'Kept — rated 5★' : 'Removed from liked');
  }
  return (
    <div onClick={selecting ? () => sel.toggle(m.id) : null} style={{ ...nutriStyles.card, padding: 10, marginBottom: 10, display: 'flex', gap: 12,
      cursor: selecting ? 'pointer' : 'default', ...(picked ? { boxShadow: '0 0 0 2px var(--accent) inset' } : {}) }}>
      {selecting && <SelectDot on={picked}/>}
      <div onClick={selecting ? null : () => onOpen(m)} style={{ cursor: 'pointer', width: 56, flexShrink: 0 }}><MoviePoster movie={m} w={56}/></div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div onClick={selecting ? null : () => onOpen(m)} style={{ cursor: 'pointer', fontWeight: 700, fontSize: 14 }}>{m.title}{m.year ? <span style={{ color: 'var(--muted)', fontWeight: 400 }}> · {m.year}</span> : null}</div>
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center', margin: '3px 0 5px', fontSize: 11.5, color: 'var(--muted)' }}>
          <MovieRatingChips m={m} mine={false}/>
          {watched && <span style={{ color: '#4FA862', fontWeight: 600 }}>✓ Watched</span>}
          {star === 5 && !m.liked && <span style={{ color: '#C2554E' }}>liked via 5★</span>}
        </div>
        {!selecting && <>
          <MovieStars value={m.userRating} onChange={r => store.setMovieRating(m.id, r)} size={18}/>
          <div style={{ display: 'flex', gap: 6, marginTop: 7, flexWrap: 'wrap' }}>
            <button onClick={toggleWatched} style={{ ...chipBtn, ...(watched ? { color: '#4FA862', borderColor: 'rgba(79,168,98,0.4)' } : {}) }}>{watched ? '✓ Watched' : 'Mark watched'}</button>
            <button onClick={() => onPickList(m.id)} style={chipBtn}>＋ List</button>
            <button onClick={removeLike} style={chipBtn}>Remove like</button>
          </div>
        </>}
      </div>
    </div>
  );
}

/* ── Daily rating queue (watched movies missing a personal rating) ─────── */
// Daily "skip" tracking (device-local, resets each day via a date-keyed storage key).
function _movieSkipKey() { const d = new Date(); return 'nutri_movie_rateskip_' + d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); }
function getMovieSkips() { try { const v = JSON.parse(localStorage.getItem(_movieSkipKey()) || '[]'); return Array.isArray(v) ? v : []; } catch (_) { return []; } }
function addMovieSkip(id) { try { const s = getMovieSkips(); if (s.indexOf(id) < 0) { s.push(id); localStorage.setItem(_movieSkipKey(), JSON.stringify(s)); } } catch (_) {} }
function clearMovieSkips() { try { localStorage.removeItem(_movieSkipKey()); } catch (_) {} }

function moviesNeedingRating(movies, opts) {
  const skip = (opts && opts.skip) || [];
  return (movies || []).filter(m => m.watchedDate && movieStarValue(m) === 0 && skip.indexOf(m.id) < 0);
}
function dailyRatingQueue(movies, n, opts) {
  const o = opts || {};
  let pool = moviesNeedingRating(movies, o);
  if (o.shuffle) pool = _shuffle(pool);
  else pool.sort((a, b) => (moviePrimaryDate(a) || '').localeCompare(moviePrimaryDate(b) || '') || (a.id || '').localeCompare(b.id || ''));
  return pool.slice(0, n || 5);
}
// Reusable rating queue — used in Watched and Liked. 5 unrated watched movies, Refresh + Skip.
function DailyRatingQueue({ onOpenMovie, flashToast, title }) {
  const store = useStore();
  const movies = store.movies();
  const [skips, setSkips] = React.useState(() => getMovieSkips());
  const [shuffleOn, setShuffleOn] = React.useState(false);
  const totalUnrated = movies.filter(m => m.watchedDate && movieStarValue(m) === 0).length;
  if (totalUnrated === 0) {
    return (
      <div style={{ ...nutriStyles.card, padding: 16, marginBottom: 14, textAlign: 'center' }}>
        <div style={{ fontSize: 22 }}>🎉</div>
        <div style={{ fontSize: 13, fontWeight: 600, marginTop: 4 }}>Every watched movie is rated</div>
        <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2 }}>Mark more movies watched to rate them here.</div>
      </div>
    );
  }
  const queue = dailyRatingQueue(movies, 5, { skip: skips, shuffle: shuffleOn });
  function rate(m, r) { store.setMovieRating(m.id, r); if (r) flashToast && flashToast(r === 5 ? 'Rated 5★ — added to Liked' : 'Rated ' + r + '★'); }
  function skip(m) { addMovieSkip(m.id); setSkips(getMovieSkips()); }
  function refresh() { clearMovieSkips(); setSkips([]); setShuffleOn(true); }
  return (
    <div style={{ ...nutriStyles.card, padding: 12, marginBottom: 14 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, gap: 8 }}>
        <div style={{ fontWeight: 700, fontSize: 14 }}>{title || '⭐ Rate watched movies'} <span style={{ fontWeight: 400, fontSize: 11, color: 'var(--muted)' }}>· {totalUnrated} to rate</span></div>
        <button onClick={refresh} style={subtleBtn}>↻ Refresh</button>
      </div>
      {queue.length === 0 ? (
        <div style={{ fontSize: 12.5, color: 'var(--muted)', padding: '6px 2px' }}>You’ve gone through this batch — tap Refresh for more.</div>
      ) : (
        <div style={{ display: 'flex', gap: 12, overflowX: 'auto', paddingBottom: 4 }} className="nutri-scroll-x">
          {queue.map(m => (
            <div key={m.id} style={{ width: 130, flexShrink: 0 }}>
              <div onClick={() => onOpenMovie(m)} style={{ cursor: 'pointer' }}><MoviePoster movie={m} w={130}/></div>
              <div style={{ fontSize: 11.5, fontWeight: 600, marginTop: 5, lineHeight: 1.2, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{m.title}</div>
              <div style={{ fontSize: 10.5, color: 'var(--muted)', margin: '1px 0 5px' }}>
                {m.year || ''}{movieImdbRating(m) != null ? ' · IMDb ' + _ratingStr(movieImdbRating(m)) : ' · IMDb —'}
              </div>
              <MovieStars value={m.userRating} size={19} onChange={r => rate(m, r)}/>
              <button onClick={() => skip(m)} style={{ ...chipBtn, marginTop: 6, width: '100%' }}>Skip</button>
            </div>
          ))}
        </div>
      )}
      <div style={{ fontSize: 10.5, color: 'var(--muted-2)', marginTop: 6 }}>Rate or skip — the next watched movie takes its place.</div>
    </div>
  );
}

/* ── WATCHED tab ──────────────────────────────────────────────────────── */
function MoviesWatched({ onOpenMovie, flashToast }) {
  const store = useStore();
  const sel = useMovieSelection();
  const watched = store.movies().filter(movieIsWatched)
    .sort((a, b) => (b.watchedDate || '').localeCompare(a.watchedDate || ''));
  return (
    <div style={{ padding: 14, paddingBottom: sel.selecting ? 80 : 14 }}>
      {!sel.selecting && <DailyRatingQueue onOpenMovie={onOpenMovie} flashToast={flashToast}/>}
      {watched.length === 0 ? (
        <EmptyState icon="👁️" title="Nothing watched yet" subtitle="Mark movies watched and they’ll appear here with the date you saw them."/>
      ) : (
        <>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
            <div style={{ fontSize: 12.5, color: 'var(--muted)' }}>{watched.length} watched</div>
            {!sel.selecting && <button onClick={sel.start} style={subtleBtn}>Select</button>}
          </div>
          {watched.map(m => {
            const links = store.movieDiaryLinks(m);
            const star = movieStarValue(m);
            const selecting = sel.selecting; const picked = selecting && sel.has(m.id);
            return (
              <div key={m.id} onClick={() => selecting ? sel.toggle(m.id) : onOpenMovie(m)} style={{ ...nutriStyles.card, padding: 10, marginBottom: 10, display: 'flex', gap: 12, cursor: 'pointer', ...(picked ? { boxShadow: '0 0 0 2px var(--accent) inset' } : {}) }}>
                {selecting && <SelectDot on={picked}/>}
                <MoviePoster movie={m} w={56}/>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontWeight: 700, fontSize: 14 }}>{m.title} {m.year ? <span style={{ color: 'var(--muted)', fontWeight: 400 }}>· {m.year}</span> : null}</div>
                  <div style={{ fontSize: 12, color: '#4FA862', fontWeight: 600, marginTop: 2 }}>
                    ✓ {_mdy(m.watchedDate)}{m.systemDateAdded ? ' (date approx.)' : ''}
                  </div>
                  <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 3, fontSize: 11, color: 'var(--muted)' }}>
                    <MovieRatingChips m={m}/>
                    {star === 0 && <span style={{ color: 'var(--muted-2)' }}>not rated</span>}
                    {movieIsLiked(m) && <span style={{ color: '#C2554E' }}>❤️ liked</span>}
                    {links.length > 0 && <span style={{ color: '#A99CE0' }}>📓 {links.length} memor{links.length === 1 ? 'y' : 'ies'}</span>}
                  </div>
                </div>
              </div>
            );
          })}
        </>
      )}
      {sel.selecting && <MovieBulkBar sel={sel} context={{ type: 'watched' }} flashToast={flashToast}/>}
    </div>
  );
}

/* ════════════════════════════════════════════════════════════════════════
   BULK SELECTION (spec §5) — select movies in any list and move/copy to another
   list, Watchlist or Watched. Shared by Watchlist · Lists · Liked · Watched.
   ════════════════════════════════════════════════════════════════════════ */
function useMovieSelection() {
  const [selecting, setSelecting] = React.useState(false);
  const [sel, setSel] = React.useState(() => new Set());
  return {
    selecting, count: sel.size, ids: Array.from(sel),
    has: id => sel.has(id),
    toggle: id => setSel(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; }),
    selectAll: ids => setSel(new Set(ids)),
    start: () => { setSel(new Set()); setSelecting(true); },
    clear: () => { setSel(new Set()); setSelecting(false); },
  };
}
function SelectDot({ on }) {
  return (
    <div style={{ width: 22, height: 22, borderRadius: '50%', flexShrink: 0, alignSelf: 'center',
      border: '2px solid ' + (on ? 'var(--accent)' : 'var(--border-2)'), background: on ? 'var(--accent)' : 'transparent',
      display: 'grid', placeItems: 'center', color: 'var(--on-accent)' }}>{on ? <CheckIcon size={13}/> : null}</div>
  );
}
const _bulkBtn = { padding: '7px 11px', borderRadius: 10, background: 'var(--surface-2)', color: 'var(--text)', border: '1px solid var(--border-2)', fontSize: 12, fontWeight: 600, cursor: 'pointer', font: 'inherit', whiteSpace: 'nowrap' };
function _targetRow(color) {
  return { width: '100%', textAlign: 'left', padding: '12px 14px', marginBottom: 8, borderRadius: 12, cursor: 'pointer', font: 'inherit',
    background: 'var(--surface)', border: '1px solid var(--border)', color: color || 'var(--text)', fontWeight: 600, fontSize: 14, display: 'flex', alignItems: 'center', gap: 8 };
}
// Bottom action bar shown while selecting; opens a target picker for Add/Move.
function MovieBulkBar({ sel, context, flashToast }) {
  const store = useStore();
  const [picker, setPicker] = React.useState(null); // 'add' | 'move'
  const ids = sel.ids; const n = ids.length;
  function finish(msg) { flashToast && flashToast(msg); sel.clear(); }
  function addWatchlist() { const r = store.addMoviesToWatchlist(ids); finish((r.added || 0) + ' added to Watchlist' + (r.added !== n ? (' · ' + (n - r.added) + ' skipped') : '')); }
  function markWatched() { store.markMoviesWatched(ids); finish(n + ' marked watched'); }
  function removeHere() {
    if (context.type === 'watchlist') { store.removeMoviesFromWatchlist(ids); finish(n + ' removed from Watchlist'); }
    else if (context.type === 'list') { store.removeMoviesFromList(ids, context.listId); finish(n + ' removed from list'); }
  }
  function onTarget(target) {
    setPicker(null);
    if (target.watchlist) { const r = store.addMoviesToWatchlist(ids); finish((r.added || 0) + ' added to Watchlist'); return; }
    if (picker === 'move' && context.type === 'list') { store.moveMoviesToList(ids, context.listId, target.listId); finish(n + ' moved to ' + target.name); }
    else { const r = store.addMoviesToList(ids, target.listId); finish((r.added != null ? r.added : n) + ' added to ' + target.name); }
  }
  return (
    <>
      <div style={{ position: 'fixed', left: 0, right: 0, bottom: 0, zIndex: 55, background: 'var(--surface)', borderTop: '1px solid var(--border-2)',
        boxShadow: 'var(--shadow-lg)', padding: '10px 12px calc(10px + var(--safe-bottom))', display: 'flex', alignItems: 'center', gap: 10 }}>
        <span style={{ fontWeight: 700, fontSize: 13, flexShrink: 0 }}>{n} selected</span>
        <div style={{ display: 'flex', gap: 6, overflowX: 'auto', flex: 1, justifyContent: 'flex-end' }} className="nutri-scroll-x">
          <button onClick={() => n && setPicker('add')} style={{ ..._bulkBtn, opacity: n ? 1 : 0.5 }}>Add to…</button>
          {context.type === 'list' && <button onClick={() => n && setPicker('move')} style={{ ..._bulkBtn, opacity: n ? 1 : 0.5 }}>Move to…</button>}
          <button onClick={() => n && addWatchlist()} style={{ ..._bulkBtn, opacity: n ? 1 : 0.5 }}>🔖 Watchlist</button>
          <button onClick={() => n && markWatched()} style={{ ..._bulkBtn, opacity: n ? 1 : 0.5 }}>✓ Watched</button>
          {(context.type === 'list' || context.type === 'watchlist') && <button onClick={() => n && removeHere()} style={{ ..._bulkBtn, color: '#C2554E', opacity: n ? 1 : 0.5 }}>Remove</button>}
          <button onClick={sel.clear} style={_bulkBtn}>Cancel</button>
        </div>
      </div>
      {picker && <MovieBulkTargetOverlay title={(picker === 'move' ? 'Move ' : 'Add ') + n + ' to'} excludeListId={context.type === 'list' ? context.listId : null} onPick={onTarget} onClose={() => setPicker(null)}/>}
    </>
  );
}
function MovieBulkTargetOverlay({ title, excludeListId, onPick, onClose }) {
  const store = useStore();
  const lists = store.movieLists().filter(l => l.id !== excludeListId && !l.archived);
  const [newName, setNewName] = React.useState('');
  function create() { const name = newName.trim(); if (!name) return; const id = store.addMovieList(name); onPick({ listId: id, name }); }
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 70, display: 'flex', alignItems: 'flex-end' }}>
      <div onClick={e => e.stopPropagation()} style={{ background: 'var(--bg)', width: '100%', borderTopLeftRadius: 18, borderTopRightRadius: 18, padding: 16, maxHeight: '72vh', overflow: 'auto', paddingBottom: 'calc(16px + var(--safe-bottom))', animation: 'slideUp .2s ease' }}>
        <div style={{ fontWeight: 700, fontSize: 15, marginBottom: 12 }}>{title}</div>
        <button onClick={() => onPick({ watchlist: true, name: 'Watchlist' })} style={_targetRow('#5784D8')}><BookmarkIcon size={16} fill="#5784D8"/>Watchlist</button>
        {lists.map(l => <button key={l.id} onClick={() => onPick({ listId: l.id, name: l.name })} style={_targetRow()}><ListPlusIcon size={16}/>{l.name}</button>)}
        <div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
          <input value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && create()} placeholder="New list…" style={{ ...inputStyle, flex: 1 }}/>
          <button onClick={create} disabled={!newName.trim()} style={{ ...primaryBtn, opacity: newName.trim() ? 1 : 0.5 }}>Create</button>
        </div>
      </div>
    </div>
  );
}

/* ── WATCHLIST tab (with sorting) ─────────────────────────────────────── */
const MOVIE_WL_SORTS = [
  ['added_desc',  'Newest added'],
  ['added_asc',   'Oldest added'],
  ['import_order','Import order'],
  ['year_desc',   'Year — newest'],
  ['year_asc',    'Year — oldest'],
  ['imdb_desc',   'IMDb — high to low'],
  ['imdb_asc',    'IMDb — low to high'],
  ['mine_desc',   'My rating — high to low'],
  ['mine_asc',    'My rating — low to high'],
  ['title_asc',   'Title A–Z'],
  ['title_desc',  'Title Z–A'],
];
function _wlAddedKey(m) { return m.addedDate || m.sourceAddedDateFromCSV || m.importedDate || (m.createdAt ? m.createdAt.slice(0, 10) : '') || ''; }
// Rating sort that always pushes movies WITHOUT that rating to the bottom (spec §14).
function _ratingCmp(getter, asc) {
  return (a, b) => {
    const av = getter(a), bv = getter(b), am = (av == null), bm = (bv == null);
    if (am && bm) return (a.title || '').localeCompare(b.title || '');
    if (am) return 1; if (bm) return -1;
    return asc ? (av - bv) : (bv - av);
  };
}
function sortWatchlist(list, sortId) {
  const arr = (list || []).slice();
  const addedDesc = (a, b) => (_wlAddedKey(b) || '').localeCompare(_wlAddedKey(a) || '') || (b.createdAt || '').localeCompare(a.createdAt || '');
  const addedAsc = (a, b) => (_wlAddedKey(a) || '').localeCompare(_wlAddedKey(b) || '') || (a.createdAt || '').localeCompare(b.createdAt || '');
  const imdbOf = m => movieImdbRating(m);
  const mineOf = m => { const s = movieStarValue(m); return s > 0 ? s : null; };
  const cmp = {
    added_desc: addedDesc,
    added_asc: addedAsc,
    import_order: (a, b) => { const ao = a.importOrder, bo = b.importOrder; if (ao != null && bo != null) return ao - bo; if (ao != null) return -1; if (bo != null) return 1; return addedAsc(a, b); },
    year_desc: (a, b) => ((b.year || 0) - (a.year || 0)) || (a.title || '').localeCompare(b.title || ''),
    year_asc: (a, b) => ((a.year || 9999) - (b.year || 9999)) || (a.title || '').localeCompare(b.title || ''),
    imdb_desc: _ratingCmp(imdbOf, false),
    imdb_asc: _ratingCmp(imdbOf, true),
    mine_desc: _ratingCmp(mineOf, false),
    mine_asc: _ratingCmp(mineOf, true),
    title_asc: (a, b) => (a.title || '').localeCompare(b.title || ''),
    title_desc: (a, b) => (b.title || '').localeCompare(a.title || ''),
  };
  return arr.sort(cmp[sortId] || addedDesc);
}
function MoviesWatchlist({ onOpenMovie, onPickList, flashToast }) {
  const store = useStore();
  const sort = store.movieWatchlistSort();
  const sel = useMovieSelection();
  // Watchlist = saved-to-watch and not yet watched (watching it moves it to Watched).
  const list = store.movies().filter(m => m.watchlist && !m.watchedDate);
  const sorted = sortWatchlist(list, sort);
  return (
    <div style={{ padding: 14, paddingBottom: sel.selecting ? 80 : 14 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
        <div style={{ fontSize: 12.5, color: 'var(--muted)' }}>🔖 {list.length} to watch</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          {list.length > 0 && !sel.selecting && <button onClick={sel.start} style={subtleBtn}>Select</button>}
          <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--muted)' }}>
            Sort
            <select value={sort} onChange={e => store.setMovieWatchlistSort(e.target.value)} style={{ ...inputStyle, width: 'auto', padding: '6px 8px' }}>
              {MOVIE_WL_SORTS.map(([id, label]) => <option key={id} value={id}>{label}</option>)}
            </select>
          </label>
        </div>
      </div>
      {list.length === 0
        ? <EmptyState icon="🔖" title="Watchlist is empty" subtitle="Tap Save on any movie to add it here, or import a CSV as Watchlist."/>
        : sorted.map(m => <MovieWatchlistRow key={m.id} m={m} store={store} onOpen={onOpenMovie} onPickList={onPickList} flashToast={flashToast} sel={sel}/>)}
      {sel.selecting && <MovieBulkBar sel={sel} context={{ type: 'watchlist' }} flashToast={flashToast}/>}
    </div>
  );
}
function MovieWatchlistRow({ m, store, onOpen, onPickList, flashToast, sel }) {
  const selecting = sel && sel.selecting;
  const picked = selecting && sel.has(m.id);
  function markWatched() { store.markWatched(m.id); flashToast && flashToast('Marked watched ✓ — moved to Watched'); }
  function removeWl() { store.toggleMovieWatchlist(m.id); flashToast && flashToast('Removed from watchlist'); }
  const rowClick = selecting ? () => sel.toggle(m.id) : null;
  return (
    <div onClick={rowClick} style={{ ...nutriStyles.card, padding: 10, marginBottom: 10, display: 'flex', gap: 12,
      cursor: selecting ? 'pointer' : 'default', ...(picked ? { boxShadow: '0 0 0 2px var(--accent) inset' } : {}) }}>
      {selecting && <SelectDot on={picked}/>}
      <div onClick={selecting ? null : () => onOpen(m)} style={{ cursor: 'pointer', width: 56, flexShrink: 0 }}><MoviePoster movie={m} w={56}/></div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div onClick={selecting ? null : () => onOpen(m)} style={{ cursor: 'pointer', fontWeight: 700, fontSize: 14 }}>{m.title}{m.year ? <span style={{ color: 'var(--muted)', fontWeight: 400 }}> · {m.year}</span> : null}</div>
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center', margin: '3px 0 7px', fontSize: 11.5, color: 'var(--muted)' }}>
          <MovieRatingChips m={m}/>
          {(m.genres && m.genres.length > 0) && <span style={{ color: 'var(--muted-2)' }}>{m.genres.slice(0, 2).join(' · ')}</span>}
        </div>
        {!selecting && (
          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
            <button onClick={markWatched} style={{ ...chipBtn, color: '#4FA862', borderColor: 'rgba(79,168,98,0.4)' }}>✓ Mark watched</button>
            <button onClick={() => onPickList(m.id)} style={chipBtn}>＋ List</button>
            <button onClick={removeWl} style={chipBtn}>Remove</button>
          </div>
        )}
      </div>
    </div>
  );
}

/* ── LISTS tab ────────────────────────────────────────────────────────── */
function MoviesLists({ onOpenMovie, flashToast }) {
  const store = useStore();
  const lists = store.movieLists();
  const movies = store.movies();
  const [openId, setOpenId] = React.useState(null);
  const [newName, setNewName] = React.useState('');
  const [renaming, setRenaming] = React.useState(null);
  const [renameVal, setRenameVal] = React.useState('');
  const sel = useMovieSelection();

  function moviesIn(listId) { return movies.filter(m => (m.lists || []).indexOf(listId) >= 0); }
  function createList() {
    const n = newName.trim(); if (!n) return;
    store.addMovieList(n); setNewName(''); flashToast && flashToast('List created');
  }
  function delList(l) {
    const count = moviesIn(l.id).length;
    if (!confirm('Delete “' + l.name + '”?' + (count ? '\n\n' + count + ' movie(s) stay in your library — they’re just removed from this list.' : ''))) return;
    store.deleteMovieList(l.id); flashToast && flashToast('List deleted');
  }

  if (openId) {
    const l = lists.find(x => x.id === openId);
    if (!l) { setOpenId(null); return null; }
    const inList = moviesIn(l.id);
    return (
      <div style={{ padding: 14, paddingBottom: sel.selecting ? 80 : 14 }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 12 }}>
          <button onClick={() => { sel.clear(); setOpenId(null); }} style={subtleBtn}>‹ All lists</button>
          {inList.length > 0 && !sel.selecting && <button onClick={sel.start} style={subtleBtn}>Select</button>}
        </div>
        <div style={{ fontWeight: 700, fontSize: 17, marginBottom: 2 }}>{l.name}</div>
        <div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 12 }}>{inList.length} movie{inList.length === 1 ? '' : 's'}{l.recovered ? ' · recovered — rename me' : ''}</div>
        {inList.length === 0
          ? <EmptyState icon="🎞️" title="Empty list" subtitle="Add movies to this list from any movie’s actions."/>
          : <MoviePosterGrid movies={inList} onOpenMovie={onOpenMovie} sel={sel}/>}
        {sel.selecting && <MovieBulkBar sel={sel} context={{ type: 'list', listId: l.id }} flashToast={flashToast}/>}
      </div>
    );
  }

  return (
    <div style={{ padding: 14 }}>
      <div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
        <input value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && createList()}
          placeholder="New list name…" style={{ ...inputStyle, flex: 1 }}/>
        <button onClick={createList} disabled={!newName.trim()} style={{ ...primaryBtn, opacity: newName.trim() ? 1 : 0.5 }}>Create</button>
      </div>
      {lists.length === 0 && <EmptyState icon="🗂️" title="No lists yet" subtitle="Create lists like “Date night” or “Watch with friends”."/>}
      {lists.map(l => {
        const inList = moviesIn(l.id);
        const previews = inList.slice(0, 4);
        return (
          <div key={l.id} style={{ ...nutriStyles.card, padding: 12, marginBottom: 10 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <div onClick={() => setOpenId(l.id)} style={{ display: 'flex', gap: 4, cursor: 'pointer' }}>
                {previews.length ? previews.map(m => <MoviePoster key={m.id} movie={m} w={34} radius={6}/>)
                  : <div style={{ width: 34, height: 51, borderRadius: 6, background: 'var(--surface-3)', display: 'grid', placeItems: 'center', color: 'var(--muted-2)' }}><FilmIcon size={16}/></div>}
              </div>
              <div onClick={() => setOpenId(l.id)} style={{ flex: 1, minWidth: 0, cursor: 'pointer' }}>
                {renaming === l.id ? (
                  <input autoFocus value={renameVal} onChange={e => setRenameVal(e.target.value)} onClick={e => e.stopPropagation()}
                    onKeyDown={e => { if (e.key === 'Enter') { store.renameMovieList(l.id, renameVal); setRenaming(null); } if (e.key === 'Escape') setRenaming(null); }}
                    onBlur={() => { store.renameMovieList(l.id, renameVal); setRenaming(null); }}
                    style={{ ...inputStyle, padding: '4px 8px' }}/>
                ) : (
                  <div style={{ fontWeight: 700, fontSize: 14.5 }}>{l.name}{l.archived ? ' · archived' : ''}</div>
                )}
                <div style={{ fontSize: 11.5, color: 'var(--muted)' }}>{inList.length} movie{inList.length === 1 ? '' : 's'}</div>
              </div>
              <button onClick={() => { setRenaming(l.id); setRenameVal(l.name); }} style={iconGhost} aria-label="Rename"><EditIcon size={15}/></button>
              <button onClick={() => delList(l)} style={iconGhost} aria-label="Delete"><TrashIconM size={15}/></button>
            </div>
          </div>
        );
      })}
    </div>
  );
}

function MoviePosterGrid({ movies, onOpenMovie, sel }) {
  const selecting = sel && sel.selecting;
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10 }}>
      {movies.map(m => {
        const picked = selecting && sel.has(m.id);
        return (
          <div key={m.id} onClick={() => selecting ? sel.toggle(m.id) : onOpenMovie(m)} style={{ cursor: 'pointer', position: 'relative' }}>
            <div style={{ position: 'relative', borderRadius: 12, overflow: 'hidden', boxShadow: picked ? '0 0 0 2px var(--accent)' : 'none' }}>
              <MoviePoster movie={m}/>
              {selecting && <div style={{ position: 'absolute', top: 6, right: 6 }}><SelectDot on={picked}/></div>}
              {selecting && picked && <div style={{ position: 'absolute', inset: 0, background: 'rgba(31,61,46,0.18)' }}/>}
            </div>
            <div style={{ fontSize: 11.5, fontWeight: 600, marginTop: 4, lineHeight: 1.25, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{m.title}</div>
            <div style={{ fontSize: 10.5, color: 'var(--muted)' }}>
              {m.year || ''}{movieImdbRating(m) != null ? ' · IMDb ' + _ratingStr(movieImdbRating(m)) : ' · IMDb —'}{movieStarValue(m) > 0 ? ' · My ' + movieStarValue(m) + '/5' : ''}{m.watchedDate ? ' · ✓' : ''}{movieIsLiked(m) ? ' · ❤️' : ''}
            </div>
          </div>
        );
      })}
    </div>
  );
}

/* ════════════════════════════════════════════════════════════════════════
   SHEETS
   ════════════════════════════════════════════════════════════════════════ */

/* ── Movie detail ─────────────────────────────────────────────────────── */
function MovieDetailSheet({ movie, onClose, onPickList, onOpenMovie, flashToast }) {
  const store = useStore();
  const idx = movieLibIndex(store.movies());
  const lib = resolveLibMovie(movie, idx);
  const [full, setFull] = React.useState(movie && movie.runtime != null ? movie : null);
  const [similar, setSimilar] = React.useState([]);
  const [recs, setRecs] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [editingDate, setEditingDate] = React.useState(false);
  const [dateVal, setDateVal] = React.useState((lib && lib.watchedDate) || NUTRI_TODAY);
  const [notesVal, setNotesVal] = React.useState((lib && lib.userNotes) || '');
  const tmdbId = (movie && movie.tmdbId) != null ? movie.tmdbId : (lib && lib.tmdbId);
  const [liveImdb, setLiveImdb] = React.useState(null); // transient OMDb result for non-library movies

  React.useEffect(() => {
    let alive = true;
    if (tmdbId != null && TMDB.hasCredentials()) {
      setLoading(true);
      TMDB.details(tmdbId).then(d => {
        if (!alive) return;
        setFull(d);
        // Opportunistically fetch the real IMDb rating (OMDb) once we know the imdbId.
        maybeFetchImdb((d && d.imdbId) || (lib && lib.imdbId) || (movie && movie.imdbId));
      }).catch(() => {}).finally(() => alive && setLoading(false));
      TMDB.similar(tmdbId).then(r => alive && setSimilar(r.slice(0, 12))).catch(() => {});
      TMDB.recommendations(tmdbId).then(r => alive && setRecs(r.slice(0, 12))).catch(() => {});
    } else {
      maybeFetchImdb((lib && lib.imdbId) || (movie && movie.imdbId));
    }
    function maybeFetchImdb(imdbId) {
      if (!imdbId || !window.OMDB || !OMDB.hasKey()) return;
      const have = (lib && lib.imdbRating != null);
      if (have) return;
      OMDB.byImdb(imdbId).then(r => {
        if (!alive || r.imdbRating == null) return;
        setLiveImdb(r.imdbRating);
        if (lib && lib.id) store.setMovieImdbRating(lib.id, r); // persist + sync
      }).catch(() => {});
    }
    return () => { alive = false; };
  }, [tmdbId]);

  const m = full || lib || movie;
  const watched = movieIsWatched(lib);
  const liked = movieIsLiked(lib);
  const disliked = movieIsDisliked(lib);
  const onWatchlist = !!(lib && lib.watchlist);
  const diaryLinks = lib ? store.movieDiaryLinks(lib) : [];
  const diaryEntries = store.diary();

  function ensure() { return (lib && lib.id) || store.ensureMovie(m, 'detail'); }
  function like()     { store.toggleMovieLiked(ensure());    flashToast && flashToast(liked ? 'Like removed' : 'Liked ❤️'); }
  function dislike()  { store.toggleMovieDisliked(ensure()); flashToast && flashToast(disliked ? 'Cleared' : 'Nope — hidden from picks'); }
  function watchlist(){ store.toggleMovieWatchlist(ensure()); flashToast && flashToast(onWatchlist ? 'Removed from watchlist' : 'Saved to watchlist 🔖'); }
  function toggleWatched() {
    const id = ensure();
    if (watched) { if (!confirm('Remove “' + m.title + '” from watched?')) return; store.unmarkWatched(id); flashToast && flashToast('Removed from watched'); }
    else { store.markWatched(id, dateVal); flashToast && flashToast('Marked watched ✓'); }
  }
  function saveDate() { const id = ensure(); store.setWatchedDate(id, dateVal); setEditingDate(false); flashToast && flashToast('Watched date updated'); }
  function saveNotes() { const id = ensure(); store.setMovieNotes(id, notesVal); flashToast && flashToast('Note saved'); }
  function rate(r) { const id = ensure(); store.setMovieRating(id, r); if (r === 5) flashToast && flashToast('5★ — added to Liked'); }
  function del() { if (lib && confirm('Remove “' + m.title + '” from your library?')) { store.deleteMovie(lib.id); flashToast && flashToast('Removed'); onClose(); } }

  return (
    <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', background: 'var(--bg)', animation: 'sheetIn .25s cubic-bezier(.2,.7,.2,1)', paddingTop: 'var(--safe-top)' }}>
      <div style={{ padding: '10px 14px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
        <button onClick={onClose} style={topIcon}><XIcon size={18}/></button>
        <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--muted)' }}>Movie</div>
        {lib ? <button onClick={del} style={topIcon} aria-label="Delete"><TrashIconM size={16}/></button> : <div style={{ width: 36 }}/>}
      </div>
      <div style={{ flex: 1, overflow: 'auto', paddingBottom: 90 }}>
        {/* backdrop / poster header */}
        <div style={{ position: 'relative' }}>
          {m.backdropUrl
            ? <div style={{ height: 170, backgroundImage: `linear-gradient(to bottom, rgba(0,0,0,0.1), var(--bg)), url(${m.backdropUrl})`, backgroundSize: 'cover', backgroundPosition: 'center' }}/>
            : <div style={{ height: 90 }}/>}
          <div style={{ display: 'flex', gap: 14, padding: '0 16px', marginTop: m.backdropUrl ? -70 : 0 }}>
            <div style={{ width: 110, flexShrink: 0 }}><MoviePoster movie={m} w={110}/></div>
            <div style={{ flex: 1, minWidth: 0, paddingTop: m.backdropUrl ? 74 : 0 }}>
              <div style={{ fontWeight: 700, fontSize: 18, lineHeight: 1.2 }}>{m.title}</div>
              {m.originalTitle && m.originalTitle !== m.title && <div style={{ fontSize: 12, color: 'var(--muted)', fontStyle: 'italic' }}>{m.originalTitle}</div>}
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center', marginTop: 6, fontSize: 12.5, color: 'var(--muted)' }}>
                {m.year && <span>{m.year}</span>}
                {m.runtime ? <span>{m.runtime}m</span> : null}
                {(() => { const imdb = movieImdbRating(lib) != null ? movieImdbRating(lib) : liveImdb;
                  return imdb != null
                    ? <span style={{ color: '#E0A911', fontWeight: 800 }}>IMDb {_ratingStr(imdb)}{(lib && lib.imdbVotes) ? ' · ' + _compactNum(lib.imdbVotes) : ''}</span>
                    : null; })()}
                {m.tmdbRating != null && <span style={{ color: '#E89B3C', fontWeight: 700 }}>TMDb {_ratingStr(m.tmdbRating)}{m.voteCount ? ' (' + _compactNum(m.voteCount) + ')' : ''}</span>}
              </div>
            </div>
          </div>
        </div>

        <div style={{ padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 14 }}>
          {/* status chips (independent dimensions) */}
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {watched && <span style={statusChip('#4FA862')}>✓ Watched</span>}
            {liked && <span style={statusChip('#C2554E')}>❤️ Liked</span>}
            {onWatchlist && !watched && <span style={statusChip('#5784D8')}>🔖 Watchlist</span>}
            {disliked && <span style={statusChip('var(--muted)')}>👎 Disliked</span>}
            {m.genres && m.genres.map(g => <span key={g} style={statusChip('var(--muted)')}>{g}</span>)}
          </div>

          {/* primary actions */}
          <div style={{ display: 'flex', gap: 8 }}>
            <button onClick={like} style={actBtn(liked, '#C2554E')}><HeartIcon size={18} fill={liked ? '#fff' : 'none'}/>{liked ? 'Liked' : 'Like'}</button>
            <button onClick={dislike} style={actBtn(disliked, 'var(--muted)')}><ThumbDownIcon size={18} fill={disliked ? '#fff' : 'none'}/>Nope</button>
            <button onClick={watchlist} style={actBtn(onWatchlist, '#5784D8')}><BookmarkIcon size={18} fill={onWatchlist ? '#fff' : 'none'}/>{onWatchlist ? 'Saved' : 'Save'}</button>
            <button onClick={() => onPickList(ensure())} style={actBtn(false)}><ListPlusIcon size={18}/>List</button>
          </div>

          {/* watched toggle + date */}
          <div style={{ ...nutriStyles.card, padding: 12 }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
              <div style={{ fontWeight: 600, fontSize: 13.5 }}>{watched ? 'Watched' : 'Mark as watched'}</div>
              <button onClick={toggleWatched} style={watched
                ? { ...primaryBtn, background: '#4FA862', borderColor: '#4FA862', display: 'inline-flex', alignItems: 'center', gap: 5 }
                : subtleBtn}>
                {watched ? <><CheckIcon size={15}/> Watched</> : 'Mark watched'}
              </button>
            </div>
            {watched && (
              <div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                {editingDate ? (
                  <>
                    <input type="date" value={dateVal} onChange={e => setDateVal(e.target.value)} style={{ ...inputStyle, width: 'auto' }}/>
                    <button onClick={saveDate} style={primaryBtn}>Save</button>
                    <button onClick={() => setEditingDate(false)} style={subtleBtn}>Cancel</button>
                  </>
                ) : (
                  <>
                    <span style={{ fontSize: 13, color: '#4FA862', fontWeight: 600 }}>✓ {_mdy(lib.watchedDate)}{lib.systemDateAdded ? ' (approx.)' : ''}</span>
                    <button onClick={() => { setDateVal(lib.watchedDate || NUTRI_TODAY); setEditingDate(true); }} style={subtleBtn}>Edit date</button>
                  </>
                )}
              </div>
            )}
            {/* user rating */}
            <div style={{ marginTop: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
              <span style={{ fontSize: 12.5, color: 'var(--muted)' }}>Your rating</span>
              <MovieStars value={lib ? lib.userRating : null} onChange={rate}/>
            </div>
          </div>

          {/* dates summary */}
          {lib && (
            <div style={{ fontSize: 11.5, color: 'var(--muted)', display: 'flex', flexWrap: 'wrap', gap: 12 }}>
              {lib.addedDate && <span>Added {_mdy(lib.addedDate)}</span>}
              {lib.importedDate && <span>Imported {_mdy(lib.importedDate)}</span>}
              {lib.sourceAddedDateFromCSV && <span>CSV date {_mdy(lib.sourceAddedDateFromCSV)}</span>}
            </div>
          )}

          {/* overview */}
          {m.overview && <div><div style={miniLabel}>Overview</div><div style={{ fontSize: 13.5, lineHeight: 1.55, color: 'var(--text-2)' }}>{m.overview}</div></div>}
          {m.director && <div style={{ fontSize: 12.5, color: 'var(--muted)' }}><b style={{ color: 'var(--text)' }}>Director:</b> {m.director}</div>}
          {m.cast && m.cast.length > 0 && <div style={{ fontSize: 12.5, color: 'var(--muted)' }}><b style={{ color: 'var(--text)' }}>Cast:</b> {m.cast.join(', ')}</div>}

          {/* notes */}
          <div>
            <div style={miniLabel}>Your notes</div>
            <textarea value={notesVal} onChange={e => setNotesVal(e.target.value)} onBlur={saveNotes}
              placeholder="What did you think? Who recommended it?" rows={2}
              style={{ ...inputStyle, resize: 'vertical', minHeight: 52 }}/>
          </div>

          {/* linked diary memories */}
          {diaryLinks.length > 0 && (
            <div style={{ ...nutriStyles.card, padding: 12 }}>
              <div style={{ fontWeight: 600, fontSize: 13, marginBottom: 8, color: '#A99CE0' }}>📓 Linked memories</div>
              {diaryLinks.map(did => {
                const e = diaryEntries.find(x => x.id === did);
                if (!e) return null;
                return <div key={did} style={{ fontSize: 12.5, color: 'var(--text-2)', padding: '4px 0', borderTop: '1px solid var(--border)' }}>
                  <b>{e.title || 'Memory'}</b> <span style={{ color: 'var(--muted)' }}>· {_mdy(e.dateKey || e.date)}</span>
                </div>;
              })}
              <div style={{ fontSize: 10.5, color: 'var(--muted-2)', marginTop: 6 }}>Matched by date — your diary text is never changed.</div>
            </div>
          )}

          {/* similar + recommendations */}
          {loading && <div style={{ fontSize: 12, color: 'var(--muted)' }}>Loading more…</div>}
          {recs.length > 0 && <MovieRail title="Recommended" movies={recs} onOpen={(mm) => onOpenMovie && onOpenMovie(mm)}/>}
          {similar.length > 0 && <MovieRail title="Similar" movies={similar} onOpen={(mm) => onOpenMovie && onOpenMovie(mm)}/>}
        </div>
      </div>
    </div>
  );
}
function MovieRail({ title, movies, onOpen }) {
  const store = useStore();
  // Filter out movies the user has already watched or disliked from detail-sheet rails.
  const exclKeys = getExcludedMovieKeys(store.movies());
  const visible  = (movies || []).filter(m => !movieKeyExcluded(m, exclKeys));
  if (!visible.length) return null;
  return (
    <div>
      <div style={miniLabel}>{title}</div>
      <div style={{ display: 'flex', gap: 10, overflowX: 'auto', paddingBottom: 4 }} className="nutri-scroll-x">
        {visible.map(m => (
          <div key={m.tmdbId} onClick={() => onOpen(m)} style={{ width: 92, flexShrink: 0, cursor: 'pointer' }}>
            <MoviePoster movie={m} w={92}/>
            <div style={{ fontSize: 11, fontWeight: 600, marginTop: 4, lineHeight: 1.2, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{m.title}</div>
            {movieImdbRating(m) != null
              ? <div style={{ fontSize: 10.5, color: '#E0A911', fontWeight: 700 }}>IMDb {_ratingStr(movieImdbRating(m))}</div>
              : <div style={{ fontSize: 10.5, color: 'var(--muted-2)' }}>IMDb —</div>}
          </div>
        ))}
      </div>
    </div>
  );
}

/* ── Add movie manually (search → pick → status + date) ───────────────── */
function AddMovieSheet({ onClose, onPickList, flashToast }) {
  const store = useStore();
  const hasCreds = TMDB.hasCredentials();
  const [query, setQuery] = React.useState('');
  const [results, setResults] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [err, setErr] = React.useState(null);
  const [picked, setPicked] = React.useState(null);
  const [status, setStatus] = React.useState('watchlist');
  const [date, setDate] = React.useState(NUTRI_TODAY);
  const timer = React.useRef(0);

  function runSearch(q) {
    setErr(null);
    if (!q.trim() || !hasCreds) { setResults([]); return; }
    setLoading(true);
    TMDB.search(q.trim()).then(r => setResults(r.results.slice(0, 20))).catch(e => setErr(e.message)).finally(() => setLoading(false));
  }
  React.useEffect(() => { clearTimeout(timer.current); timer.current = setTimeout(() => runSearch(query), 350); return () => clearTimeout(timer.current); }, [query]);

  function save() {
    if (!picked) return;
    const id = store.ensureMovie(picked, 'manual');
    if (status === 'watched') store.markWatched(id, date);
    else store.setMovieStatus(id, status);
    const existed = store.movies().filter(m => m.tmdbId === picked.tmdbId).length;
    flashToast && flashToast('Added “' + picked.title + '”' + (status === 'watched' ? ' ✓' : ''));
    onClose();
  }

  if (!hasCreds) {
    return (
      <SheetShell title="Add a movie" onClose={onClose}>
        <EmptyState icon="🎬" title="Connect TMDb first" subtitle="Add your TMDb key in Movie Night → Settings to search and add movies."/>
      </SheetShell>
    );
  }

  return (
    <SheetShell title="Add a movie" onClose={onClose} onSave={picked ? save : null} canSave={!!picked}>
      {!picked && (
        <>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 12, padding: '8px 12px', marginBottom: 12 }}>
            <SearchIcon size={16}/>
            <input autoFocus value={query} onChange={e => setQuery(e.target.value)} placeholder="Search a movie title…"
              style={{ flex: 1, border: 0, background: 'transparent', outline: 'none', font: 'inherit', fontSize: 14, color: 'var(--text)' }}/>
          </div>
          {loading && <div style={{ color: 'var(--muted)', fontSize: 13, padding: 8 }}>Searching…</div>}
          {err && <div style={{ color: '#C2554E', fontSize: 12.5, padding: 8 }}>{err}</div>}
          {results.map(m => {
            const dup = store.findMovie(m);
            return (
              <button key={m.tmdbId} onClick={() => setPicked(m)} style={{
                width: '100%', textAlign: 'left', display: 'flex', gap: 10, alignItems: 'center',
                padding: 8, marginBottom: 8, borderRadius: 12, background: 'var(--surface)', border: '1px solid var(--border)', cursor: 'pointer', font: 'inherit',
              }}>
                <MoviePoster movie={m} w={44}/>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontWeight: 600, fontSize: 13.5, color: 'var(--text)' }}>{m.title}</div>
                  <div style={{ fontSize: 11.5, color: 'var(--muted)' }}>{m.year || '—'}{m.tmdbRating != null ? ' · ★ ' + _ratingStr(m.tmdbRating) : ''}{dup ? ' · already in library' : ''}</div>
                </div>
                <ChevronRight size={16}/>
              </button>
            );
          })}
          {!loading && query.trim() && results.length === 0 && !err && <div style={{ color: 'var(--muted)', fontSize: 13, padding: 8 }}>No matches.</div>}
        </>
      )}
      {picked && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div style={{ display: 'flex', gap: 12 }}>
            <MoviePoster movie={picked} w={80}/>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontWeight: 700, fontSize: 15 }}>{picked.title}</div>
              <div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 2 }}>{picked.year || ''}{picked.tmdbRating != null ? ' · ★ ' + _ratingStr(picked.tmdbRating) : ''}</div>
              <button onClick={() => setPicked(null)} style={{ ...subtleBtn, marginTop: 8 }}>‹ Pick another</button>
            </div>
          </div>
          <FormRow label="Status">
            <Segment options={['watchlist','liked','watched','disliked']} value={status} onChange={setStatus}/>
          </FormRow>
          {status === 'watched' && (
            <FormRow label="Watched date">
              <input type="date" value={date} onChange={e => setDate(e.target.value)} style={inputStyle}/>
            </FormRow>
          )}
          {store.findMovie(picked) && <div style={{ fontSize: 12, color: '#E89B3C' }}>Already in your library — this updates it (no duplicate).</div>}
        </div>
      )}
    </SheetShell>
  );
}

/* ── List picker (add a movie to one or more lists; create inline) ─────── */
function MovieListPickerSheet({ movieId, onClose, onResolve, flashToast }) {
  const store = useStore();
  const movie = store.movies().find(m => m.id === movieId);
  const lists = store.movieLists();
  const [newName, setNewName] = React.useState('');
  const addedRef = React.useRef(false);
  // Report to the caller (Cards swipe-up) whether anything was added, so it can advance the deck.
  function close() { if (onResolve) { try { onResolve(addedRef.current); } catch (_) {} } onClose(); }
  if (!movie) { return <SheetShell title="Add to list" onClose={close}><div style={{ color: 'var(--muted)' }}>Movie not found.</div></SheetShell>; }
  const inLists = movie.lists || [];
  const onWatchlist = !!movie.watchlist && !movie.watchedDate;
  function toggle(listId) {
    if (inLists.indexOf(listId) >= 0) { store.removeMovieFromList(movie.id, listId); }
    else { store.addMovieToList(movie.id, listId); addedRef.current = true; }
  }
  function toggleWatchlist() {
    store.toggleMovieWatchlist(movie.id);
    if (!onWatchlist) addedRef.current = true; // adding → Cards advances to next card
    flashToast && flashToast(onWatchlist ? 'Removed from Watchlist' : 'Added to Watchlist 🔖');
  }
  function createAndAdd() {
    const n = newName.trim(); if (!n) return;
    const id = store.addMovieList(n);
    store.addMovieToList(movie.id, id);
    addedRef.current = true;
    setNewName(''); flashToast && flashToast('Added to “' + n + '”');
  }
  return (
    <SheetShell title="Save to…" onClose={close} onSave={close} canSave={true}>
      <div style={{ display: 'flex', gap: 12, marginBottom: 14, alignItems: 'center' }}>
        <MoviePoster movie={movie} w={44}/>
        <div style={{ fontWeight: 600, fontSize: 14 }}>{movie.title}</div>
      </div>
      {/* Watchlist — first-class option (spec §4) */}
      <button onClick={toggleWatchlist} style={{
        width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '12px 14px', marginBottom: 8, borderRadius: 12, cursor: 'pointer', font: 'inherit',
        background: onWatchlist ? 'rgba(87,132,216,0.14)' : 'var(--surface)', border: '1px solid ' + (onWatchlist ? '#5784D8' : 'var(--border)'),
      }}>
        <span style={{ display: 'flex', alignItems: 'center', gap: 8, fontWeight: 600, fontSize: 14, color: 'var(--text)' }}><BookmarkIcon size={16} fill={onWatchlist ? '#5784D8' : 'none'}/>Watchlist</span>
        <span style={{ color: onWatchlist ? '#5784D8' : 'var(--muted-2)' }}>{onWatchlist ? <CheckIcon size={18}/> : '＋'}</span>
      </button>
      <div style={{ fontSize: 11, color: 'var(--muted)', fontWeight: 600, letterSpacing: '0.04em', textTransform: 'uppercase', margin: '12px 0 6px' }}>Lists</div>
      <div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
        <input value={newName} onChange={e => setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && createAndAdd()} placeholder="New list…" style={{ ...inputStyle, flex: 1 }}/>
        <button onClick={createAndAdd} disabled={!newName.trim()} style={{ ...primaryBtn, opacity: newName.trim() ? 1 : 0.5 }}>Create</button>
      </div>
      {lists.length === 0 && <div style={{ color: 'var(--muted)', fontSize: 13 }}>No lists yet — create one above.</div>}
      {lists.map(l => {
        const on = inLists.indexOf(l.id) >= 0;
        return (
          <button key={l.id} onClick={() => toggle(l.id)} style={{
            width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
            padding: '12px 14px', marginBottom: 8, borderRadius: 12, cursor: 'pointer', font: 'inherit',
            background: on ? 'rgba(79,168,98,0.12)' : 'var(--surface)', border: '1px solid ' + (on ? '#4FA862' : 'var(--border)'),
          }}>
            <span style={{ fontWeight: 600, fontSize: 14, color: 'var(--text)' }}>{l.name}</span>
            <span style={{ color: on ? '#4FA862' : 'var(--muted-2)' }}>{on ? <CheckIcon size={18}/> : '＋'}</span>
          </button>
        );
      })}
    </SheetShell>
  );
}

/* ── Settings (credentials, test, clear, attribution) ─────────────────── */
function _movieSyncDt(iso) {
  if (!iso) return 'never';
  try { return new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }
  catch (_) { return iso; }
}
function MovieSettingsSheet({ onClose, flashToast }) {
  const store = useStore();
  const creds = store.movieCredentials();
  const [apiKey, setApiKey] = React.useState(creds.apiKey || '');
  const [readToken, setReadToken] = React.useState(creds.readToken || '');
  const [omdbKey, setOmdbKey] = React.useState(creds.omdbApiKey || '');
  const [showKey, setShowKey] = React.useState(!(creds.apiKey || creds.readToken));
  const [showOmdb, setShowOmdb] = React.useState(!creds.omdbApiKey);
  const [testing, setTesting] = React.useState(false);
  const [result, setResult] = React.useState(null);
  const [omdbTesting, setOmdbTesting] = React.useState(false);
  const [omdbResult, setOmdbResult] = React.useState(null);
  const [busy, setBusy] = React.useState(null);      // 'imdb' | 'posters' | 'reload'
  const [progress, setProgress] = React.useState(null);
  const stopRef = React.useRef(false);
  const stats = store.movieSyncStats();
  const [health, setHealth] = React.useState(null);   // backend proxy status
  const [checking, setChecking] = React.useState(true);
  const [showDev, setShowDev] = React.useState(false); // API-key inputs hidden by default
  React.useEffect(() => {
    let alive = true;
    if (window.TMDB && TMDB.proxyHealth) TMDB.proxyHealth().then(h => { if (alive) { setHealth(h); setChecking(false); } });
    else setChecking(false);
    return () => { alive = false; };
  }, []);
  function recheck() { setChecking(true); TMDB.proxyHealth().then(h => { setHealth(h); setChecking(false); }); }
  const _hasAnyLocalKey = !!(creds.apiKey || creds.readToken || creds.omdbApiKey);
  function svcStatus() {
    if (checking && !health) return { label: 'Checking…', color: 'var(--muted)' };
    if (health && health.ok && health.tmdb) return { label: '✓ Connected', color: '#4FA862', sub: health.omdb ? 'TMDb + OMDb (IMDb) ready' : 'TMDb ready · set OMDB_API_KEY secret for IMDb ratings' };
    if (health && health.ok && !health.tmdb) return { label: '⚠ Backend up, API keys not set on server', color: '#E89B3C', sub: 'Set the TMDb/OMDb secrets (see DEPLOY_FUNCTIONS.md)' };
    if (_hasAnyLocalKey) return { label: 'Using your saved key', color: '#E89B3C', sub: 'Backend not deployed yet — deploy functions to hide keys' };
    return { label: 'Not connected', color: '#C2554E', sub: 'Deploy the Movie Night functions (DEPLOY_FUNCTIONS.md), or add a key under Developer' };
  }

  function mask(s) { if (!s) return ''; if (s.length <= 8) return '••••'; return s.slice(0, 4) + '••••' + s.slice(-4); }
  function save() {
    store.setMovieCredentials({ apiKey: apiKey.trim(), readToken: readToken.trim(), omdbApiKey: omdbKey.trim() });
    setShowKey(false); setShowOmdb(false); setResult(null);
    flashToast && flashToast('Saved');
  }
  async function test() {
    store.setMovieCredentials({ apiKey: apiKey.trim(), readToken: readToken.trim() });
    setTesting(true); setResult(null);
    const r = await TMDB.testConnection();
    setResult(r); setTesting(false);
    if (r.ok) { setShowKey(false); flashToast && flashToast('Connected ✓'); }
  }
  async function testOmdb() {
    store.setMovieCredentials({ omdbApiKey: omdbKey.trim() });
    setOmdbTesting(true); setOmdbResult(null);
    const r = await OMDB.testConnection();
    setOmdbResult(r); setOmdbTesting(false);
    if (r.ok) { setShowOmdb(false); flashToast && flashToast('OMDb connected ✓'); }
  }
  function clear() {
    if (!confirm('Clear your TMDb credentials? Your library, lists and ratings are kept.')) return;
    store.clearMovieCredentials(); setApiKey(''); setReadToken(''); setOmdbKey(''); setShowKey(true); setShowOmdb(true); setResult(null); setOmdbResult(null);
    flashToast && flashToast('Credentials cleared');
  }
  const hasSaved = !!(creds.apiKey || creds.readToken);

  // ── Data Sync / Repair runners ──
  async function runImdbSync() {
    if (busy) return;
    if (!(window.OMDB && OMDB.hasKey())) { flashToast && flashToast('Add your OMDb API key first'); return; }
    setBusy('imdb'); stopRef.current = false; setProgress({ phase: 'imdb', processed: 0, target: stats.missingImdbRating });
    const res = await store.syncImdbRatings({ onProgress: p => setProgress({ phase: 'imdb', ...p }), shouldStop: () => stopRef.current });
    setBusy(null); setProgress(null);
    if (res && res.error === 'no-omdb-key') flashToast && flashToast('Add your OMDb API key first');
    else flashToast && flashToast('IMDb sync — ' + (res.fetched || 0) + ' updated' + (res.failed ? (' · ' + res.failed + ' failed') : '') + (res.noImdb ? (' · ' + res.noImdb + ' no IMDb id') : ''));
  }
  async function runPosterRepair() {
    if (busy) return;
    if (!TMDB.hasCredentials()) { flashToast && flashToast('Connect TMDb first'); return; }
    setBusy('posters'); stopRef.current = false; setProgress({ phase: 'posters', processed: 0, target: stats.missingPoster });
    const res = await store.repairMoviePosters({ onProgress: p => setProgress({ phase: 'posters', ...p }), shouldStop: () => stopRef.current });
    setBusy(null); setProgress(null);
    flashToast && flashToast('Posters — ' + (res.repaired || 0) + ' fixed' + (res.stillMissing ? (' · ' + res.stillMissing + ' still missing') : ''));
  }
  async function runReload() {
    if (busy) return;
    if (!confirm('Reload Movie Night data from the cloud? Any unsynced local changes on this device may be replaced with the cloud copy.')) return;
    setBusy('reload');
    const r = await store.reloadMoviesFromCloud();
    setBusy(null);
    flashToast && flashToast(r.ok ? ('Reloaded ' + r.movies + ' movies from cloud') : ('Reload failed: ' + (r.error || '')));
  }
  function clearCache() {
    movieCacheClear();
    flashToast && flashToast('Local discovery cache cleared — your saved data is untouched');
  }
  function repairWatched() {
    const r = store.repairMovies();
    flashToast && flashToast(r.mergedDuplicates > 0 ? ('Synced · merged ' + r.mergedDuplicates + ' duplicate' + (r.mergedDuplicates === 1 ? '' : 's')) : 'Watched sync complete ✓');
  }
  function repairLists() {
    const r = store.repairMovieLists();
    flashToast && flashToast(r.recovered > 0 ? ('Recovered ' + r.recovered + ' list' + (r.recovered === 1 ? '' : 's') + ' — rename in Lists') : 'No lists needed recovery');
  }
  // One-tap full repair (spec §9): dedup + recover lists + posters, then IMDb if a key exists.
  async function runRepairAll() {
    if (busy) return;
    if (!confirm('Repair Movie Night Data?\n\nThis merges duplicates, recovers lost lists, refetches missing posters' + ((window.OMDB && OMDB.hasKey()) ? ', and syncs IMDb ratings' : '') + '. Your liked / watched / watchlist / lists / ratings / notes are all preserved.')) return;
    setBusy('all');
    store.repairMovies();
    store.repairMovieLists();
    stopRef.current = false;
    let posters = { repaired: 0 };
    if (TMDB.hasCredentials()) {
      setProgress({ phase: 'posters', processed: 0, target: stats.missingPoster });
      posters = await store.repairMoviePosters({ onProgress: p => setProgress({ phase: 'posters', ...p }), shouldStop: () => stopRef.current });
    }
    let imdb = null;
    if (window.OMDB && OMDB.hasKey() && !stopRef.current) {
      setProgress({ phase: 'imdb', processed: 0, target: stats.missingImdbRating });
      imdb = await store.syncImdbRatings({ onProgress: p => setProgress({ phase: 'imdb', ...p }), shouldStop: () => stopRef.current });
    }
    setBusy(null); setProgress(null);
    flashToast && flashToast('Repair complete — ' + (posters.repaired || 0) + ' posters' + (imdb ? (' · ' + (imdb.fetched || 0) + ' IMDb ratings') : '') + ' fixed');
  }

  const repairBtn = { ...subtleBtn, width: '100%', textAlign: 'left', display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 12px', opacity: busy ? 0.55 : 1 };

  return (
    <SheetShell title="Movie Night · Settings" onClose={onClose} onSave={save} canSave={!!(apiKey.trim() || readToken.trim() || omdbKey.trim())}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
        {/* ── Movie data service (secure backend) ── */}
        {(() => { const s = svcStatus(); return (
          <div style={{ ...nutriStyles.card, padding: 14 }}>
            <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontWeight: 700, fontSize: 14 }}>Movie data service</div>
                <div style={{ fontSize: 13, fontWeight: 700, marginTop: 4, color: s.color }}>{s.label}</div>
                {s.sub && <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2, lineHeight: 1.5 }}>{s.sub}</div>}
                {health && health.at && <div style={{ fontSize: 11, color: 'var(--muted-2)', marginTop: 3 }}>Last check: {_movieSyncDt(health.at)}</div>}
              </div>
              <button onClick={recheck} disabled={checking} style={subtleBtn}>{checking ? 'Checking…' : 'Recheck'}</button>
            </div>
            <div style={{ fontSize: 11, color: 'var(--muted-2)', marginTop: 10, lineHeight: 1.55 }}>
              Movies, posters &amp; IMDb ratings load automatically through a secure backend — no API keys needed in the app.
            </div>
            <button onClick={() => setShowDev(v => !v)} style={{ background: 'none', border: 0, color: 'var(--accent)', cursor: 'pointer', font: 'inherit', fontSize: 11.5, fontWeight: 600, marginTop: 8, padding: 0 }}>
              {showDev ? '▾ Hide developer / API keys' : '▸ Developer / API keys'}
            </button>
          </div>
        ); })()}

        {showDev && (<React.Fragment>
        <div style={{ ...nutriStyles.card, padding: 12 }}>
          <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 4 }}>TMDb credentials <span style={{ fontWeight: 400, fontSize: 11, color: 'var(--muted)' }}>· fallback only</span></div>
          <div style={{ fontSize: 12, color: 'var(--muted)', lineHeight: 1.5 }}>
            Not needed once the backend is deployed. Free from <span style={{ color: 'var(--text)' }}>themoviedb.org → Settings → API</span>. Paste your
            <b> API Key (v3)</b> or <b>Read Access Token (v4)</b> — either works.
          </div>
        </div>

        {hasSaved && !showKey ? (
          <div style={{ ...nutriStyles.card, padding: 12 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <div>
                <div style={{ fontSize: 12.5, fontWeight: 600 }}>Saved ✓</div>
                <div style={{ fontSize: 11.5, color: 'var(--muted)', fontFamily: 'monospace', marginTop: 3 }}>
                  {creds.readToken ? 'Token ' + mask(creds.readToken) : 'Key ' + mask(creds.apiKey)}
                </div>
              </div>
              <button onClick={() => setShowKey(true)} style={subtleBtn}>Edit</button>
            </div>
          </div>
        ) : (
          <>
            <FormRow label="API Key (v3)">
              <input value={apiKey} onChange={e => setApiKey(e.target.value)} placeholder="your_tmdb_api_key_here"
                autoComplete="off" autoCapitalize="off" spellCheck={false} style={{ ...inputStyle, fontFamily: 'monospace', fontSize: 12.5 }}/>
            </FormRow>
            <FormRow label="Read Access Token (v4)">
              <textarea value={readToken} onChange={e => setReadToken(e.target.value)} placeholder="your_tmdb_read_access_token_here"
                rows={3} autoComplete="off" autoCapitalize="off" spellCheck={false} style={{ ...inputStyle, fontFamily: 'monospace', fontSize: 12, resize: 'vertical', wordBreak: 'break-all' }}/>
            </FormRow>
          </>
        )}

        <div style={{ display: 'flex', gap: 8 }}>
          <button onClick={test} disabled={testing || !(apiKey.trim() || readToken.trim())} style={{ ...primaryBtn, flex: 1, opacity: (apiKey.trim() || readToken.trim()) ? 1 : 0.5 }}>
            {testing ? 'Testing…' : '⚡ Test TMDb'}
          </button>
          {hasSaved && <button onClick={clear} style={{ ...subtleBtn, color: '#C2554E', borderColor: 'rgba(194,85,78,0.4)' }}>Clear</button>}
        </div>

        {result && (
          <div style={{ ...nutriStyles.card, padding: 12, borderColor: result.ok ? 'rgba(79,168,98,0.5)' : 'rgba(194,85,78,0.5)' }}>
            <div style={{ fontSize: 13, fontWeight: 600, color: result.ok ? '#4FA862' : '#C2554E' }}>{result.ok ? '✓ ' : '✕ '}{result.message}</div>
            {!result.ok && <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 6, lineHeight: 1.5 }}>
              Double-check you copied the full value. The v4 token is a long string starting with “eyJ…”. The v3 key is a 32-character string. No quotes or spaces.
            </div>}
          </div>
        )}
        </React.Fragment>)}

        {/* OMDb — real IMDb ratings · ALWAYS visible (the user's manual IMDb-rating path) */}
        <div style={{ ...nutriStyles.card, padding: 12 }}>
          <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 4 }}>OMDb API key <span style={{ fontWeight: 400, fontSize: 11.5, color: 'var(--muted)' }}>· real IMDb ratings</span></div>
          <div style={{ fontSize: 12, color: 'var(--muted)', lineHeight: 1.5 }}>
            Free from <span style={{ color: 'var(--text)' }}>omdbapi.com → API Key</span>. Used <b>only</b> for the real IMDb
            rating (TMDb stays the source for posters &amp; everything else). Optional — leave blank to use TMDb ratings only.
          </div>
        </div>
        {creds.omdbApiKey && !showOmdb ? (
          <div style={{ ...nutriStyles.card, padding: 12 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <div>
                <div style={{ fontSize: 12.5, fontWeight: 600 }}>OMDb saved ✓</div>
                <div style={{ fontSize: 11.5, color: 'var(--muted)', fontFamily: 'monospace', marginTop: 3 }}>Key {mask(creds.omdbApiKey)}</div>
              </div>
              <button onClick={() => setShowOmdb(true)} style={subtleBtn}>Edit</button>
            </div>
          </div>
        ) : (
          <FormRow label="OMDb API Key">
            <input value={omdbKey} onChange={e => setOmdbKey(e.target.value)} placeholder="your_omdb_api_key_here"
              autoComplete="off" autoCapitalize="off" spellCheck={false} style={{ ...inputStyle, fontFamily: 'monospace', fontSize: 12.5 }}/>
          </FormRow>
        )}
        <div style={{ display: 'flex', gap: 8 }}>
          <button onClick={testOmdb} disabled={omdbTesting || !omdbKey.trim()} style={{ ...subtleBtn, flex: 1, opacity: omdbKey.trim() ? 1 : 0.5 }}>
            {omdbTesting ? 'Testing…' : '⚡ Test OMDb connection'}
          </button>
        </div>
        {omdbResult && (
          <div style={{ ...nutriStyles.card, padding: 12, borderColor: omdbResult.ok ? 'rgba(79,168,98,0.5)' : 'rgba(194,85,78,0.5)' }}>
            <div style={{ fontSize: 13, fontWeight: 600, color: omdbResult.ok ? '#4FA862' : '#C2554E' }}>{omdbResult.ok ? '✓ ' : '✕ '}{omdbResult.message}</div>
          </div>
        )}

        {/* ── Data Sync / Repair ── */}
        <div style={{ ...nutriStyles.card, padding: 12 }}>
          <div style={{ fontWeight: 700, fontSize: 14, marginBottom: 8 }}>Data Sync / Repair</div>
          {/* stats grid */}
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, fontSize: 12, marginBottom: 10 }}>
            <_Stat label="Movies" value={stats.total}/>
            <_Stat label="Watched" value={stats.watched}/>
            <_Stat label="Liked" value={stats.liked}/>
            <_Stat label="Watchlist" value={stats.watchlist}/>
            <_Stat label="Lists" value={stats.lists}/>
            <_Stat label="Missing posters" value={stats.missingPoster} warn={stats.missingPoster > 0}/>
            <_Stat label="IMDb ratings" value={stats.withImdbRating + ' / ' + stats.total}/>
            <_Stat label="No IMDb rating" value={stats.missingImdbRating} warn={stats.missingImdbRating > 0}/>
          </div>
          <div style={{ fontSize: 11, color: 'var(--muted-2)', lineHeight: 1.7, marginBottom: 10 }}>
            Last cloud sync: <b style={{ color: 'var(--muted)' }}>{_movieSyncDt(stats.lastSyncAt)}</b><br/>
            Last IMDb sync: <b style={{ color: 'var(--muted)' }}>{_movieSyncDt(stats.imdbSyncedAt)}</b>
            {stats.postersRepairedAt ? <> · Posters repaired: <b style={{ color: 'var(--muted)' }}>{_movieSyncDt(stats.postersRepairedAt)}</b></> : null}
          </div>

          {/* live progress */}
          {progress && (
            <div style={{ ...nutriStyles.card, padding: 10, marginBottom: 10, background: 'var(--surface-2)' }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, fontWeight: 600, marginBottom: 6 }}>
                <span>{progress.phase === 'imdb' ? 'Syncing IMDb ratings…' : 'Repairing posters…'}</span>
                <span style={{ color: 'var(--muted)' }}>{progress.processed || 0}/{progress.target || 0}</span>
              </div>
              <div style={{ height: 6, borderRadius: 3, background: 'var(--ring-track)', overflow: 'hidden' }}>
                <div style={{ height: '100%', width: ((progress.target ? Math.min(100, Math.round((progress.processed || 0) / progress.target * 100)) : 0)) + '%', background: 'var(--accent)', transition: 'width .2s' }}/>
              </div>
              <div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 6 }}>
                {progress.phase === 'imdb'
                  ? ((progress.fetched || 0) + ' fetched · ' + (progress.noImdb || 0) + ' no IMDb · ' + (progress.failed || 0) + ' failed')
                  : ((progress.repaired || 0) + ' fixed · ' + (progress.stillMissing || 0) + ' still missing · ' + (progress.failed || 0) + ' failed')}
              </div>
              <button onClick={() => { stopRef.current = true; }} style={{ ...subtleBtn, marginTop: 8 }}>Stop</button>
            </div>
          )}

          <div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
            <button onClick={runRepairAll} disabled={!!busy} style={{ ...repairBtn, background: 'var(--accent)', color: 'var(--on-accent)', borderColor: 'var(--accent)', fontWeight: 700 }}><span>✨ Repair Movie Night Data</span><span style={{ fontSize: 11.5, opacity: 0.85 }}>all-in-one</span></button>
            <button onClick={repairWatched} disabled={!!busy} style={repairBtn}><span>↻ Repair Watched Import Sync</span></button>
            {stats.orphanedLists > 0 && <button onClick={repairLists} disabled={!!busy} style={{ ...repairBtn, borderColor: 'rgba(232,155,60,0.5)' }}><span>🗂️ Recover Lost Lists</span><span style={{ color: '#C2554E', fontSize: 11.5 }}>{stats.orphanedLists} lost</span></button>}
            <button onClick={runPosterRepair} disabled={!!busy} style={repairBtn}><span>🖼️ Repair Missing Posters</span><span style={{ color: 'var(--muted)', fontSize: 11.5 }}>{stats.missingPoster}</span></button>
            <button onClick={runImdbSync} disabled={!!busy} style={repairBtn}><span>⭐ Sync IMDb Ratings</span><span style={{ color: 'var(--muted)', fontSize: 11.5 }}>{stats.missingImdbRating} left</span></button>
            <button onClick={runReload} disabled={!!busy} style={repairBtn}><span>☁︎ Reload From Firebase</span></button>
            <button onClick={clearCache} disabled={!!busy} style={repairBtn}><span>🧹 Clear Local Movie Cache</span></button>
          </div>
          <div style={{ fontSize: 10.5, color: 'var(--muted-2)', lineHeight: 1.6, marginTop: 8 }}>
            Clearing the local cache only drops the in-memory discovery feed — your movies, lists, ratings &amp; notes in Firebase are never touched.
          </div>
        </div>

        <div style={{ fontSize: 11, color: 'var(--muted-2)', lineHeight: 1.6, borderTop: '1px solid var(--border)', paddingTop: 12 }}>
          {TMDB.ATTRIBUTION}<br/>{(window.OMDB && OMDB.ATTRIBUTION) || ''}<br/>Credentials are stored with your account and used only for your requests.
        </div>
      </div>
    </SheetShell>
  );
}
function _Stat({ label, value, warn }) {
  return (
    <div style={{ background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 10, padding: '7px 10px' }}>
      <div style={{ fontSize: 10.5, color: 'var(--muted)' }}>{label}</div>
      <div style={{ fontSize: 15, fontWeight: 700, color: warn ? '#C2554E' : 'var(--text)' }}>{value}</div>
    </div>
  );
}

/* ── CSV import (upload → preview → mode → batch → summary) ────────────── */
function parseMovieCsv(text) {
  // Minimal RFC-4180-ish parser (handles quoted fields, embedded commas/newlines).
  const rows = []; let row = [], field = '', i = 0, inQ = false;
  while (i < text.length) {
    const c = text[i];
    if (inQ) {
      if (c === '"') { if (text[i + 1] === '"') { field += '"'; i += 2; continue; } inQ = false; i++; continue; }
      field += c; i++; continue;
    }
    if (c === '"') { inQ = true; i++; continue; }
    if (c === ',') { row.push(field); field = ''; i++; continue; }
    if (c === '\r') { i++; continue; }
    if (c === '\n') { row.push(field); rows.push(row); row = []; field = ''; i++; continue; }
    field += c; i++;
  }
  if (field.length || row.length) { row.push(field); rows.push(row); }
  return rows.filter(r => r.length && r.some(x => (x || '').trim()));
}
function _findCol(headers, names) {
  const lower = headers.map(h => (h || '').trim().toLowerCase());
  for (const n of names) { const idx = lower.indexOf(n.toLowerCase()); if (idx >= 0) return idx; }
  return -1;
}
function _normCsvDate(s) {
  const t = (s || '').trim(); if (!t) return '';
  // Common forms: 2021-05-22, 2021-05-22T..., 22 May 2021, 5/22/21
  let m = t.match(/^(\d{4})-(\d{2})-(\d{2})/); if (m) return m[1] + '-' + m[2] + '-' + m[3];
  const d = new Date(t); if (!isNaN(d.getTime())) return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
  return '';
}
function MovieCsvImportSheet({ onClose, flashToast }) {
  const store = useStore();
  const [rows, setRows] = React.useState(null);
  const [headers, setHeaders] = React.useState([]);
  const [parsed, setParsed] = React.useState([]);
  const [mode, setMode] = React.useState('watched'); // watched | watchlist | liked | library
  const [fileName, setFileName] = React.useState('');
  const [summary, setSummary] = React.useState(null);

  function handleFile(file) {
    if (!file) return;
    setFileName(file.name);
    const reader = new FileReader();
    reader.onload = () => {
      const grid = parseMovieCsv(String(reader.result || ''));
      if (grid.length < 2) { flashToast && flashToast('Empty or unreadable CSV', 'err'); return; }
      const hdr = grid[0];
      const body = grid.slice(1);
      const iImdb = _findCol(hdr, ['Const', 'imdb_id', 'imdbID', 'IMDb ID', 'imdb']);
      const iTitle = _findCol(hdr, ['Title', 'Name', 'Original Title', 'Movie']);
      const iYear = _findCol(hdr, ['Year', 'Release Year']);
      const iDate = _findCol(hdr, ['Date Rated', 'Created', 'Date Added', 'WatchedDate', 'Watched Date', 'Date']);
      const iRating = _findCol(hdr, ['Your Rating', 'Rating', 'My Rating']);
      const out = body.map(r => ({
        imdbId: iImdb >= 0 ? (r[iImdb] || '').trim() : '',
        title: iTitle >= 0 ? (r[iTitle] || '').trim() : '',
        year: iYear >= 0 ? (parseInt(r[iYear], 10) || null) : null,
        csvDate: iDate >= 0 ? _normCsvDate(r[iDate]) : '',
        userRating: iRating >= 0 && r[iRating] ? (parseFloat(r[iRating]) || null) : null,
      })).filter(x => x.title || x.imdbId);
      setHeaders(hdr); setRows(body); setParsed(out); setSummary(null);
    };
    reader.readAsText(file);
  }

  function doImport() {
    const today = todayISO();
    const records = parsed.map((p, i) => {
      const base = {
        imdbId: p.imdbId || '', title: p.title || (p.imdbId || 'Untitled'), year: p.year || null,
        userRating: p.userRating != null ? p.userRating : null, importedFrom: 'csv',
        lists: [], importOrder: i,
      };
      const hasDate = !!p.csvDate;
      if (mode === 'watched') {
        base.watchedDate = p.csvDate || today;
        base.systemDateAdded = !hasDate;
        base.sourceAddedDateFromCSV = p.csvDate || null;
      } else {
        base.userStatus = mode === 'library' ? 'new' : mode; // watchlist | liked | new
        base.importedDate = p.csvDate || today;
        base.sourceAddedDateFromCSV = p.csvDate || null;
        base.systemDateAdded = !hasDate;
      }
      return base;
    });
    const res = store.importMovies(records);
    setSummary(res);
    flashToast && flashToast('Imported ' + res.added + ' · merged ' + res.merged);
  }

  return (
    <SheetShell title="Import movies (CSV)" onClose={onClose}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
        {!summary && (
          <>
            {/* Android WebView blocks .click() on hidden inputs → wrap in a label (handoff §9.5) */}
            <label style={{ ...primaryBtn, display: 'inline-flex', justifyContent: 'center', cursor: 'pointer' }}>
              {fileName ? '↻ Choose another file' : '⬆︎ Choose CSV file'}
              <input type="file" accept=".csv,text/csv,text/plain" onChange={e => handleFile(e.target.files && e.target.files[0])} style={{ display: 'none' }}/>
            </label>
            {fileName && <div style={{ fontSize: 12, color: 'var(--muted)' }}>{fileName} · {parsed.length} rows detected</div>}

            <div style={{ fontSize: 12, color: 'var(--muted)', lineHeight: 1.5 }}>
              Works with IMDb / Letterboxd exports. Detected columns: <b>Title</b>, <b>Year</b>, <b>Const/imdb_id</b>,
              <b> Date Rated / Date Added / Created</b>, <b>Rating</b>. Movies match by IMDb ID, then title — no duplicates.
            </div>

            {parsed.length > 0 && (
              <>
                <FormRow label="Import as">
                  <Segment options={['watched','watchlist','liked','library']} value={mode} onChange={setMode}/>
                </FormRow>
                <div style={{ fontSize: 11.5, color: 'var(--muted)' }}>
                  {mode === 'watched'
                    ? 'CSV date → watched date (missing → today, flagged approximate).'
                    : 'CSV date → imported date (kept as source date).'}
                </div>
                {/* preview */}
                <div style={{ ...nutriStyles.card, padding: 0, overflow: 'hidden' }}>
                  <div style={{ padding: '8px 12px', fontSize: 11.5, fontWeight: 600, color: 'var(--muted)', borderBottom: '1px solid var(--border)' }}>Preview (first 12)</div>
                  <div style={{ maxHeight: 240, overflow: 'auto' }}>
                    {parsed.slice(0, 12).map((p, i) => (
                      <div key={i} style={{ display: 'flex', justifyContent: 'space-between', gap: 10, padding: '7px 12px', borderTop: i ? '1px solid var(--border)' : 0, fontSize: 12 }}>
                        <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.title || p.imdbId}{p.year ? ' (' + p.year + ')' : ''}</span>
                        <span style={{ color: 'var(--muted)', flexShrink: 0 }}>{p.csvDate || 'no date'}{p.imdbId ? ' · ' + p.imdbId : ' · ⚠ review'}</span>
                      </div>
                    ))}
                  </div>
                </div>
                <button onClick={doImport} style={{ ...primaryBtn, width: '100%' }}>Import {parsed.length} movies</button>
                <div style={{ fontSize: 10.5, color: 'var(--muted-2)' }}>{TMDB.ATTRIBUTION}</div>
              </>
            )}
          </>
        )}
        {summary && (
          <div style={{ ...nutriStyles.card, padding: 18, textAlign: 'center' }}>
            <div style={{ fontSize: 30, marginBottom: 6 }}>✅</div>
            <div style={{ fontWeight: 700, fontSize: 16 }}>Import complete</div>
            <div style={{ fontSize: 13, color: 'var(--muted)', marginTop: 8, lineHeight: 1.7 }}>
              <b style={{ color: 'var(--text)' }}>{summary.added}</b> new · <b style={{ color: 'var(--text)' }}>{summary.merged}</b> updated<br/>
              Library now has <b style={{ color: 'var(--text)' }}>{summary.total}</b> movies.
            </div>
            <div style={{ fontSize: 11.5, color: 'var(--muted-2)', marginTop: 10 }}>
              Movies imported without TMDb IDs show placeholder posters; open one and it’ll enrich from TMDb when connected.
            </div>
            <button onClick={onClose} style={{ ...primaryBtn, marginTop: 14 }}>Done</button>
          </div>
        )}
      </div>
    </SheetShell>
  );
}

/* ── shared style atoms ───────────────────────────────────────────────── */
const miniLabel = { fontSize: 11, color: 'var(--muted)', fontWeight: 600, letterSpacing: '0.04em', textTransform: 'uppercase', marginBottom: 6 };
function pill(active) {
  return {
    padding: '6px 12px', borderRadius: 999, whiteSpace: 'nowrap', flexShrink: 0,
    background: active ? 'var(--accent)' : 'var(--surface)', color: active ? 'var(--on-accent)' : 'var(--text)',
    border: '1px solid ' + (active ? 'var(--accent)' : 'var(--border-2)'),
    fontSize: 12, fontWeight: 600, cursor: 'pointer', font: 'inherit',
  };
}
function statusChip(color) {
  return { padding: '4px 10px', borderRadius: 999, background: 'var(--surface-2)', border: '1px solid var(--border)', color: color, fontSize: 11.5, fontWeight: 600 };
}
function actBtn(active, color) {
  return {
    flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, padding: '10px 0',
    borderRadius: 14, cursor: 'pointer', font: 'inherit', fontSize: 11, fontWeight: 600,
    background: active ? (color || 'var(--accent)') : 'var(--surface)', color: active ? '#fff' : 'var(--text)',
    border: '1px solid ' + (active ? (color || 'var(--accent)') : 'var(--border)'),
  };
}
function circleBtn(color, size) {
  const s = size || 58;
  return { width: s, height: s, borderRadius: '50%', display: 'grid', placeItems: 'center', cursor: 'pointer',
    background: 'var(--surface)', color: color, border: '2px solid ' + color, boxShadow: 'var(--shadow-md)', font: 'inherit' };
}
const primaryBtn = { padding: '9px 16px', borderRadius: 12, background: 'var(--accent)', color: 'var(--on-accent)', border: '1px solid var(--accent)', fontSize: 13, fontWeight: 600, cursor: 'pointer', font: 'inherit' };
const subtleBtn = { padding: '7px 12px', borderRadius: 10, background: 'var(--surface-2)', color: 'var(--text)', border: '1px solid var(--border-2)', fontSize: 12, fontWeight: 600, cursor: 'pointer', font: 'inherit' };
const chipBtn = { padding: '5px 10px', borderRadius: 999, background: 'var(--surface-2)', color: 'var(--text)', border: '1px solid var(--border-2)', fontSize: 11, fontWeight: 600, cursor: 'pointer', font: 'inherit' };
const iconSquare = { width: 40, borderRadius: 12, background: 'var(--surface-2)', border: '1px solid var(--border-2)', color: 'var(--text)', fontSize: 20, cursor: 'pointer', font: 'inherit' };
const iconGhost = { width: 32, height: 32, borderRadius: 8, background: 'transparent', border: 0, color: 'var(--muted)', cursor: 'pointer', display: 'grid', placeItems: 'center' };

Object.assign(window, {
  MoviesBrowse, MoviesCards, MoviesLists, MoviesLiked, MoviesWatched, MoviesWatchlist,
  MovieDetailSheet, AddMovieSheet, MovieSettingsSheet, MovieListPickerSheet, MovieCsvImportSheet,
  MoviePoster, MoviePosterGrid, MovieConnectPrompt, MovieLinkField, MovieRatingLine, MovieRatingChips, MovieSyncBanner,
  buildMovieRecommendations, buildMovieForYou, MoviesBestOfYear, MoviesSimilar,
  movieIsWatched, moviePrimaryDate, movieStarValue, movieIsLiked, movieIsDisliked, movieImdbRating, moviePosterUrl,
  getExcludedMovieKeys, movieKeyExcluded, movieCacheClear, sortWatchlist,
  dailyRatingQueue, moviesNeedingRating,
});
