// app-v2.jsx — Lista v2 engine. Evolved from app.jsx: per-aisle glass cards,
// in-list search, undo snackbar, tab drag-reorder, recipe quick-add,
// haptic-feel micro-animations. Exposes window.ShoppingApp2 (+ Stepper for sheets).
(function () {
const { useState, useEffect, useMemo, useRef, useCallback } = React;
const Ico = window.Ico;
const { setCtx, Circle2, Stepper2, Row2, Menu2, MenuItem2, Snackbar, Backdrop, fx, press } = window.V2;

// ── helpers ─────────────────────────────────────────────────────────
const lc = (s) => s.trim().toLowerCase();
const clone = (x) => JSON.parse(JSON.stringify(x));
const newId = () => 'i_' + Math.random().toString(36).slice(2, 9);

const ICONS_KEY = 'lista:icons', RECENTS_KEY = 'lista:recents';
const readJSON = (k, d) => { try { return JSON.parse(localStorage.getItem(k)) || d; } catch (e) { return d; } };
const writeJSON = (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} };
const rememberIcon = (name, emoji) => { const m = readJSON(ICONS_KEY, {}); const key = lc(name); if (emoji) m[key] = emoji; else delete m[key]; writeJSON(ICONS_KEY, m); };
const recallIcon = (name) => readJSON(ICONS_KEY, {})[lc(name)] || '';
const readRecents = (sk) => (readJSON(RECENTS_KEY, {})[sk] || []);
const pushRecent = (sk, entry) => {
  const all = readJSON(RECENTS_KEY, {});
  const list = (all[sk] || []).filter((e) => lc(e.name) !== lc(entry.name));
  list.unshift({ name: entry.name, emoji: entry.emoji || '', cat: entry.cat || 'Other', unit: entry.unit || '' });
  all[sk] = list.slice(0, 12); writeJSON(RECENTS_KEY, all);
};

function inferEntry(name, list, catalog) {
  const remembered = recallIcon(name);
  const q = lc(name);
  const hit =
    catalog.find((c) => lc(c.name) === q) ||
    catalog.find((c) => lc(c.name).startsWith(q)) ||
    catalog.find((c) => q.includes(lc(c.name)));
  if (hit) return { cat: hit.cat, unit: hit.unit, emoji: remembered || hit.emoji || '' };
  const kw = (window.SHOP_KEYWORDS || []).find((k) => k.kw.some((w) => q.includes(w)));
  if (kw) {
    const useCat = list.grouped && list.catOrder.includes(kw.cat) ? kw.cat : list.defaultCat;
    return { cat: useCat, unit: '', emoji: remembered || kw.emoji };
  }
  return { cat: list.defaultCat, unit: '', emoji: remembered || '' };
}

function buildView(list, mode) {
  const unchecked = list.items.filter((i) => !i.checked);
  const checked = list.items.filter((i) => i.checked);
  const eff = (!list.grouped && mode === 'aisle') ? 'added' : mode;
  let groups;
  if (eff === 'aisle' && list.grouped) {
    const order = list.catOrder.slice();
    unchecked.forEach((i) => { if (!order.includes(i.cat)) order.push(i.cat); });
    groups = order.map((cat) => ({ cat, items: unchecked.filter((i) => i.cat === cat) })).filter((g) => g.items.length);
  } else {
    let items = unchecked.slice();
    if (eff === 'alpha') items.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
    else if (eff === 'qty') items.sort((a, b) => (b.qty - a.qty) || a.name.localeCompare(b.name));
    groups = [{ cat: null, items }];
  }
  return { groups, checked, total: list.items.length, doneCount: checked.length };
}

function sortOptionsFor(list) {
  return [
    list.grouped && { key: 'aisle', uk: 'byAisle' },
    { key: 'alpha', uk: 'atoz' },
    { key: 'added', uk: 'recent' },
    list.showQty && { key: 'qty', uk: 'quantity' },
  ].filter(Boolean);
}
const defaultSortFor = (list) => (list.grouped ? 'aisle' : 'added');

function migrateItems(lists) {
  lists.forEach((list) => {
    const cat = (window.SHOP_SUGGEST && window.SHOP_SUGGEST[list.suggestKey]) || [];
    list.items.forEach((item) => {
      if (item.note === undefined) item.note = '';
      if (item.emoji === undefined) item.emoji = '';
      if (!item.emoji) {
        const hit = cat.find((c) => lc(c.name) === lc(item.name));
        if (hit && hit.emoji) { item.emoji = hit.emoji; return; }
        const q = lc(item.name);
        const kw = (window.SHOP_KEYWORDS || []).find((k) => k.kw.some((w) => q.includes(w)));
        if (kw) item.emoji = kw.emoji;
      }
    });
  });
  return lists;
}

// ── the app ─────────────────────────────────────────────────────────
function ShoppingApp2({ theme, instanceKey, showThumbs = true, onSettings, unitSystem = 'metric', bare = false, locale = 'en', companionId = 'brock', recentsEnabled = true, onShare, sharing = false, backdrop = 'dynamic' }) {
  const t = theme;
  setCtx({ unitSystem, locale });
  const L = window.I18N_T ? window.I18N_T(locale) : null;
  const tr = (k) => (L ? L.ui(k) : k);
  const trItem = (n) => (L ? L.item(n) : n);
  const trCat = (c) => (L ? L.cat(c) : c);
  const trList = (n) => (L ? L.list(n) : n);
  const trChecked = (c) => (L ? L.checked(c) : c);
  const dir = L ? L.dir : 'ltr';
  const ckLower = (c) => (locale === 'en' ? trChecked(c).toLowerCase() : trChecked(c));
  const storeKey = 'shoplist:v3:' + instanceKey;
  const saved = (() => { try { return JSON.parse(localStorage.getItem(storeKey) || 'null'); } catch (e) { return null; } })();

  const [lists, setLists] = useState(() => (saved && Array.isArray(saved.lists)) ? migrateItems(saved.lists) : clone(window.SHOP_SEED));
  const [activeId, setActiveId] = useState(() => (saved && saved.activeId) || (lists[0] && lists[0].id));
  const [sortModes, setSortModes] = useState(() => (saved && saved.sortModes) || {});
  const [sortOpen, setSortOpen] = useState(false);
  const [listMenuOpen, setListMenuOpen] = useState(false);
  const [query, setQuery] = useState('');
  const [focused, setFocused] = useState(false);
  const [searchOpen, setSearchOpen] = useState(false);
  const [searchQ, setSearchQ] = useState('');
  const [editEmojiId, setEditEmojiId] = useState(null);
  const [detailId, setDetailId] = useState(null);
  const [listSheet, setListSheet] = useState(null);
  const [recipesOpen, setRecipesOpen] = useState(false);
  const [flashId, setFlashId] = useState(null);
  const [lastAddedId, setLastAddedId] = useState(null);
  const [dragId, setDragId] = useState(null);
  const [dragTabId, setDragTabId] = useState(null);
  const [snack, setSnack] = useState(null);
  const [celebrate, setCelebrate] = useState(false);
  const celebTimer = useRef(null);
  const snackTimer = useRef(null);
  const dragRef = useRef({ id: null });
  const tabDrag = useRef({ id: null, timer: null, started: false, x: 0, y: 0 });
  const inputRef = useRef(null);
  const searchRef = useRef(null);
  const bodyRef = useRef(null);
  const flashTimer = useRef(null);

  useEffect(() => { try { localStorage.setItem(storeKey, JSON.stringify({ lists, activeId, sortModes })); } catch (e) {} }, [lists, activeId, sortModes]);
  useEffect(() => { if (searchOpen && searchRef.current) searchRef.current.focus(); }, [searchOpen]);

  const active = lists.find((l) => l.id === activeId) || lists[0];
  const catalog = window.SHOP_SUGGEST[active.suggestKey] || [];
  const sortOpts = sortOptionsFor(active);
  const sortMode = sortModes[active.id] || defaultSortFor(active);
  const effSort = (!active.grouped && sortMode === 'aisle') ? 'added' : sortMode;
  const sortLabel = tr((sortOpts.find((o) => o.key === effSort) || sortOpts[0]).uk);
  const rawView = useMemo(() => buildView(active, sortMode), [active, sortMode]);

  // in-list search filter
  const sq = lc(searchQ || '');
  const matches = (i) => !sq || lc(trItem(i.name)).includes(sq) || lc(i.name).includes(sq) || (i.note && lc(i.note).includes(sq));
  const view = useMemo(() => {
    if (!searchOpen || !sq) return rawView;
    const groups = rawView.groups.map((g) => ({ ...g, items: g.items.filter(matches) })).filter((g) => g.items.length);
    return { ...rawView, groups, checked: rawView.checked.filter(matches) };
  }, [rawView, searchOpen, sq, locale]);

  const patchActive = useCallback((fn) => { setLists((ls) => ls.map((l) => (l.id === activeId ? fn(l) : l))); }, [activeId]);
  const patchList = (listId, fn) => setLists((ls) => ls.map((l) => (l.id === listId ? fn(l) : l)));
  const setSort = (mode) => setSortModes((m) => ({ ...m, [active.id]: mode }));

  const showSnack = (label, undo) => {
    setSnack({ label, undoLabel: tr('undo'), undo });
    if (snackTimer.current) clearTimeout(snackTimer.current);
    snackTimer.current = setTimeout(() => setSnack(null), 5000);
  };
  const doUndo = () => { if (snack && snack.undo) snack.undo(); setSnack(null); };

  const doFlash = (id) => {
    setFlashId(id);
    if (flashTimer.current) clearTimeout(flashTimer.current);
    flashTimer.current = setTimeout(() => setFlashId(null), 480);
  };
  const toggle = (id) => {
    const itx = active.items.find((i) => i.id === id);
    if (itx && !itx.checked) { doFlash(id); pushRecent(active.suggestKey, itx); }
    patchActive((l) => {
      const items = l.items.map((i) => (i.id === id ? { ...i, checked: !i.checked } : i));
      const wasAllDone = l.items.length > 0 && l.items.every((i) => i.checked);
      const nowAllDone = items.length > 0 && items.every((i) => i.checked);
      if (nowAllDone && !wasAllDone) {
        setTimeout(() => {
          setCelebrate(true);
          if (celebTimer.current) clearTimeout(celebTimer.current);
          celebTimer.current = setTimeout(() => setCelebrate(false), 2800);
        }, 0);
      }
      return { ...l, items };
    });
  };
  const step = (id, d) => patchActive((l) => ({ ...l, items: l.items.map((i) => (i.id === id ? { ...i, qty: Math.max(1, i.qty + d) } : i)) }));
  const remove = (id) => {
    const listId = active.id;
    const idx = active.items.findIndex((i) => i.id === id);
    const item = active.items[idx];
    patchActive((l) => ({ ...l, items: l.items.filter((i) => i.id !== id) }));
    if (item) showSnack(`${trItem(item.name)} · ${tr('itemRemoved')}`, () =>
      patchList(listId, (l) => { const arr = l.items.slice(); arr.splice(Math.min(idx, arr.length), 0, item); return { ...l, items: arr }; }));
  };
  const clearChecked = () => {
    const listId = active.id;
    const kept = active.items.map((i, idx) => ({ i, idx })).filter((x) => x.i.checked);
    patchActive((l) => ({ ...l, items: l.items.filter((i) => !i.checked) }));
    if (kept.length) showSnack(tr('clearedChecked'), () =>
      patchList(listId, (l) => { const arr = l.items.slice(); kept.forEach((x) => arr.splice(Math.min(x.idx, arr.length), 0, x.i)); return { ...l, items: arr }; }));
  };
  const uncheckAll = () => patchActive((l) => ({ ...l, items: l.items.map((i) => ({ ...i, checked: false })) }));
  const setEmoji = (id, emoji) => { const itx = active.items.find((i) => i.id === id); if (itx) rememberIcon(itx.name, emoji); patchActive((l) => ({ ...l, items: l.items.map((i) => (i.id === id ? { ...i, emoji } : i)) })); };
  const saveItem = (id, name, note) => patchActive((l) => ({ ...l, items: l.items.map((i) => (i.id === id ? { ...i, name, note } : i)) }));

  // list management
  const addList = ({ name, emoji, type }) => {
    const l = window.makeList(type, name, emoji);
    setLists((ls) => [...ls, l]);
    setSortModes((m) => ({ ...m, [l.id]: defaultSortFor(l) }));
    setActiveId(l.id);
  };
  const renameList = (id, name, emoji) => setLists((ls) => ls.map((l) => (l.id === id ? { ...l, name, emoji } : l)));
  const deleteList = (id) => {
    if (lists.length <= 1) return;
    const idx = lists.findIndex((l) => l.id === id);
    const gone = lists[idx];
    const next = lists.filter((l) => l.id !== id);
    setLists(next);
    if (id === activeId) setActiveId(next[Math.max(0, idx - 1)].id);
    showSnack(`${trList(gone.name)} · ${tr('listRemoved')}`, () => {
      setLists((ls) => { const arr = ls.slice(); arr.splice(Math.min(idx, arr.length), 0, gone); return arr; });
      setActiveId(gone.id);
    });
  };

  const editingEmoji = editEmojiId ? active.items.find((i) => i.id === editEmojiId) : null;
  const detailItem = detailId ? active.items.find((i) => i.id === detailId) : null;

  const addItem = (rawName, entry) => {
    const name = (entry ? entry.name : rawName).trim();
    if (!name) return;
    const exist = active.items.find((i) => !i.checked && lc(i.name) === lc(name));
    if (exist) {
      if (active.showQty) step(exist.id, 1);
      doFlash(exist.id);
    } else {
      const info = entry || inferEntry(name, active, catalog);
      const row = { id: newId(), name, qty: 1, unit: info.unit || '', cat: info.cat || active.defaultCat, checked: false, emoji: info.emoji || '', note: '' };
      patchActive((l) => ({ ...l, items: [row, ...l.items] }));
      setLastAddedId(row.id);
    }
    setQuery('');
    if (bodyRef.current) bodyRef.current.scrollTop = 0;
    if (inputRef.current) inputRef.current.focus();
  };
  const addMany = (names) => {
    const rows = [];
    names.forEach((name) => {
      if (active.items.find((i) => !i.checked && lc(i.name) === lc(name))) return;
      const info = inferEntry(name, active, catalog);
      rows.push({ id: newId(), name, qty: 1, unit: info.unit || '', cat: info.cat || active.defaultCat, checked: false, emoji: info.emoji || '', note: '' });
    });
    if (!rows.length) return;
    patchActive((l) => ({ ...l, items: [...rows, ...l.items] }));
    setLastAddedId(rows[0].id);
  };

  const suggestions = useMemo(() => {
    const present = new Set(active.items.filter((i) => !i.checked).map((i) => lc(i.name)));
    let pool = catalog.filter((c) => !present.has(lc(c.name)));
    const q = lc(query);
    if (q) { pool = pool.filter((c) => lc(c.name).includes(q) || lc(trItem(c.name)).includes(q)); pool.sort((a, b) => (lc(a.name).startsWith(q) ? 0 : 1) - (lc(b.name).startsWith(q) ? 0 : 1)); }
    return pool.slice(0, 6);
  }, [active.items, catalog, query, locale]);
  const buyAgain = useMemo(() => {
    if (!recentsEnabled) return [];
    const present = new Set(active.items.filter((i) => !i.checked).map((i) => lc(i.name)));
    return readRecents(active.suggestKey).filter((e) => !present.has(lc(e.name))).slice(0, 6);
  }, [active.items, active.suggestKey, recentsEnabled]);
  const showBuyAgain = focused && !query && buyAgain.length > 0;
  const showChips = (query.length > 0 || (focused && buyAgain.length === 0)) && suggestions.length > 0;

  // drag-to-reorder rows (manual order only)
  const canReorder = effSort === 'added' && !sq;
  const reorderTo = (overId) => {
    const id = dragRef.current.id; if (!id || !overId || id === overId) return;
    patchActive((l) => {
      const arr = l.items.slice();
      const from = arr.findIndex((i) => i.id === id), to = arr.findIndex((i) => i.id === overId);
      if (from < 0 || to < 0) return l;
      const [m] = arr.splice(from, 1); arr.splice(to, 0, m); return { ...l, items: arr };
    });
  };
  const onGripDown = (e, id) => {
    e.preventDefault();
    dragRef.current = { id }; setDragId(id);
    const move = (ev) => {
      const el = document.elementFromPoint(ev.clientX, ev.clientY);
      const row = el && el.closest ? el.closest('[data-iid]') : null;
      if (row && row.dataset.iid) reorderTo(row.dataset.iid);
    };
    const up = () => { dragRef.current = { id: null }; setDragId(null); window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); };
    window.addEventListener('pointermove', move);
    window.addEventListener('pointerup', up);
  };

  // long-press drag to reorder list tabs
  const onTabDown = (e, id) => {
    const td = tabDrag.current;
    td.id = id; td.started = false; td.x = e.clientX; td.y = e.clientY;
    td.timer = setTimeout(() => { td.started = true; setDragTabId(id); }, 240);
    const move = (ev) => {
      if (!td.started) {
        if (Math.abs(ev.clientX - td.x) > 8 || Math.abs(ev.clientY - td.y) > 8) { clearTimeout(td.timer); cleanup(); }
        return;
      }
      ev.preventDefault();
      const el = document.elementFromPoint(ev.clientX, ev.clientY);
      const tab = el && el.closest ? el.closest('[data-tabid]') : null;
      const overId = tab && tab.dataset.tabid;
      if (overId && overId !== td.id) {
        setLists((ls) => {
          const from = ls.findIndex((l) => l.id === td.id), to = ls.findIndex((l) => l.id === overId);
          if (from < 0 || to < 0) return ls;
          const arr = ls.slice(); const [m] = arr.splice(from, 1); arr.splice(to, 0, m); return arr;
        });
      }
    };
    const cleanup = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); };
    const up = () => { clearTimeout(td.timer); if (td.started) setTimeout(() => setDragTabId(null), 0); td.id = null; td.started = false; cleanup(); };
    window.addEventListener('pointermove', move, { passive: false });
    window.addEventListener('pointerup', up);
  };

  const pct = view.total ? Math.round((rawView.doneCount / rawView.total) * 100) : 0;
  const toGet = rawView.total - rawView.doneCount;
  const chromeBg = t.chromeBg;
  const cardBorder = t.cardBorder;
  const dragCover = t.glass ? (t.mode === 'dark' ? 'rgba(18,24,36,0.94)' : 'rgba(245,247,252,0.94)') : t.color.surface;
  const hasRecipes = active.suggestKey === 'groceries' && active.showQty;
  const searching = searchOpen && !!sq;

  const groupCard = (g, gi) => (
    <div key={g.cat || 'all'} style={{ background: t.color.surface, ...fx(t), borderRadius: t.radius.card, boxShadow: t.shadow.card, padding: '4px 4px', border: cardBorder, marginBottom: 10 }}>
      {g.cat && (
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '11px 14px 5px' }}>
          <span style={{ fontFamily: t.font.head, fontSize: 11.5, fontWeight: 700, letterSpacing: '.1em', textTransform: 'uppercase', color: t.color.accent, whiteSpace: 'nowrap', flex: '0 0 auto', paddingInlineEnd: 8 }}>{trCat(g.cat)}</span>
          <span style={{ fontSize: 11, fontWeight: 700, color: t.color.textFaint, fontVariantNumeric: 'tabular-nums' }}>{g.items.length}</span>
        </div>
      )}
      {g.items.map((item, ii) => (
        <div key={item.id} data-iid={item.id} style={{ borderTop: ii > 0 ? `1px solid ${t.color.divider}` : 'none', margin: '0 6px', opacity: dragId === item.id ? 0.4 : 1 }}>
          <Row2 t={t} list={active} item={item} showThumbs={showThumbs} swipe grip={canReorder} onGripDown={onGripDown} sharing={sharing}
            flash={flashId === item.id} entering={lastAddedId === item.id} surfaceBg={dragCover}
            onToggle={toggle} onStep={step} onRemove={remove} onEditEmoji={setEditEmojiId} onOpenDetail={setDetailId} />
        </div>
      ))}
    </div>
  );

  return (
    <div dir={dir} style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column', background: t.color.canvas, fontFamily: t.font.body, color: t.color.text, position: 'relative', zIndex: 0, WebkitFontSmoothing: 'antialiased', direction: dir }}>
      <Backdrop t={t} mode={backdrop} />
      {bare ? (
        <div style={{ flex: '0 0 auto', height: 'env(safe-area-inset-top, 12px)', minHeight: 12, background: chromeBg }} />
      ) : (
      <div style={{ height: 40, flex: '0 0 auto', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 18px', position: 'relative', fontFamily: t.font.head, color: t.color.text }}>
        <span style={{ fontSize: 13.5, fontWeight: 600, letterSpacing: '.01em' }}>9:30</span>
        <div style={{ position: 'absolute', left: '50%', top: 11, transform: 'translateX(-50%)', width: 12, height: 12, borderRadius: '50%', background: t.mode === 'dark' ? '#05070a' : '#0c1018' }} />
        <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
          <svg width="15" height="13" viewBox="0 0 15 13" fill={t.statusIcons}><path d="M14 1.5v10H11.6v-7.6zM9.2 4.3v7.2H6.8V6.7zM4.4 7.3v4.2H2V9.7z" /></svg>
          <svg width="15" height="13" viewBox="0 0 16 13" fill={t.statusIcons}><path d="M8 11.6l6.9-8.6A11 11 0 008 .5 11 11 0 001.1 3z" /></svg>
          <svg width="13" height="13" viewBox="0 0 14 14" fill={t.statusIcons}><rect x="3.4" y="1.6" width="7.2" height="11" rx="1.6" /><rect x="5.3" y="0.6" width="3.4" height="1.8" rx="0.6" /></svg>
        </div>
      </div>
      )}

      {/* header + tabs */}
      <div style={{ flex: '0 0 auto', padding: '4px 20px 0' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
          <div style={{ fontSize: 12, fontWeight: 700, letterSpacing: '.14em', textTransform: 'uppercase', color: t.color.textFaint, fontFamily: t.font.head, whiteSpace: 'nowrap' }}>{tr('myLists')}</div>
          {onSettings && (
            <button onClick={onSettings} aria-label="Settings" title="Settings"
              style={{ flex: '0 0 auto', width: 32, height: 32, borderRadius: t.radius.pill, border: 'none', background: 'transparent', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0, transition: 'transform .18s ease' }}
              onPointerDown={(e) => (e.currentTarget.style.transform = 'scale(.9) rotate(35deg)')}
              onPointerUp={(e) => (e.currentTarget.style.transform = '')}
              onPointerLeave={(e) => (e.currentTarget.style.transform = '')}>
              {Ico.gear(t.color.textMuted, 19)}
            </button>
          )}
        </div>
        <div style={{ display: 'flex', gap: 8, marginTop: 12, marginLeft: -20, marginRight: -20, padding: '0 20px 2px', overflowX: 'auto' }} className="noscroll">
          {lists.map((l) => {
            const isActive = l.id === activeId;
            const cnt = l.items.filter((i) => !i.checked).length;
            const beingDragged = dragTabId === l.id;
            return (
              <button key={l.id} data-tabid={l.id} onPointerDown={(e) => onTabDown(e, l.id)}
                onClick={() => { if (dragTabId) return; setActiveId(l.id); setQuery(''); setSearchOpen(false); setSearchQ(''); }}
                title="Drag to reorder"
                style={{
                  flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: 7, padding: '8px 13px', borderRadius: t.radius.pill,
                  border: isActive ? 'none' : `1px solid ${t.glass ? t.color.border : 'transparent'}`, cursor: 'pointer',
                  fontFamily: t.font.head, fontSize: 13.5, fontWeight: 600, ...(isActive ? {} : fx(t)),
                  background: isActive ? t.accentGrad : t.color.surfaceAlt, color: isActive ? t.color.accentText : t.color.textMuted,
                  whiteSpace: 'nowrap',
                  boxShadow: isActive ? `0 4px 14px ${t.accentGlow}` : 'none',
                  transform: beingDragged ? 'scale(1.07)' : 'none', opacity: beingDragged ? 0.85 : 1,
                  transition: 'background .15s, color .15s, transform .15s, box-shadow .2s', touchAction: 'pan-x',
                }}>
                <span style={{ fontSize: 14 }}>{l.emoji}</span>
                <span>{trList(l.name)}</span>
                <span style={{ fontSize: 11, fontWeight: 700, fontVariantNumeric: 'tabular-nums', padding: '1px 6px', borderRadius: t.radius.pill, lineHeight: 1.4, background: isActive ? 'rgba(255,255,255,.24)' : (t.mode === 'dark' ? 'rgba(255,255,255,.06)' : 'rgba(0,0,0,.05)'), color: isActive ? t.color.accentText : t.color.textFaint }}>{cnt}</span>
              </button>
            );
          })}
          <button onClick={() => setListSheet({ mode: 'new' })} aria-label="New list" title="New list"
            style={{ flex: '0 0 auto', width: 38, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '8px 0', borderRadius: t.radius.pill, border: `1px dashed ${t.color.border}`, background: 'transparent', cursor: 'pointer', color: t.color.textMuted, ...fx(t) }}>
            {Ico.plus(t.color.textMuted, 15)}
          </button>
        </div>
      </div>

      {/* title area */}
      <div style={{ flex: '0 0 auto', padding: '14px 20px 10px' }}>
        <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 10 }}>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: t.font.head, fontSize: trList(active.name).length > 16 ? 22 : 27, fontWeight: t.headerWeight, letterSpacing: t.titleSpacing, lineHeight: 1.08, color: t.color.text, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{trList(active.name)}</div>
            <div style={{ display: 'flex', alignItems: 'center', gap: 9, marginTop: 6, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 13, color: t.color.textMuted, fontWeight: 500 }}>
                {toGet} {active.showQty ? tr('toGet') : tr('toDo')}{rawView.doneCount ? ` · ${rawView.doneCount} ${ckLower(active.checkedLabel)}` : ''}
              </span>
            </div>
          </div>
          <div style={{ flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: 7 }}>
            <button onClick={() => { if (searchOpen) { setSearchQ(''); } setSearchOpen((v) => !v); setSortOpen(false); setListMenuOpen(false); }} aria-label={tr('search')} title={tr('search')}
              style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 36, height: 36, borderRadius: t.radius.pill, cursor: 'pointer', border: `1px solid ${searchOpen ? 'transparent' : t.color.border}`, background: searchOpen ? t.color.accentSoft : 'transparent', transition: 'background .15s' }}>
              {Ico.search(searchOpen ? t.color.accent : t.color.textMuted, 15)}
            </button>
            <div style={{ position: 'relative' }}>
              <button onClick={() => { setSortOpen((v) => !v); setListMenuOpen(false); }}
                style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 11px', borderRadius: t.radius.pill, cursor: 'pointer', border: `1px solid ${sortOpen ? 'transparent' : t.color.border}`, background: sortOpen ? t.color.accentSoft : 'transparent', color: sortOpen ? t.color.accent : t.color.textMuted, fontFamily: t.font.head, fontSize: 12.5, fontWeight: 600, whiteSpace: 'nowrap', transition: 'background .15s, color .15s' }} title="Sort items">
                {Ico.sort(sortOpen ? t.color.accent : t.color.textMuted, 14)}
                <span>{sortLabel}</span>
                {Ico.chevron(sortOpen ? t.color.accent : t.color.textFaint, 11)}
              </button>
              <Menu2 t={t} open={sortOpen} onClose={() => setSortOpen(false)} title={tr('sortBy')}>
                {sortOpts.map((o) => (<MenuItem2 key={o.key} t={t} label={tr(o.uk)} selected={o.key === effSort} onClick={() => { setSort(o.key); setSortOpen(false); }} />))}
              </Menu2>
            </div>
            <div style={{ position: 'relative' }}>
              <button onClick={() => { setListMenuOpen((v) => !v); setSortOpen(false); }} aria-label="List options"
                style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 36, height: 36, borderRadius: t.radius.pill, cursor: 'pointer', border: `1px solid ${listMenuOpen ? 'transparent' : t.color.border}`, background: listMenuOpen ? t.color.accentSoft : 'transparent' }} title="List options">
                {Ico.dots(listMenuOpen ? t.color.accent : t.color.textMuted, 16)}
              </button>
              <Menu2 t={t} open={listMenuOpen} onClose={() => setListMenuOpen(false)}>
                <MenuItem2 t={t} label={tr('renameList')} icon={Ico.pencil(t.color.textMuted, 15)} onClick={() => { setListSheet({ mode: 'edit', list: active }); setListMenuOpen(false); }} />
                {rawView.doneCount > 0 && <MenuItem2 t={t} label={tr('uncheckAll')} icon={Ico.undo(t.color.textMuted, 15)} onClick={() => { uncheckAll(); setListMenuOpen(false); }} />}
                {rawView.doneCount > 0 && <MenuItem2 t={t} label={`${tr('clear')} ${ckLower(active.checkedLabel)}`} icon={Ico.trash(t.color.textMuted, 15)} onClick={() => { clearChecked(); setListMenuOpen(false); }} />}
                {lists.length > 1 && <MenuItem2 t={t} label={tr('deleteList')} icon={Ico.trash(t.color.danger, 15)} danger onClick={() => { deleteList(active.id); setListMenuOpen(false); }} />}
              </Menu2>
            </div>
          </div>
        </div>

        {searchOpen && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 11, background: t.color.surface, ...fx(t), borderRadius: t.radius.pill, border: `1.5px solid ${t.color.accent}`, padding: '0 12px', height: 42, animation: 'l2-enter .22s cubic-bezier(.3,1.1,.4,1)' }}>
            {Ico.search(t.color.textFaint, 15)}
            <input ref={searchRef} value={searchQ} onChange={(e) => setSearchQ(e.target.value)} placeholder={tr('searchList')}
              onKeyDown={(e) => { if (e.key === 'Escape') { setSearchQ(''); setSearchOpen(false); } }}
              style={{ flex: '1 1 auto', minWidth: 0, border: 'none', outline: 'none', background: 'transparent', fontFamily: t.font.body, fontSize: 14.5, fontWeight: 500, color: t.color.text }} />
            <button onClick={() => { setSearchQ(''); setSearchOpen(false); }} aria-label="close search" style={{ flex: '0 0 auto', border: 'none', background: 'transparent', cursor: 'pointer', padding: 4, display: 'flex' }}>
              {Ico.x(t.color.textFaint, 12)}
            </button>
          </div>
        )}

        <div style={{ marginTop: 12, height: 5, borderRadius: 999, background: t.color.track, overflow: 'hidden' }}>
          <div style={{ width: pct + '%', height: '100%', borderRadius: 999, background: t.accentGrad, transition: 'width .35s cubic-bezier(.3,.8,.3,1)' }} />
        </div>
      </div>

      {/* scrollable body */}
      <div ref={bodyRef} className="noscroll" style={{ flex: '1 1 auto', overflowY: 'auto', padding: '4px 12px 14px' }}>
        {view.groups.map(groupCard)}
        {view.groups.length === 0 && (
          <div style={{ background: t.color.surface, ...fx(t), borderRadius: t.radius.card, boxShadow: t.shadow.card, border: cardBorder, padding: '34px 20px 40px', textAlign: 'center', color: t.color.textFaint }}>
            <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 6 }}><window.Companion id={companionId} size={92} /></div>
            <div style={{ fontFamily: t.font.head, fontSize: 15, fontWeight: 700, color: t.color.text }}>{searching ? tr('noMatches') : tr('allDone')}</div>
            <div style={{ fontSize: 12.5, marginTop: 4 }}>{searching ? `“${searchQ.trim()}”` : tr('allDoneHint')}</div>
          </div>
        )}

        {view.checked.length > 0 && (
          <div style={{ marginTop: 6 }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 18px 8px' }}>
              <span style={{ fontFamily: t.font.head, fontSize: 12, fontWeight: 700, letterSpacing: '.06em', textTransform: 'uppercase', color: t.color.textFaint }}>{trChecked(active.checkedLabel)} · {view.checked.length}</span>
              <button onClick={clearChecked} style={{ border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: t.font.head, fontSize: 12.5, fontWeight: 600, color: t.color.textMuted, padding: '2px 4px' }}>{tr('clear')}</button>
            </div>
            <div style={{ background: t.checkedBg, ...(t.glass ? fx(t) : {}), borderRadius: t.radius.card, padding: '4px 4px', border: cardBorder }}>
              {view.checked.map((item, ii) => (
                <div key={item.id} style={{ borderTop: ii > 0 ? `1px solid ${t.color.divider}` : 'none', margin: '0 6px' }}>
                  <Row2 t={t} list={active} item={item} showThumbs={showThumbs} swipe={false} sharing={sharing} flash={flashId === item.id} onToggle={toggle} onStep={step} onRemove={remove} onEditEmoji={setEditEmojiId} onOpenDetail={setDetailId} />
                </div>
              ))}
            </div>
          </div>
        )}
      </div>

      {/* add bar */}
      <div style={{ flex: '0 0 auto', background: chromeBg, boxShadow: t.shadow.addbar, padding: '10px 16px 10px' }}>
        {showBuyAgain && (
          <div className="noscroll" style={{ display: 'flex', alignItems: 'center', gap: 7, overflowX: 'auto', paddingBottom: 9, marginLeft: -2 }}>
            <span style={{ flex: '0 0 auto', fontFamily: t.font.head, fontSize: 11, fontWeight: 700, letterSpacing: '.06em', textTransform: 'uppercase', color: t.color.textFaint, paddingInlineEnd: 2 }}>{tr('buyAgain')}</span>
            {buyAgain.map((c) => (
              <button key={c.name} onClick={() => addItem(null, c)}
                style={{ flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: 6, padding: '7px 11px 7px 9px', borderRadius: t.radius.chip, cursor: 'pointer', border: `1px solid ${t.color.border}`, background: t.color.surface, ...fx(t), fontFamily: t.font.body, fontSize: 13, fontWeight: 500, color: t.color.text }}>
                {showThumbs && c.emoji ? <span style={{ fontSize: 14, lineHeight: 1 }}>{c.emoji}</span> : Ico.plus(t.color.accent, 12)}
                <span>{trItem(c.name)}</span>
              </button>
            ))}
          </div>
        )}
        {showChips && (
          <div className="noscroll" style={{ display: 'flex', gap: 7, overflowX: 'auto', paddingBottom: 9, marginLeft: -2 }}>
            {suggestions.map((c) => (
              <button key={c.name} onClick={() => addItem(null, c)}
                style={{ flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: 6, padding: '7px 11px 7px 9px', borderRadius: t.radius.chip, cursor: 'pointer', border: `1px solid ${t.color.border}`, background: t.color.surface, ...fx(t), fontFamily: t.font.body, fontSize: 13, fontWeight: 500, color: t.color.text }}>
                {showThumbs && c.emoji ? <span style={{ fontSize: 14, lineHeight: 1 }}>{c.emoji}</span> : Ico.plus(t.color.accent, 12)}
                <span>{trItem(c.name)}</span>
              </button>
            ))}
          </div>
        )}
        <form onSubmit={(e) => { e.preventDefault(); addItem(query); }} style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
          {hasRecipes && (
            <button type="button" onClick={() => setRecipesOpen(true)} aria-label={tr('recipes')} title={tr('recipes')} {...press(0.88)}
              style={{ flex: '0 0 auto', width: 46, height: 46, borderRadius: t.radius.pill, border: `1px solid ${t.color.border}`, background: t.color.surface, ...fx(t), cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0, transition: 'transform .12s' }}>
              {Ico.chef(t.color.accent, 21)}
            </button>
          )}
          <div style={{ flex: '1 1 auto', display: 'flex', alignItems: 'center', gap: 9, background: t.color.surface, ...fx(t), borderRadius: t.radius.pill, border: `1.5px solid ${focused ? t.color.accent : t.color.border}`, padding: '0 6px', paddingInlineStart: 15, height: 46, transition: 'border-color .15s, box-shadow .2s', boxShadow: focused ? `0 0 0 3px ${t.color.accentSoft}` : 'none' }}>
            <input ref={inputRef} value={query} onChange={(e) => setQuery(e.target.value)} onFocus={() => setFocused(true)} onBlur={() => setTimeout(() => setFocused(false), 120)} placeholder={active.showQty ? tr('addItem') : tr('addTask')}
              style={{ flex: '1 1 auto', minWidth: 0, border: 'none', outline: 'none', background: 'transparent', fontFamily: t.font.body, fontSize: 15.5, fontWeight: 500, color: t.color.text }} />
            <button type="submit" aria-label="add" disabled={!query.trim()} {...(query.trim() ? press(0.86) : {})}
              style={{ flex: '0 0 auto', width: 36, height: 36, borderRadius: t.radius.pill, border: 'none', background: query.trim() ? t.accentGrad : t.color.surfaceAlt, boxShadow: query.trim() ? t.shadow.fab : 'none', cursor: query.trim() ? 'pointer' : 'default', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background .15s, box-shadow .2s, transform .12s' }}>
              {Ico.plus(query.trim() ? t.color.accentText : t.color.textFaint, 16)}
            </button>
          </div>
        </form>
      </div>

      {bare ? (
        <div style={{ flex: '0 0 auto', height: 'env(safe-area-inset-bottom, 8px)', minHeight: 8, background: chromeBg }} />
      ) : (
      <div style={{ flex: '0 0 auto', height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', background: chromeBg }}>
        <div style={{ width: 108, height: 4, borderRadius: 2, background: t.statusIcons, opacity: 0.35 }} />
      </div>
      )}

      <Snackbar t={t} snack={snack} onUndo={doUndo} onDismiss={() => setSnack(null)} />

      {/* list-complete celebration */}
      {celebrate && (
        <div onClick={() => setCelebrate(false)} style={{ position: 'absolute', inset: 0, zIndex: 78, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', background: t.mode === 'dark' ? 'rgba(6,9,15,.62)' : 'rgba(20,30,50,.34)', ...fx(t) }}>
          {[['14%', '24%', '#ffd23f'], ['82%', '20%', '#5b9bff'], ['20%', '68%', '#34d07f'], ['78%', '64%', '#e0723e'], ['46%', '14%', '#af52de'], ['64%', '32%', '#2bd4bf'], ['30%', '42%', '#ff5f8f'], ['72%', '48%', '#ffd23f']].map((c, i) => (
            <span key={i} style={{ position: 'absolute', left: c[0], top: c[1], width: 9, height: 9, borderRadius: i % 2 ? '50%' : 2, background: c[2], transform: `rotate(${i * 40}deg)`, animation: 'lista-pop .6s ease both' }} />
          ))}
          <div style={{ animation: 'lista-pop .5s cubic-bezier(.3,.9,.3,1)' }}><window.Companion id={companionId} size={132} /></div>
          <div style={{ fontFamily: t.font.head, fontSize: 24, fontWeight: 800, color: '#fff', marginTop: 12, letterSpacing: t.titleSpacing, whiteSpace: 'nowrap' }}>{tr('celebrate')}</div>
          <div style={{ fontSize: 14, color: 'rgba(255,255,255,.82)', marginTop: 8, textAlign: 'center', padding: '0 28px' }}>{tr('celebrateSub')}</div>
        </div>
      )}

      {/* sheets */}
      <window.ItemSheet t={t} locale={locale} item={detailItem} list={active}
        onSave={saveItem} onChangeIcon={(id) => setEditEmojiId(id)} onStep={step} onDelete={remove} onClose={() => setDetailId(null)} />
      <window.ListSheet t={t} locale={locale} sheet={listSheet}
        onSubmit={(d) => { if (listSheet && listSheet.mode === 'edit') renameList(listSheet.list.id, d.name, d.emoji); else addList(d); }}
        onDelete={deleteList} onClose={() => setListSheet(null)} />
      <window.EmojiSheet t={t} locale={locale} item={editingEmoji}
        onPick={(e) => { setEmoji(editEmojiId, e); setEditEmojiId(null); }}
        onClear={() => { setEmoji(editEmojiId, ''); setEditEmojiId(null); }}
        onClose={() => setEditEmojiId(null)} />
      <window.RecipeSheet t={t} locale={locale} open={recipesOpen} list={active}
        onAdd={(names) => { addMany(names); }} onClose={() => setRecipesOpen(false)} />
    </div>
  );
}

Object.assign(window, { ShoppingApp2, Stepper: Stepper2 });
})();
