// mobile.jsx — phone-only StudyBuddy frontend.
//
// Mounted by app.jsx for signed-in users on narrow viewports (<720px) or
// when ?mobile=1 is set. Pure UI — no Supabase, no real generation, no
// state.js / subjects.js / events.js. Hardcoded mock data. Every button
// either navigates, opens a modal, toggles state, or shows a toast.
//
// All internals scoped via IIFE — function names like `Topbar`, `Hamburger`,
// `LibraryScreen`, etc. are intentionally local so they don't collide with
// the desktop app.jsx, which has its own LibraryScreen / FlashcardScreen / etc.
//
// Public surface (exposed on window):
//   MobileApp           — React component, drop into App.
//   isMobileViewport()  — boolean, true if narrow OR ?mobile=1.

(function () {
  const { useState, useEffect, useRef, useMemo, createContext, useContext } = React;

  // ─── viewport detection ────────────────────────────────────────────────
  function isMobileViewport() {
    try {
      const url = new URL(window.location.href);
      if (url.searchParams.get('mobile') === '1') return true;
    } catch {}
    return window.innerWidth < 720;
  }

  // ─── mock data ─────────────────────────────────────────────────────────
  // Shapes match (where reasonable) the real Supabase rows so swapping in
  // useSubjects()/useGuides()/useEvents() is a near drop-in later.

  // All seven mock arrays/objects intentionally empty — every screen renders
  // its empty state. Wire backend hooks (useSubjects / useGuides / etc.) into
  // these names later for a near-drop-in.
  const MOCK_SUBJECTS = [];
  const MOCK_GUIDES = [];
  const MOCK_EVENTS = [];
  const MOCK_NOTES = [];

  // ─── helpers ───────────────────────────────────────────────────────────
  // Adapters: read from window.* fresh on every call so the data caches stay
  // the source of truth. Pass an explicit `subjects` list when available so
  // React renders update when the cache changes.

  const subjectById = (id, subjects) => {
    const list = subjects || (typeof window.useSubjects === 'function' ? null : MOCK_SUBJECTS);
    if (list) return list.find((s) => s.id === id);
    return null;
  };
  const subjectName = (id, subjects) => (subjectById(id, subjects) || {}).name || 'Other';

  // Renders a subject's icon: an uploaded image (icon_path → public URL) when
  // available, otherwise the emoji glyph, otherwise a fallback diamond. Pass
  // either `subject` (the row) or both `id` and `subjects` (cache list).
  function SubjectIcon({ subject, id, subjects, size, style }) {
    const s = subject || subjectById(id, subjects);
    const path = s && s.icon_path;
    if (path && typeof window.iconUrlForPath === 'function') {
      const url = window.iconUrlForPath(path);
      if (url) {
        const px = size || 16;
        return (
          <img
            src={url}
            alt=""
            style={{ width: px, height: px, objectFit: 'cover', borderRadius: 4, verticalAlign: 'middle', ...(style || {}) }}
          />
        );
      }
    }
    return <span style={style}>{(s && s.emoji) || '◈'}</span>;
  }

  // Adapt a Supabase `guides` row (app-level: { id, subjectId, title, sources, content, generatedAt })
  // to the flatter shape the mobile screens render.
  function adaptGuide(g) {
    if (!g) return null;
    const content = g.content || {};
    const flashcards = content.flashcards || [];
    const quiz = content.quiz || [];
    return {
      id: g.id,
      subjectId: g.subjectId || (g.content && g.content.subjectId) || null,
      title: g.title,
      sources: Array.isArray(g.sources) ? g.sources.length : (g.sources || 0),
      cards: flashcards.length,
      quizCount: quiz.length,
      when: relTime(g.generatedAt),
      mastery: 0,
      _raw: g,
    };
  }

  // Convert a parser file kind ('text' | 'image') + filename to the mobile UI
  // bucket ('pdf' | 'doc' | 'img' | 'txt').
  function uiSourceKind(src) {
    if (!src) return 'doc';
    if (src.kind === 'image') return 'img';
    const ext = (src.name || '').split('.').pop().toLowerCase();
    if (ext === 'pdf') return 'pdf';
    if (ext === 'docx' || ext === 'doc') return 'doc';
    if (ext === 'txt' || ext === 'md' || ext === 'markdown') return 'txt';
    return 'doc';
  }

  function findGuideById(id) {
    if (typeof window.findGuideSync === 'function') {
      return window.findGuideSync(id);
    }
    return null;
  }

  function eventsForDay(events, dayNum, monthIdx, year) {
    return (events || []).filter((e) => {
      if (!e || !e.date) return false;
      const [y, m, d] = e.date.split('-').map(Number);
      return d === dayNum && (m - 1) === monthIdx && y === year;
    });
  }

  function fmtDateLong(iso) {
    if (!iso) return '';
    const d = new Date(iso + 'T00:00:00');
    return d.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
  }

  // Local relativeTime fallback (app.jsx's helper isn't on window).
  function relTime(iso) {
    if (!iso) return '';
    const sec = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
    if (isNaN(sec) || sec < 0) return '';
    if (sec < 60)    return 'just now';
    if (sec < 3600)  return Math.floor(sec / 60) + 'm ago';
    if (sec < 86400) return Math.floor(sec / 3600) + 'h ago';
    return Math.floor(sec / 86400) + 'd ago';
  }

  function todayLocalISO() {
    if (typeof window.todayISO === 'function') return window.todayISO();
    const d = new Date();
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    return `${y}-${m}-${day}`;
  }

  function daysFromToday(iso) {
    if (!iso) return 0;
    if (typeof window.daysBetween === 'function') {
      try { return window.daysBetween(todayLocalISO(), iso); } catch {}
    }
    const today = new Date(todayLocalISO() + 'T00:00:00');
    const d = new Date(iso + 'T00:00:00');
    return Math.round((d - today) / 86400000);
  }

  // ─── nav context ───────────────────────────────────────────────────────

  const NavCtx = createContext(null);
  const useNav = () => useContext(NavCtx);

  // ─── shared chrome ─────────────────────────────────────────────────────

  function StatusBar() {
    // Hidden by default in production CSS; we still render so dev can flip
    // data-show="1" for screenshots.
    return <div className="sb-status"><span>9:41</span><div className="sb-status-icons" /></div>;
  }

  function Topbar({ leading, crumbs, actions }) {
    return (
      <div className="sb-topbar">
        <div className="sb-bar-left">
          {leading}
          {crumbs && (
            <div className="sb-crumbs">
              {crumbs.map((c, i) => (
                <React.Fragment key={i}>
                  {i > 0 && <span className="sb-sep">/</span>}
                  <span className={i === crumbs.length - 1 ? 'sb-crumb-last' : ''}>{c}</span>
                </React.Fragment>
              ))}
            </div>
          )}
        </div>
        <div className="sb-bar-actions">{actions}</div>
      </div>
    );
  }

  function Hamburger({ onClick }) {
    return (
      <button className="sb-icon-btn" aria-label="Menu" onClick={onClick}>
        <svg width="18" height="14" viewBox="0 0 18 14" fill="none">
          <path d="M1 1h16M1 7h16M1 13h16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
        </svg>
      </button>
    );
  }

  function Kebab({ onClick }) {
    return (
      <button className="sb-icon-btn" aria-label="More" onClick={onClick}>
        <svg width="4" height="16" viewBox="0 0 4 16">
          <circle cx="2" cy="2.5" r="1.5" fill="currentColor" />
          <circle cx="2" cy="8" r="1.5" fill="currentColor" />
          <circle cx="2" cy="13.5" r="1.5" fill="currentColor" />
        </svg>
      </button>
    );
  }

  function ChevLeft() {
    return (
      <svg width="8" height="14" viewBox="0 0 8 14" fill="none">
        <path d="M7 1L1 7l6 6" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
      </svg>
    );
  }
  function ChevRight() {
    return (
      <svg width="8" height="14" viewBox="0 0 8 14" fill="none">
        <path d="M1 1l6 6-6 6" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
      </svg>
    );
  }
  function PlusIcon() {
    return (
      <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
        <path d="M5.5 1v9M1 5.5h9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
      </svg>
    );
  }
  function Sparkle() {
    return (
      <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
        <path d="M5.5 0.5L6.5 4 10 5l-3.5 1L5.5 9.5 4.5 6 1 5l3.5-1L5.5 0.5z" fill="currentColor" />
      </svg>
    );
  }

  function BackBtn() {
    const nav = useNav();
    return (
      <button className="sb-icon-btn" onClick={() => nav.back()} aria-label="Back">
        <ChevLeft />
      </button>
    );
  }

  function CloseBtn({ onClick }) {
    const nav = useNav();
    return (
      <button className="sb-icon-btn" onClick={onClick || (() => nav.back())} aria-label="Close">
        ✕
      </button>
    );
  }

  function Doodles() {
    const items = [
      { c: '📐', x: 14, y: 30 },
      { c: '✎',  x: 78, y: 90 },
      { c: '∮',  x: 36, y: 180 },
      { c: '★',  x: 88, y: 220 },
      { c: '𝛼', x: 12, y: 320 },
      { c: '◈',  x: 62, y: 380 },
      { c: 'λ',  x: 28, y: 470 },
      { c: '🜂', x: 82, y: 530 },
      { c: '∞',  x: 18, y: 620 },
      { c: '𝜋', x: 70, y: 700 },
    ];
    return (
      <div className="sb-doodles">
        {items.map((it, i) => (
          <div key={i} style={{
            position: 'absolute',
            left: `${it.x}%`,
            top: it.y,
            fontFamily: 'var(--serif)',
            fontSize: 24,
            color: 'var(--ink-strong)',
          }}>{it.c}</div>
        ))}
      </div>
    );
  }

  // ─── drawer ────────────────────────────────────────────────────────────

  function Drawer({ open, onClose, active, onSelect, openNewGuide, openNewNote }) {
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const guidesRaw = window.useGuides ? window.useGuides() : MOCK_GUIDES;
    const events = window.useEvents ? window.useEvents() : MOCK_EVENTS;
    const notes = window.useNotes ? window.useNotes() : MOCK_NOTES;
    const auth = window.useAuth ? window.useAuth() : { user: null };
    const profile = (auth && auth.user && window.getProfile) ? window.getProfile(auth.user) : { displayName: '', email: '', avatarUrl: null };

    const guideCount = guidesRaw.length;
    const eventCount = events.length;
    const subjectCounts = useMemo(() => {
      const c = {};
      for (const g of guidesRaw) {
        const sid = g.subjectId || (g.content && g.content.subjectId);
        if (sid) c[sid] = (c[sid] || 0) + 1;
      }
      return c;
    }, [guidesRaw]);
    const noteCounts = useMemo(() => {
      const c = { unsorted: 0 };
      for (const n of notes) {
        if (n.subject_id) c[n.subject_id] = (c[n.subject_id] || 0) + 1;
        else c.unsorted += 1;
      }
      return c;
    }, [notes]);

    const initials = (profile.displayName || profile.email || 'U')
      .split(/\s+/).filter(Boolean).slice(0, 2)
      .map((p) => p[0].toUpperCase()).join('') || 'U';

    const onSignOut = async () => {
      try {
        await window.signOut();
        onClose();
      } catch (e) {
        window._sbToast && window._sbToast('sign-out failed: ' + (e.message || e));
      }
    };

    const go = (key) => { onSelect(key); onClose(); };

    return (
      <div className={'sb-drawer' + (open ? ' open' : '')}>
        <div className="sb-scrim" onClick={onClose}></div>
        <div className="sb-drawer-body">
          <div className="sb-sb-head">
            <div className="sb-sb-brand">
              <div className="sb-brand-logo">T</div>
              <div className="sb-brand-mark">Tung.scholar</div>
            </div>
            <button className="sb-icon-btn" onClick={() => { onClose(); go('settings'); }}>⚙</button>
          </div>
          <div className="sb-search-box" onClick={() => { onClose(); window._sbToast && window._sbToast('search not wired yet on mobile'); }}>
            <span className="sb-search-glyph">⌕</span>
            <span style={{ flex: 1 }}>Search</span>
          </div>
          <button className="sb-nav-primary" onClick={() => { onClose(); openNewGuide && openNewGuide(); }}>
            <PlusIcon /> New guide
          </button>
          <div className="sb-sb-scroll">
            <div className="sb-nav-section">
              <div className={'sb-nav-item' + (active === 'dashboard' ? ' active' : '')} onClick={() => go('dashboard')}>
                <div className="sb-nav-glyph">⌂</div>
                <div className="sb-nav-label">Dashboard</div>
              </div>
              <div className={'sb-nav-item' + (active === 'library' ? ' active' : '')} onClick={() => go('library')}>
                <div className="sb-nav-glyph">❖</div>
                <div className="sb-nav-label">All guides</div>
                <span className="sb-badge">{guideCount}</span>
              </div>
              <div className={'sb-nav-item' + (active === 'calendar' ? ' active' : '')} onClick={() => go('calendar')}>
                <div className="sb-nav-glyph">▦</div>
                <div className="sb-nav-label">Calendar</div>
                <span className="sb-badge">{eventCount}</span>
              </div>
              <div className={'sb-nav-item' + (active === 'notes' ? ' active' : '')} onClick={() => go('notes')}>
                <div className="sb-nav-glyph">✎</div>
                <div className="sb-nav-label">All notes</div>
                <span className="sb-badge">{notes.length}</span>
              </div>
            </div>
            <div className="sb-nav-section">
              <div className="sb-nav-section-label">
                <span>Subjects</span>
                <button className="sb-icon-btn" style={{ width: 22, height: 22, fontSize: 14 }} onClick={(e) => { e.stopPropagation(); onClose(); openNewGuide && openNewGuide(); }}>
                  <PlusIcon />
                </button>
              </div>
              {subjects.map((s) => (
                <div
                  key={s.id}
                  className={'sb-nav-item' + (active === 'folder:' + s.id ? ' active' : '')}
                  onClick={() => { onClose(); onSelect('folder:' + s.id); }}
                >
                  <div className="sb-nav-glyph"><SubjectIcon subject={s} /></div>
                  <div className="sb-nav-label">{s.name}</div>
                  <span className="sb-badge">{subjectCounts[s.id] || 0}</span>
                </div>
              ))}
            </div>
            <div className="sb-nav-section">
              <div className="sb-nav-section-label">
                <span>Notes</span>
                <button className="sb-icon-btn" style={{ width: 22, height: 22, fontSize: 14 }} onClick={(e) => { e.stopPropagation(); onClose(); openNewNote && openNewNote(); }}>
                  <PlusIcon />
                </button>
              </div>
              <div className="sb-nav-item" onClick={() => { onClose(); onSelect('notes'); }}>
                <div className="sb-nav-glyph">◔</div>
                <div className="sb-nav-label">Unsorted</div>
                <span className="sb-badge">{noteCounts.unsorted || 0}</span>
              </div>
              {subjects.filter((s) => noteCounts[s.id]).map((s) => (
                <div key={s.id} className="sb-nav-item" onClick={() => { onClose(); onSelect('folder:' + s.id); }}>
                  <div className="sb-nav-glyph"><SubjectIcon subject={s} /></div>
                  <div className="sb-nav-label">{s.name}</div>
                  <span className="sb-badge">{noteCounts[s.id]}</span>
                </div>
              ))}
            </div>
          </div>
          <div className="sb-sb-foot">
            <div className="sb-user-card">
              <div className="sb-avatar">
                {profile.avatarUrl
                  ? <img src={profile.avatarUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 'inherit' }} />
                  : initials}
              </div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div className="sb-uc-name">{profile.displayName || (profile.email || '').split('@')[0] || 'You'}</div>
                <div className="sb-uc-email">{profile.email}</div>
              </div>
              <button className="sb-icon-btn" onClick={onSignOut} aria-label="Sign out">⏻</button>
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ─── shell wrapper ─────────────────────────────────────────────────────

  function Screen({ children }) {
    return <div className="sb-screen"><StatusBar />{children}</div>;
  }

  // ─── modals ────────────────────────────────────────────────────────────

  function ModalSheet({ onClose, children }) {
    useEffect(() => {
      const onKey = (e) => { if (e.key === 'Escape') onClose(); };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [onClose]);
    return (
      <div className="sb-modal-scrim" onClick={onClose}>
        <div className="sb-modal-sheet" onClick={(e) => e.stopPropagation()}>
          <div className="sb-modal-handle"></div>
          {children}
        </div>
      </div>
    );
  }

  function NewGuideModal({ onClose, defaultSubjectId }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const [title, setTitle]         = useState('');
    const [subjectId, setSubjectId] = useState(defaultSubjectId || (subjects[0] && subjects[0].id) || '');
    const submit = () => {
      onClose();
      // Stash chosen { subjectId, title } so UploadScreen / GenerationScreen
      // can read it via params. The actual guide row is inserted after the
      // OpenAI generation completes.
      nav.go('upload', { subjectId: subjectId || null, title });
    };
    return (
      <ModalSheet onClose={onClose}>
        <h3 className="sb-modal-title">New guide</h3>
        <p className="sb-modal-sub">Set a title, pick a subject. We'll ask for sources next.</p>
        <label className="sb-field-label" htmlFor="newguide-title">Guide title</label>
        <input
          id="newguide-title"
          className="sb-input-mb"
          type="text"
          autoComplete="off"
          autoCapitalize="sentences"
          placeholder="e.g. Photosynthesis"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
        {subjects.length > 0 && (
          <>
            <label className="sb-field-label">Subject</label>
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {subjects.map((s) => (
                <span key={s.id} className={'sb-chip' + (s.id === subjectId ? ' active' : '')} onClick={() => setSubjectId(s.id)}>
                  <SubjectIcon subject={s} /> {s.name}
                </span>
              ))}
            </div>
          </>
        )}
        <div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
          <button className="sb-btn sb-block sb-ghost" onClick={onClose}>Cancel</button>
          <button className="sb-btn sb-block sb-accent" onClick={submit}>Continue →</button>
        </div>
      </ModalSheet>
    );
  }

  function NewNoteModal({ onClose, defaultSubjectId }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const [title, setTitle]         = useState('');
    const [subjectId, setSubjectId] = useState(defaultSubjectId || '');
    const [busy, setBusy] = useState(false);
    const submit = async () => {
      if (busy) return;
      setBusy(true);
      try {
        const row = await window.addNote({ subjectId: subjectId || null, title: title || 'Untitled note' });
        onClose();
        nav.go('editor', { noteId: row.id });
      } catch (e) {
        setBusy(false);
        window._sbToast && window._sbToast('Could not create note: ' + (e.message || e));
      }
    };
    return (
      <ModalSheet onClose={onClose}>
        <h3 className="sb-modal-title">New note</h3>
        <p className="sb-modal-sub">Title it (you can change later). Pick a subject if you want it filed.</p>
        <input className="sb-input-mb" placeholder="Note title" value={title} onChange={(e) => setTitle(e.target.value)} />
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
          <span className={'sb-chip' + (subjectId === '' ? ' active' : '')} onClick={() => setSubjectId('')}>Unsorted</span>
          {subjects.map((s) => (
            <span key={s.id} className={'sb-chip' + (s.id === subjectId ? ' active' : '')} onClick={() => setSubjectId(s.id)}>
              <SubjectIcon subject={s} /> {s.name}
            </span>
          ))}
        </div>
        <div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
          <button className="sb-btn sb-block sb-ghost" onClick={onClose} disabled={busy}>Cancel</button>
          <button className="sb-btn sb-block sb-accent" onClick={submit} disabled={busy}>{busy ? 'Creating…' : 'Open editor →'}</button>
        </div>
      </ModalSheet>
    );
  }

  function NewEventModal({ onClose, defaultDate, defaultSubjectId }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const [title, setTitle] = useState('');
    const [kind,  setKind]  = useState('exam');
    const [date,  setDate]  = useState(defaultDate || todayLocalISO());
    const [subjectId, setSubjectId] = useState(defaultSubjectId || '');
    const [notes, setNotes] = useState('');
    const [busy, setBusy] = useState(false);
    const submit = async () => {
      if (busy) return;
      if (!title.trim()) {
        window._sbToast && window._sbToast('Title is required');
        return;
      }
      if (!date) {
        window._sbToast && window._sbToast('Date is required');
        return;
      }
      setBusy(true);
      try {
        await window.addEvent({ subjectId: subjectId || null, title, kind, date, notes });
        onClose();
        window._sbToast && window._sbToast('event saved');
      } catch (e) {
        setBusy(false);
        window._sbToast && window._sbToast('Could not save event: ' + (e.message || e));
      }
    };
    return (
      <ModalSheet onClose={onClose}>
        <h3 className="sb-modal-title">New event</h3>
        <p className="sb-modal-sub">Track an exam, assignment, or study block.</p>
        <input className="sb-input-mb" placeholder="Title" value={title} onChange={(e) => setTitle(e.target.value)} />
        <input className="sb-input-mb" type="date" value={date} onChange={(e) => setDate(e.target.value)} />
        <div style={{ display: 'flex', gap: 6, marginBottom: 8, flexWrap: 'wrap' }}>
          {['exam', 'assignment', 'study', 'other'].map((k) => (
            <span key={k} className={'sb-chip' + (k === kind ? ' active' : '')} onClick={() => setKind(k)}>
              <span className={'sb-dot ' + k} style={{ marginRight: 6 }}></span>{k}
            </span>
          ))}
        </div>
        {subjects.length > 0 && (
          <>
            <label className="sb-field-label">Subject</label>
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 8 }}>
              <span className={'sb-chip' + (subjectId === '' ? ' active' : '')} onClick={() => setSubjectId('')}>none</span>
              {subjects.map((s) => (
                <span key={s.id} className={'sb-chip' + (s.id === subjectId ? ' active' : '')} onClick={() => setSubjectId(s.id)}>
                  <SubjectIcon subject={s} /> {s.name}
                </span>
              ))}
            </div>
          </>
        )}
        <input className="sb-input-mb" placeholder="Notes (optional)" value={notes} onChange={(e) => setNotes(e.target.value)} />
        <div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
          <button className="sb-btn sb-block sb-ghost" onClick={onClose} disabled={busy}>Cancel</button>
          <button className="sb-btn sb-block sb-accent" onClick={submit} disabled={busy}>{busy ? 'Saving…' : 'Save event'}</button>
        </div>
      </ModalSheet>
    );
  }

  // ─── mini calendar ─────────────────────────────────────────────────────

  function MiniCal({ compact = false, events, year, month, onPick }) {
    // Defaults to the current month so the calendar reflects "now".
    const now = new Date();
    const _year = year ?? now.getFullYear();
    const _month = month ?? now.getMonth();
    // Callers always pass `events` from a useEvents() hook one level up, so
    // we don't need our own hook subscription here. Avoiding it also keeps
    // hooks unconditional (Rules of Hooks). Falls back to [] if not passed.
    const eventList = events || [];

    const [stepMonth, setStepMonth] = useState(_month);
    const [stepYear, setStepYear] = useState(_year);
    const useStep = month == null;
    const M = useStep ? stepMonth : _month;
    const Y = useStep ? stepYear : _year;
    const todayHere = (now.getFullYear() === Y && now.getMonth() === M) ? now.getDate() : -1;

    const first = new Date(Y, M, 1);
    const startDow = first.getDay(); // 0..6 (Sun..Sat)
    const lastDay = new Date(Y, M + 1, 0).getDate();
    const prevLast = new Date(Y, M, 0).getDate();
    const cells = [];
    for (let i = startDow - 1; i >= 0; i--) cells.push({ n: prevLast - i, dim: true });
    for (let d = 1; d <= lastDay; d++) {
      const evHere = eventsForDay(eventList, d, M, Y).map((e) => e.kind);
      cells.push({ n: d, ev: evHere, today: d === todayHere });
    }
    while (cells.length % 7 !== 0) cells.push({ n: cells.length - lastDay - startDow + 1, dim: true });
    while (cells.length < 42) cells.push({ n: cells.length - lastDay - startDow + 1, dim: true });

    const monthName = new Date(Y, M, 1).toLocaleString(undefined, { month: 'long', year: 'numeric' });

    return (
      <div className="sb-mini-cal">
        <div className="sb-mini-cal-head">
          <div className="sb-month">{monthName}</div>
          <div className="sb-ctrls">
            <button className="sb-icon-btn" style={{ width: 28, height: 28 }} onClick={() => {
              if (M === 0) { setStepMonth(11); setStepYear(Y - 1); }
              else setStepMonth(M - 1);
            }}><ChevLeft /></button>
            <button className="sb-icon-btn" style={{ width: 40, height: 28, fontSize: 11.5 }} onClick={() => { setStepMonth(now.getMonth()); setStepYear(now.getFullYear()); }}>Today</button>
            <button className="sb-icon-btn" style={{ width: 28, height: 28 }} onClick={() => {
              if (M === 11) { setStepMonth(0); setStepYear(Y + 1); }
              else setStepMonth(M + 1);
            }}><ChevRight /></button>
          </div>
        </div>
        <div className="sb-mini-cal-grid">
          {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => (
            <div key={d} className="sb-mini-dow">{d.slice(0, 3)}</div>
          ))}
          {cells.map((c, i) => (
            <div
              key={i}
              className={'sb-mini-cell' + (c.dim ? ' dim' : '') + (c.today ? ' today' : '')}
              style={compact ? { minHeight: 44 } : {}}
              onClick={() => {
                if (c.dim || !onPick) return;
                const iso = `${Y}-${String(M + 1).padStart(2, '0')}-${String(c.n).padStart(2, '0')}`;
                onPick(c.n, M, Y, iso);
              }}
            >
              <span className="sb-day-num">{c.n}</span>
              {c.ev && c.ev.length > 0 && (
                <div className="sb-mini-dots">
                  {c.ev.map((k, j) => (
                    <span key={j} className={'sb-dot ' + k}></span>
                  ))}
                </div>
              )}
            </div>
          ))}
        </div>
      </div>
    );
  }

  // ─── screens ───────────────────────────────────────────────────────────

  function DashboardScreen({ openDrawer, openNewGuide, openNewEvent }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const guidesRaw = window.useGuides ? window.useGuides() : MOCK_GUIDES;
    const events = window.useEvents ? window.useEvents() : MOCK_EVENTS;
    const auth = window.useAuth ? window.useAuth() : { user: null };
    const profile = (auth && auth.user && window.getProfile) ? window.getProfile(auth.user) : { displayName: '', email: '' };
    const firstName = (profile.displayName || (profile.email || '').split('@')[0] || 'there').split(/\s+/)[0];

    const upcoming = useMemo(() => {
      return events
        .map((e) => ({
          id: e.id,
          subjectId: e.subject_id,
          title: e.title,
          kind: e.kind,
          date: e.date,
          notes: e.notes || '',
          days: daysFromToday(e.date),
        }))
        .filter((e) => e.days >= 0)
        .sort((a, b) => a.days - b.days)
        .slice(0, 3);
    }, [events]);
    const next = upcoming[0];
    const totalCards = useMemo(
      () => guidesRaw.reduce((a, g) => a + ((g.content && g.content.flashcards) ? g.content.flashcards.length : 0), 0),
      [guidesRaw]
    );

    const today = new Date();
    const dateEyebrow = today.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });

    return (
      <Screen>
        <Topbar
          leading={<Hamburger onClick={openDrawer} />}
          crumbs={['Dashboard']}
          actions={
            <>
              <Kebab onClick={() => window._sbToast && window._sbToast('more menu — coming soon')} />
              <button className="sb-btn sb-sm sb-accent" onClick={openNewGuide}><PlusIcon /> New guide</button>
            </>
          }
        />
        <div className="sb-page-pad" style={{ position: 'relative' }}>
          <Doodles />
          <div className="sb-pp-rel">
            <div className="sb-date-eyebrow">{dateEyebrow}</div>
            <h1 className="sb-page-title">Hello, <em>{firstName}.</em></h1>
            <div className="sb-page-subtitle">
              You have <strong>{upcoming.length} things</strong> on the calendar this week,
              <span className="sb-dot-sep">·</span>
              and <strong>{guidesRaw.length} guides</strong> in your library.
            </div>

            <div className="sb-kpi-grid">
              <div className="sb-kpi"><span className="sb-kpi-label">Subjects</span><span className="sb-kpi-num">{subjects.length}</span></div>
              <div className="sb-kpi"><span className="sb-kpi-label">Guides</span><span className="sb-kpi-num">{guidesRaw.length}</span></div>
              <div className="sb-kpi"><span className="sb-kpi-label">Cards</span><span className="sb-kpi-num">{totalCards}</span></div>
              <div className="sb-kpi"><span className="sb-kpi-label">This week</span><span className="sb-kpi-num">{upcoming.length}</span></div>
            </div>

            {next && (
              <div className="sb-upnext" onClick={() => nav.go('event', { id: next.id })}>
                <div className="sb-up-label">Up next</div>
                <div className="sb-up-title">{next.title}</div>
                <div className="sb-up-meta"><SubjectIcon id={next.subjectId} subjects={subjects} /> {subjectName(next.subjectId, subjects)} · {next.kind}</div>
                <div className="sb-up-days">{next.days}<span>{next.days === 1 ? 'day' : 'days'} away</span></div>
              </div>
            )}

            <div className="sb-upcoming">
              <div className="sb-upcoming-head">
                <h3>Upcoming</h3>
                <a onClick={() => nav.go('calendar')}>Open calendar →</a>
              </div>
              {upcoming.map((e) => (
                <div key={e.id} className="sb-upcoming-row" onClick={() => nav.go('event', { id: e.id })}>
                  <span className={'sb-dot ' + e.kind}></span>
                  <span className="sb-glyph"><SubjectIcon id={e.subjectId} subjects={subjects} /></span>
                  <span className="sb-title">{e.title}</span>
                  <span className="sb-when">in {e.days}d</span>
                </div>
              ))}
            </div>

            <MiniCal compact events={events} onPick={(d, m, y, iso) => {
              const evs = eventsForDay(events, d, m, y);
              if (evs.length) nav.go('event', { id: evs[0].id });
              else if (openNewEvent) openNewEvent(iso);
              else window._sbToast && window._sbToast(`${iso} — no events`);
            }} />

            <div className="sb-quick-grid">
              <div className="sb-quick-tile primary" onClick={openNewGuide}>
                <div className="sb-qt-glyph"><PlusIcon /></div>
                <div className="sb-qt-label">New guide</div>
                <div className="sb-qt-sub">upload + generate</div>
              </div>
              <div className="sb-quick-tile" onClick={openNewGuide}>
                <div className="sb-qt-glyph">↑</div>
                <div className="sb-qt-label">Upload files</div>
                <div className="sb-qt-sub">add to a subject</div>
              </div>
              <div className="sb-quick-tile" onClick={() => nav.go('library')}>
                <div className="sb-qt-glyph">❖</div>
                <div className="sb-qt-label">Library</div>
                <div className="sb-qt-sub">{guidesRaw.length} guides</div>
              </div>
              <div className="sb-quick-tile" onClick={() => nav.go('calendar')}>
                <div className="sb-qt-glyph">▦</div>
                <div className="sb-qt-label">Calendar</div>
                <div className="sb-qt-sub">{events.length} events</div>
              </div>
            </div>
          </div>
        </div>
      </Screen>
    );
  }

  function LibraryScreen({ openDrawer, openNewGuide }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const guidesRaw = window.useGuides ? window.useGuides() : MOCK_GUIDES;
    const guides = useMemo(() => guidesRaw.map(adaptGuide).filter(Boolean), [guidesRaw]);
    const [filter, setFilter] = useState('all');
    const list = useMemo(() => {
      if (filter === 'all') return guides;
      return guides.filter((g) => g.subjectId === filter);
    }, [filter, guides]);

    return (
      <Screen>
        <Topbar
          leading={<Hamburger onClick={openDrawer} />}
          crumbs={['All guides']}
          actions={<button className="sb-btn sb-sm sb-accent" onClick={openNewGuide}><PlusIcon /> New guide</button>}
        />
        <div className="sb-page-pad">
          <div className="sb-eyebrow">§ Library</div>
          <h1 className="sb-page-title">All guides</h1>
          <div className="sb-page-subtitle">
            <strong>{list.length} {list.length === 1 ? 'guide' : 'guides'}</strong>
            <span className="sb-dot-sep">·</span>
            Filter by subject to narrow.
          </div>
          <div className="sb-chips">
            <span className={'sb-chip' + (filter === 'all' ? ' active' : '')} onClick={() => setFilter('all')}>All</span>
            {subjects.map((s) => (
              <span key={s.id} className={'sb-chip' + (filter === s.id ? ' active' : '')} onClick={() => setFilter(s.id)}>
                <SubjectIcon subject={s} /> {s.name}
              </span>
            ))}
          </div>
          {list.map((g) => (
            <div key={g.id} className="sb-card-row" onClick={() => nav.go('guide', { id: g.id })}>
              <div className="sb-cr-line1">
                <span style={{ fontSize: 13 }}><SubjectIcon id={g.subjectId} subjects={subjects} size={13} /></span>
                <span>{subjectName(g.subjectId, subjects)}</span>
                <span className="sb-time">{g.when}</span>
              </div>
              <div className="sb-cr-title">{g.title}</div>
              <div className="sb-cr-meta">{g.sources} sources · {g.cards} cards</div>
            </div>
          ))}
          {list.length === 0 && (
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              No guides for this subject yet.
            </div>
          )}
        </div>
      </Screen>
    );
  }

  function NotesIndexScreen({ openDrawer, openNewNote }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const notesRaw = window.useNotes ? window.useNotes() : MOCK_NOTES;
    const notes = useMemo(() => notesRaw.map((n) => ({
      id: n.id,
      subjectId: n.subject_id,
      title: n.title || 'Untitled note',
      body: n.body_text || '',
      when: relTime(n.updated_at),
    })), [notesRaw]);
    const [filter, setFilter] = useState('all');
    const list = useMemo(() => {
      if (filter === 'all') return notes;
      return notes.filter((n) => n.subjectId === filter);
    }, [filter, notes]);

    return (
      <Screen>
        <Topbar
          leading={<Hamburger onClick={openDrawer} />}
          crumbs={['All notes']}
          actions={<button className="sb-btn sb-sm sb-accent" onClick={openNewNote}><PlusIcon /> New note</button>}
        />
        <div className="sb-page-pad">
          <div className="sb-eyebrow">§ Notes</div>
          <h1 className="sb-page-title">All notes</h1>
          <div className="sb-page-subtitle">
            <strong>{list.length} {list.length === 1 ? 'note' : 'notes'}</strong>
            <span className="sb-dot-sep">·</span>
            Notes autosave as you write.
          </div>
          <div className="sb-chips">
            <span className={'sb-chip' + (filter === 'all' ? ' active' : '')} onClick={() => setFilter('all')}>All</span>
            {subjects.map((s) => (
              <span key={s.id} className={'sb-chip' + (filter === s.id ? ' active' : '')} onClick={() => setFilter(s.id)}>
                <SubjectIcon subject={s} /> {s.name}
              </span>
            ))}
          </div>
          {list.map((n) => (
            <div key={n.id} className="sb-note-row" onClick={() => nav.go('editor', { noteId: n.id })}>
              <div className="sb-nr-head">
                <div className="sb-nr-title">{n.title}</div>
                <div className="sb-nr-time">{n.when}</div>
              </div>
              <div className="sb-nr-snippet">
                {n.subjectId && <span style={{ marginRight: 6 }}><SubjectIcon id={n.subjectId} subjects={subjects} /> {subjectName(n.subjectId, subjects)} ·</span>}
                {(n.body || '').slice(0, 200)}
              </div>
            </div>
          ))}
          {list.length === 0 && (
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              No notes yet.
            </div>
          )}
        </div>
      </Screen>
    );
  }

  function FolderScreen({ openDrawer, openNewNote, params }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const notesRaw = window.useNotes ? window.useNotes() : MOCK_NOTES;
    const subject = subjectById(params.subjectId, subjects);
    const notes = useMemo(
      () => notesRaw.filter((n) => n.subject_id === params.subjectId),
      [notesRaw, params.subjectId]
    );

    return (
      <Screen>
        <Topbar
          leading={<BackBtn />}
          crumbs={['All notes', subject ? subject.name : 'Folder']}
          actions={
            <>
              <Kebab onClick={() => window._sbToast && window._sbToast('folder options coming soon')} />
              <button className="sb-btn sb-sm sb-accent" onClick={openNewNote}><PlusIcon /> New note</button>
            </>
          }
        />
        <div className="sb-page-pad">
          <div className="sb-eyebrow">§ Folder</div>
          <h1 className="sb-page-title" style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
            <span style={{ fontSize: 28 }}>{subject ? <SubjectIcon subject={subject} size={28} /> : '◈'}</span>
            <em>{subject ? subject.name : 'Folder'}.</em>
          </h1>
          <div className="sb-page-subtitle">
            <strong>{notes.length} {notes.length === 1 ? 'note' : 'notes'}</strong> in this folder.
            <span className="sb-dot-sep">·</span>
            Notes auto-grouped by subject.
          </div>
          <div style={{
            background: 'var(--page)',
            border: '1px solid var(--hairline)',
            borderRadius: 'var(--r-md)',
            padding: '0 14px',
          }}>
            {notes.map((n) => (
              <div key={n.id} className="sb-note-row" onClick={() => nav.go('editor', { noteId: n.id })}>
                <div className="sb-nr-title">{n.title || 'Untitled note'}</div>
                <div className="sb-nr-snippet">{(n.body_text || '').slice(0, 200)}</div>
                <div className="sb-nr-time" style={{ marginTop: 2 }}>
                  {relTime(n.updated_at)}
                </div>
              </div>
            ))}
            {notes.length === 0 && (
              <div style={{ padding: '24px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
                No notes here yet.
              </div>
            )}
          </div>
        </div>
      </Screen>
    );
  }

  function NoteEditorScreen({ params }) {
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    // Subscribe to notes cache so updates from elsewhere refresh the editor.
    const notesAll = window.useNotes ? window.useNotes() : MOCK_NOTES;
    const cached = useMemo(
      () => (typeof window.findNoteSync === 'function' ? window.findNoteSync(params.noteId) : null),
      [params.noteId, notesAll]
    );

    const [note, setNote] = useState(cached || null);
    const [title, setTitle] = useState(cached ? (cached.title || '') : '');
    const [body, setBody] = useState(cached ? (cached.body_text || '') : '');
    const [savedAt, setSavedAt] = useState(cached ? cached.updated_at : null);
    const [saving, setSaving] = useState(false);
    const initialLoaded = useRef(!!cached);
    const titleTimer = useRef(null);
    const bodyTimer = useRef(null);

    // Async fallback when navigating directly to a noteId we haven't cached.
    useEffect(() => {
      if (initialLoaded.current) return;
      let cancelled = false;
      (async () => {
        try {
          const fetched = typeof window.findNote === 'function' ? await window.findNote(params.noteId) : null;
          if (cancelled || !fetched) return;
          setNote(fetched);
          setTitle(fetched.title || '');
          setBody(fetched.body_text || '');
          setSavedAt(fetched.updated_at);
          initialLoaded.current = true;
        } catch {}
      })();
      return () => { cancelled = true; };
    }, [params.noteId]);

    // Refresh local state if the cached row updated externally (e.g. another
    // session updated_at).
    useEffect(() => {
      if (!cached) return;
      if (!note) {
        setNote(cached);
        setTitle(cached.title || '');
        setBody(cached.body_text || '');
        setSavedAt(cached.updated_at);
        initialLoaded.current = true;
      }
    }, [cached]);

    const subject = note && note.subject_id ? subjectById(note.subject_id, subjects) : null;

    // Warn once-per-session-per-note when opening a note authored in the
    // desktop Tiptap editor: the mobile editor only reads body_text and
    // re-emits a flat <p>...</p> wrapper, so any rich formatting (headings,
    // bold, lists, etc.) gets discarded on the next save. Show confirm()
    // so the user gets an unambiguous accept/decline; bail out on decline.
    const navForWarn = useNav();
    const warnedRef = useRef(false);
    useEffect(() => {
      if (!note) return;
      if (warnedRef.current) return;
      const html = note.body_html || '';
      // Strip <p>, </p>, <br>, <br/>, <br /> and whitespace; if anything
      // remains that looks like a tag, this note has rich formatting.
      const stripped = html
        .replace(/<\/?p\b[^>]*>/gi, '')
        .replace(/<br\s*\/?>/gi, '')
        .trim();
      const hasRich = /<\w/.test(stripped);
      if (!hasRich) { warnedRef.current = true; return; }
      const sessionKey = 'sb-rich-warn-' + note.id;
      try {
        if (sessionStorage.getItem(sessionKey)) {
          warnedRef.current = true;
          return;
        }
      } catch {}
      const ok = (typeof window.confirm === 'function')
        ? window.confirm("This note has rich formatting from the desktop editor. Editing it on mobile will discard formatting (bold, headings, lists, etc.). Continue?")
        : true;
      try { sessionStorage.setItem(sessionKey, '1'); } catch {}
      warnedRef.current = true;
      if (!ok) {
        navForWarn.back();
      }
    }, [note]);

    // Debounced title save. On unmount we FLUSH the pending update synchronously
    // (fire-and-forget) so a draft isn't silently lost when the user navigates
    // away within the 600ms debounce window.
    // Refs hold the latest title/body/note so the unmount flush effect can
    // fire one final save without depending on these values (which would
    // re-arm the unmount cleanup on every keystroke and defeat the debounce).
    const titleRef = useRef(title);
    const bodyRef = useRef(body);
    const noteRef = useRef(note);
    titleRef.current = title;
    bodyRef.current = body;
    noteRef.current = note;

    const buildHtml = (txt) =>
      '<p>' + (txt || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>') + '</p>';

    // Debounced title save. Cleanup ONLY clears the timer — no flush — so
    // every keystroke just resets the 600ms window like a real debounce.
    useEffect(() => {
      if (!note) return;
      if (title === (note.title || '')) return;
      setSaving(true);
      clearTimeout(titleTimer.current);
      titleTimer.current = setTimeout(async () => {
        try {
          const updated = await window.updateNote(note.id, { title: title || 'Untitled note' });
          if (updated && updated.updated_at) setSavedAt(updated.updated_at);
        } catch (e) {
          window._sbToast && window._sbToast('save failed: ' + (e.message || e));
        } finally {
          setSaving(false);
        }
      }, 600);
      return () => clearTimeout(titleTimer.current);
    }, [title, note]);

    // Debounced body save. Same shape as the title debounce.
    useEffect(() => {
      if (!note) return;
      if (body === (note.body_text || '')) return;
      setSaving(true);
      clearTimeout(bodyTimer.current);
      bodyTimer.current = setTimeout(async () => {
        try {
          const updated = await window.updateNote(note.id, { body_text: body, body_html: buildHtml(body) });
          if (updated && updated.updated_at) setSavedAt(updated.updated_at);
        } catch (e) {
          window._sbToast && window._sbToast('save failed: ' + (e.message || e));
        } finally {
          setSaving(false);
        }
      }, 600);
      return () => clearTimeout(bodyTimer.current);
    }, [body, note]);

    // Unmount-only flush — fires once when the component is actually
    // tearing down, not on every keystroke. Reads latest values from refs.
    // Empty dep array → effect runs once on mount, cleanup runs once on
    // unmount. Fire-and-forget; failures are no worse than today's
    // findNote() returning a slightly older row.
    useEffect(() => {
      return () => {
        const cur = noteRef.current;
        if (!cur || !cur.id) return;
        const t = titleRef.current;
        const b = bodyRef.current;
        if (t !== (cur.title || '')) {
          window.updateNote(cur.id, { title: t || 'Untitled note' }).catch(() => {});
        }
        if (b !== (cur.body_text || '')) {
          window.updateNote(cur.id, { body_text: b, body_html: buildHtml(b) }).catch(() => {});
        }
      };
    }, []);

    const crumbs = subject ? [subject.name, title || 'Untitled note'] : [title || 'Untitled note'];
    const wordCount = Math.max(0, (body || '').split(/\s+/).filter(Boolean).length);
    const savedDisplay = savedAt ? relTime(savedAt) : '';

    if (!note) {
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={['Note']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              Loading…
            </div>
          </div>
        </Screen>
      );
    }

    return (
      <Screen>
        <Topbar
          leading={<BackBtn />}
          crumbs={crumbs}
          actions={
            <>
              <span className="sb-saved">{saving ? '↻ Saving…' : '↻ Saved'}</span>
              <Kebab onClick={() => window._sbToast && window._sbToast('note options coming soon')} />
            </>
          }
        />
        <div className="sb-editor">
          <div className="sb-editor-meta">
            {subject && <><span><SubjectIcon subject={subject} /> {subject.name}</span><span className="sb-dot-sep">·</span></>}
            <span>edited {savedDisplay || 'just now'}</span>
            <span className="sb-dot-sep">·</span>
            <span>{wordCount} {wordCount === 1 ? 'word' : 'words'}</span>
          </div>
          <input className="sb-editor-title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Untitled note" />
          <textarea
            className="sb-editor-body"
            value={body}
            onChange={(e) => setBody(e.target.value)}
            placeholder="Start writing…"
            style={{
              width: '100%',
              minHeight: 360,
              border: 'none',
              outline: 'none',
              resize: 'vertical',
              fontFamily: 'inherit',
              fontSize: 15,
              lineHeight: 1.55,
              color: 'var(--ink-strong)',
              background: 'transparent',
              padding: '8px 0',
            }}
          />
        </div>
        <div className="sb-editor-toolbar">
          <button className="sb-tt-btn" onClick={() => window._sbToast && window._sbToast('rich-text formatting on mobile coming soon')}><strong>B</strong></button>
          <button className="sb-tt-btn" onClick={() => window._sbToast && window._sbToast('rich-text formatting on mobile coming soon')}><em>I</em></button>
          <button className="sb-tt-btn" onClick={() => window._sbToast && window._sbToast('rich-text formatting on mobile coming soon')}>U</button>
          <span className="sb-tt-divider"></span>
          <button className="sb-tt-btn" onClick={() => window._sbToast && window._sbToast('AI assist on mobile coming soon')}><Sparkle /></button>
        </div>
      </Screen>
    );
  }

  function CalendarScreen({ openDrawer, openNewEvent }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const events = window.useEvents ? window.useEvents() : MOCK_EVENTS;
    const upcoming = useMemo(() => {
      return events
        .map((e) => ({
          id: e.id,
          subjectId: e.subject_id,
          title: e.title,
          kind: e.kind,
          date: e.date,
          notes: e.notes || '',
          days: daysFromToday(e.date),
        }))
        .filter((e) => e.days >= 0)
        .sort((a, b) => a.days - b.days)
        .slice(0, 6);
    }, [events]);

    const now = new Date();
    const monthLabel = now.toLocaleString(undefined, { month: 'long' });

    return (
      <Screen>
        <Topbar
          leading={<Hamburger onClick={openDrawer} />}
          crumbs={['Calendar']}
          actions={
            <>
              <Kebab onClick={() => window._sbToast && window._sbToast('calendar options coming soon')} />
              <button className="sb-btn sb-sm sb-accent" onClick={() => openNewEvent && openNewEvent()}><PlusIcon /> Event</button>
            </>
          }
        />
        <div className="sb-page-pad">
          <div className="sb-eyebrow">§ Calendar</div>
          <h1 className="sb-page-title"><em>{monthLabel}.</em></h1>
          <div className="sb-page-subtitle"><strong>{events.length} events</strong> across {subjects.length} subjects.</div>
          <div className="sb-legend">
            <span className="sb-leg-item"><span className="sb-dot exam"></span>Exam</span>
            <span className="sb-leg-item"><span className="sb-dot assignment"></span>Assignment</span>
            <span className="sb-leg-item"><span className="sb-dot study"></span>Study</span>
          </div>
          <MiniCal events={events} onPick={(d, m, y, iso) => {
            const evs = eventsForDay(events, d, m, y);
            if (evs.length) nav.go('event', { id: evs[0].id });
            else if (openNewEvent) openNewEvent(iso);
            else window._sbToast && window._sbToast(`${iso} — no events`);
          }} />
          <h3 className="sb-section-h" style={{ marginTop: 28 }}>Upcoming</h3>
          {upcoming.map((e) => {
            const dt = new Date(e.date + 'T00:00:00');
            const day = dt.getDate();
            const mon = dt.toLocaleString(undefined, { month: 'short' }).toUpperCase();
            return (
              <div key={e.id} className="sb-event-row" onClick={() => nav.go('event', { id: e.id })}>
                <div className="sb-er-date"><span className="sb-er-d">{day}</span><span className="sb-er-m">{mon}</span></div>
                <div className="sb-er-body">
                  <div className="sb-er-line1">
                    <span className={'sb-dot ' + e.kind}></span>
                    <span><SubjectIcon id={e.subjectId} subjects={subjects} /> {subjectName(e.subjectId, subjects)}</span>
                  </div>
                  <div className="sb-er-title">{e.title}</div>
                  <div className="sb-er-meta">
                    {e.notes ? e.notes : (e.days === 0 ? 'today' : (e.days === 1 ? 'tomorrow' : `in ${e.days} days`))}
                  </div>
                </div>
              </div>
            );
          })}
          {upcoming.length === 0 && (
            <div style={{ padding: '24px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              No upcoming events.
            </div>
          )}
        </div>
      </Screen>
    );
  }

  function EventDetailScreen({ params }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const events = window.useEvents ? window.useEvents() : MOCK_EVENTS;
    const raw = events.find((e) => e.id === params.id) || null;
    const ev = raw ? {
      id: raw.id,
      subjectId: raw.subject_id,
      title: raw.title,
      kind: raw.kind,
      date: raw.date,
      notes: raw.notes || '',
    } : null;
    const days = ev ? daysFromToday(ev.date) : 0;

    const onDelete = async () => {
      if (!ev) return;
      const ok = typeof window.confirm === 'function' ? window.confirm('Delete this event?') : true;
      if (!ok) return;
      try {
        await window.deleteEvent(ev.id);
        nav.back();
      } catch (e) {
        window._sbToast && window._sbToast('delete failed: ' + (e.message || e));
      }
    };

    if (!ev) {
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={['Calendar', 'Event']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              Event not found.
            </div>
          </div>
        </Screen>
      );
    }

    return (
      <Screen>
        <Topbar leading={<BackBtn />} crumbs={['Calendar', ev.title]} actions={<Kebab onClick={onDelete} />} />
        <div className="sb-page-pad">
          <div className="sb-event-hero">
            <div className="sb-eh-pill"><span className={'sb-dot ' + ev.kind}></span>{ev.kind} · <SubjectIcon id={ev.subjectId} subjects={subjects} /> {subjectName(ev.subjectId, subjects)}</div>
            <h1 className="sb-eh-title">{ev.title}</h1>
            <div className="sb-eh-meta">
              <div className="sb-eh-row"><span className="sb-eh-k">When</span><span className="sb-eh-v">{fmtDateLong(ev.date)}</span></div>
              {ev.notes && <div className="sb-eh-row"><span className="sb-eh-k">Notes</span><span className="sb-eh-v">{ev.notes}</span></div>}
              <div className="sb-eh-row"><span className="sb-eh-k">In</span><span className="sb-eh-v"><strong>{days === 0 ? 'today' : (days < 0 ? `${-days} day${days === -1 ? '' : 's'} ago` : days + (days === 1 ? ' day' : ' days'))}</strong></span></div>
            </div>
          </div>

          <div className="sb-cta-row">
            <button className="sb-btn sb-block sb-danger" onClick={onDelete}>Delete event</button>
            <button className="sb-btn sb-block sb-accent" onClick={() => nav.go('library')}><Sparkle /> Open library</button>
          </div>
        </div>
      </Screen>
    );
  }

  function GuideOverviewScreen({ params }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    // Subscribe to guides so the page re-renders if the cache updates.
    const allGuides = window.useGuides ? window.useGuides() : MOCK_GUIDES;
    const [rawGuide, setRawGuide] = useState(() => findGuideById(params.id));

    // Async fetch fallback for direct-URL access. Tries findGuide once if
    // sync miss.
    useEffect(() => {
      if (rawGuide) return;
      let cancelled = false;
      (async () => {
        try {
          const g = typeof window.findGuide === 'function' ? await window.findGuide(params.id) : null;
          if (!cancelled && g) setRawGuide(g);
        } catch {}
      })();
      return () => { cancelled = true; };
    }, [params.id]);

    // Re-sync from cache when allGuides changes.
    useEffect(() => {
      const fresh = (allGuides || []).find((x) => x.id === params.id);
      if (fresh) setRawGuide(fresh);
    }, [allGuides, params.id]);

    const g = rawGuide ? adaptGuide(rawGuide) : null;
    const content = (rawGuide && rawGuide.content) || {};
    const sources = Array.isArray(rawGuide && rawGuide.sources) ? rawGuide.sources : [];
    const sections = content.sections || [];

    if (!g) {
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={['Guide']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              Loading guide…
            </div>
          </div>
        </Screen>
      );
    }

    return (
      <Screen>
        <Topbar leading={<BackBtn />} crumbs={[subjectName(g.subjectId, subjects), g.title]} actions={<Kebab onClick={() => window._sbToast && window._sbToast('guide options coming soon')} />} />
        <div className="sb-page-pad">
          <div className="sb-eyebrow"><SubjectIcon id={g.subjectId} subjects={subjects} /> {subjectName(g.subjectId, subjects)} · Guide</div>
          <h1 className="sb-page-title"><em>{g.title}.</em></h1>
          <div className="sb-page-subtitle">Generated from <strong>{g.sources} sources</strong> · {g.cards} cards · {g.when || 'just now'}.</div>

          {content.bigIdea && (
            <div className="sb-upnext" style={{ cursor: 'default' }}>
              <div className="sb-up-label">Big idea</div>
              <div className="sb-up-title" style={{ fontSize: 16, lineHeight: 1.5 }}>{content.bigIdea}</div>
            </div>
          )}

          <div className="sb-stat-strip">
            <div className="sb-stat"><span className="sb-stat-num">{g.cards}</span><span className="sb-stat-lbl">cards</span></div>
            <div className="sb-stat"><span className="sb-stat-num">{g.sources}</span><span className="sb-stat-lbl">sources</span></div>
            <div className="sb-stat"><span className="sb-stat-num">{g.quizCount}</span><span className="sb-stat-lbl">quiz</span></div>
            <div className="sb-stat"><span className="sb-stat-num">{sections.length}</span><span className="sb-stat-lbl">sections</span></div>
          </div>

          {sources.length > 0 && (
            <>
              <h3 className="sb-section-h">Sources</h3>
              {sources.map((s, i) => {
                const k = uiSourceKind(s);
                return (
                  <div key={i} className="sb-src-row" onClick={() => nav.go('source', { id: g.id, idx: i })}>
                    <span className={'sb-src-icon ' + k}>{k === 'pdf' ? 'PDF' : k === 'img' ? 'IMG' : k === 'txt' ? 'TXT' : 'DOC'}</span>
                    <div className="sb-src-body">
                      <div className="sb-src-title">{s.name}</div>
                      <div className="sb-src-meta">{s.kind === 'image' ? 'image' : 'document'}</div>
                    </div>
                  </div>
                );
              })}
            </>
          )}

          {sections.length > 0 && (
            <>
              <h3 className="sb-section-h">Outline</h3>
              <ol className="sb-outline">
                {sections.map((sec, i) => (
                  <li key={i}>
                    <span className="sb-ol-num">{String(sec.number || i + 1).padStart(2, '0')}</span>
                    <span className="sb-ol-t">{sec.title}</span>
                    <span className="sb-ol-c">{(sec.terms || []).length} terms</span>
                  </li>
                ))}
              </ol>
            </>
          )}

          <div className="sb-cta-row">
            <button className="sb-btn sb-block sb-ghost" onClick={() => nav.go('quiz', { id: g.id })} disabled={g.quizCount === 0}>Take quiz</button>
            <button className="sb-btn sb-block sb-accent" onClick={() => nav.go('flashcards', { id: g.id })} disabled={g.cards === 0}>Study cards →</button>
          </div>
        </div>
      </Screen>
    );
  }

  function SourceViewerScreen({ params }) {
    const nav = useNav();
    // Subscribe to guides so the page re-renders if the cache loads later.
    const guides = window.useGuides ? window.useGuides() : [];
    const [rawGuide, setRawGuide] = useState(() => findGuideById(params.id));
    const [fetching, setFetching] = useState(() => !findGuideById(params.id));

    // Async fetch fallback for direct-URL access.
    useEffect(() => {
      if (rawGuide) return;
      let cancelled = false;
      if (typeof window.findGuide === 'function') {
        window.findGuide(params.id)
          .then((row) => { if (!cancelled) { if (row) setRawGuide(row); setFetching(false); } })
          .catch(() => { if (!cancelled) setFetching(false); });
      } else {
        setFetching(false);
      }
      return () => { cancelled = true; };
    }, [params.id]);

    // Resync if the cache loads later.
    useEffect(() => {
      const hit = findGuideById(params.id);
      if (hit && hit !== rawGuide) setRawGuide(hit);
    }, [guides.length, params.id]);

    const sources = (rawGuide && Array.isArray(rawGuide.sources)) ? rawGuide.sources : [];
    const idx = Math.min(params.idx || 0, Math.max(0, sources.length - 1));
    const src = sources[idx] || null;
    const g = rawGuide ? adaptGuide(rawGuide) : null;

    // Source bytes aren't stored on guides — they were used at generation time
    // and discarded. Show metadata instead. The original file lives in the
    // user's documents library if they uploaded it there separately.

    if (!rawGuide) {
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={['Source']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              {fetching ? 'Loading guide…' : 'Guide not found.'}
            </div>
          </div>
        </Screen>
      );
    }
    if (!src) {
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={['Source']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              Source not found.
            </div>
          </div>
        </Screen>
      );
    }

    const k = uiSourceKind(src);
    return (
      <Screen>
        <Topbar leading={<BackBtn />} crumbs={[g ? g.title : 'Guide', src.name]} actions={<Kebab onClick={() => window._sbToast && window._sbToast('source options coming soon')} />} />
        <div className="sb-source-toolbar">
          <span className="sb-src-pill">{k.toUpperCase()} · source</span>
          <div style={{ flex: 1 }}></div>
          <button className="sb-tt-btn" onClick={() => window._sbToast && window._sbToast('star coming soon')}>★</button>
          <button className="sb-tt-btn" onClick={() => window._sbToast && window._sbToast('download coming soon')}>⤓</button>
        </div>
        <div className="sb-page-pad">
          <div className="sb-eyebrow">§ Source</div>
          <h1 className="sb-page-title" style={{ wordBreak: 'break-word' }}><em>{src.name}</em></h1>
          <div className="sb-page-subtitle">
            Source {idx + 1} of {sources.length}
            <span className="sb-dot-sep">·</span>
            {src.kind === 'image' ? 'image input' : 'document input'}
          </div>
          <div style={{
            background: 'var(--page)',
            border: '1px solid var(--hairline)',
            borderRadius: 'var(--r-md)',
            padding: 18,
            marginTop: 16,
            fontSize: 13,
            color: 'var(--ink-mute)',
            lineHeight: 1.55,
          }}>
            The original file isn't stored on the guide — only its name, kind, and the extracted text used for generation are kept. To view the file again, open it from your subject's Files panel on the desktop app.
          </div>
          <div className="sb-source-bottom">
            <button className="sb-btn sb-block sb-ghost" onClick={() => nav.back()}>Back to guide</button>
          </div>
        </div>
      </Screen>
    );
  }

  function FlashcardScreen({ params }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    // Subscribe to guides cache so re-render fires when it loads later.
    const guides = window.useGuides ? window.useGuides() : [];
    const [rawGuide, setRawGuide] = useState(() => findGuideById(params.id));
    const [fetching, setFetching] = useState(() => !findGuideById(params.id));

    // Async fallback for cold-load on this route.
    useEffect(() => {
      if (rawGuide) return;
      let cancelled = false;
      if (typeof window.findGuide === 'function') {
        window.findGuide(params.id)
          .then((row) => { if (!cancelled) { if (row) setRawGuide(row); setFetching(false); } })
          .catch(() => { if (!cancelled) setFetching(false); });
      } else {
        setFetching(false);
      }
      return () => { cancelled = true; };
    }, [params.id]);

    // Resync if the cache loads later.
    useEffect(() => {
      const hit = findGuideById(params.id);
      if (hit && hit !== rawGuide) setRawGuide(hit);
    }, [guides.length, params.id]);

    const g = rawGuide ? adaptGuide(rawGuide) : null;
    const flashcards = (rawGuide && rawGuide.content && rawGuide.content.flashcards) || [];
    // Subscribe to per-card review rows for this guide.
    const reviewsMap = window.useReviewsForGuide ? window.useReviewsForGuide(params.id) : new Map();

    // Build the today-due queue. Memoized on [flashcards, reviewsMap] so it
    // rebuilds when reviewsMap arrives async (initial mount sees an empty
    // map and would otherwise treat every card as new). When the user rates
    // "again" we push the card to the end of `extra` so it re-appears later
    // this session — that lives separately from the memoized base.
    const baseQueue = useMemo(() => {
      if (!flashcards.length) return [];
      if (typeof window.buildDueQueue === 'function') {
        return window.buildDueQueue(flashcards, reviewsMap, { newCap: 20 });
      }
      return flashcards.map((card, index) => ({ index, card, review: null }));
    }, [flashcards, reviewsMap]);
    const [extra, setExtra] = useState([]); // re-queued "again" cards
    const queue = useMemo(() => baseQueue.concat(extra), [baseQueue, extra]);

    const [idx, setIdx] = useState(0);
    const [flipped, setFlipped] = useState(false);
    const [reviewed, setReviewed] = useState(0);

    // Reset session position only when the route changes. The reviewsMap
    // updates every time a card is rated (upsertReview grows the map);
    // depending on it would yank the user back to position 0 mid-session.
    // baseQueue is its own useMemo keyed on [flashcards, reviewsMap], so the
    // queue itself still rebuilds correctly when reviews land async.
    useEffect(() => {
      setIdx(0);
      setFlipped(false);
      setExtra([]);
    }, [params.id]);

    const cur = queue[idx];
    const card = cur ? cur.card : null;

    // Live previews for the rate buttons.
    const previews = useMemo(() => {
      if (!cur || !window.FSRS || !window.FSRS.previewIntervals) return { 1: '', 2: '', 3: '', 4: '' };
      try { return window.FSRS.previewIntervals(cur.review || null); }
      catch { return { 1: '', 2: '', 3: '', 4: '' }; }
    }, [cur]);

    if (!rawGuide) {
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={['Cards']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              {fetching ? 'Loading guide…' : 'Guide not found.'}
            </div>
          </div>
        </Screen>
      );
    }

    if (!flashcards.length) {
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={[g.title, 'Cards']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              This guide has no flashcards yet.
            </div>
          </div>
        </Screen>
      );
    }

    if (!cur) {
      // Queue empty — done for today.
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={[g.title, 'Cards']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 24px', textAlign: 'center' }}>
              <div className="sb-eyebrow">§ Done</div>
              <h2 style={{ fontFamily: 'var(--serif)', fontSize: 26, fontWeight: 500, margin: '14px 0 6px', color: 'var(--ink-strong)' }}>You're done for now.</h2>
              <p style={{ fontSize: 14, color: 'var(--ink-mute)' }}>{reviewed} card{reviewed === 1 ? '' : 's'} reviewed this session. Come back when more cards are due.</p>
              <div className="sb-cta-row" style={{ marginTop: 20 }}>
                <button className="sb-btn sb-block sb-accent" onClick={() => nav.go('guide', { id: g.id })}>Back to guide</button>
              </div>
            </div>
          </div>
        </Screen>
      );
    }

    const rate = async (rating) => {
      const RATING_MAP = { again: 1, hard: 2, good: 3, easy: 4 };
      const r = RATING_MAP[rating] || 3;
      try {
        const prev = cur.review || null;
        const next = window.FSRS && window.FSRS.schedule ? window.FSRS.schedule(prev, r) : null;
        if (next && typeof window.upsertReview === 'function') {
          await window.upsertReview(g.id, cur.index, next);
        }
      } catch (e) {
        window._sbToast && window._sbToast('save failed: ' + (e.message || e));
      }
      setReviewed((x) => x + 1);
      setFlipped(false);
      // If user rated "again", push card back to end of session so it cycles.
      if (rating === 'again') {
        setExtra((e) => [...e, cur]);
      }
      // Always advance idx — the re-queued card sits at queue.length-1 and
      // we'll get to it via the natural progression. (Old behavior tried to
      // splice in place, but that desyncs against the memoized base queue.)
      setIdx((i) => i + 1);
    };

    return (
      <Screen>
        <Topbar leading={<BackBtn />} crumbs={[g.title, 'Cards']} actions={<Kebab onClick={() => window._sbToast && window._sbToast('card options coming soon')} />} />
        <div className="sb-fc-wrap">
          <div className="sb-fc-progress">
            <div className="sb-fc-pg-row">
              <span>Card {idx + 1} of {queue.length}</span>
              <span><SubjectIcon id={g.subjectId} subjects={subjects} /> {subjectName(g.subjectId, subjects)}</span>
            </div>
            <div className="sb-fc-pg-bar"><div className="sb-fc-pg-fill" style={{ width: `${((idx + 1) / queue.length) * 100}%` }}></div></div>
          </div>

          <div className={'sb-flashcard' + (flipped ? ' flipped' : '')} onClick={() => setFlipped((f) => !f)}>
            <div className="sb-fc-eyebrow">{flipped ? 'ANSWER' : 'QUESTION'}</div>
            <div className="sb-fc-q">{flipped ? card.back : card.front}</div>
            <div className="sb-fc-tap-hint">{flipped ? 'tap to flip back' : 'tap to reveal answer'}</div>
            <div className="sb-fc-corner">{idx + 1} / {queue.length}</div>
          </div>

          {flipped && (
            <div className="sb-fc-rate">
              <div className="sb-fc-rate-label">How well did you know it?</div>
              <div className="sb-fc-rate-row">
                <button className="sb-rate-btn again" onClick={() => rate('again')}><span className="sb-rate-glyph">✕</span><span>Again</span><span className="sb-rate-time">{previews[1] || '<5m'}</span></button>
                <button className="sb-rate-btn hard"  onClick={() => rate('hard')}><span className="sb-rate-glyph">~</span><span>Hard</span><span className="sb-rate-time">{previews[2] || ''}</span></button>
                <button className="sb-rate-btn good"  onClick={() => rate('good')}><span className="sb-rate-glyph">✓</span><span>Good</span><span className="sb-rate-time">{previews[3] || ''}</span></button>
                <button className="sb-rate-btn easy"  onClick={() => rate('easy')}><span className="sb-rate-glyph">★</span><span>Easy</span><span className="sb-rate-time">{previews[4] || ''}</span></button>
              </div>
            </div>
          )}

          <div className="sb-fc-stats">
            <span>Session: <strong>{reviewed} reviewed</strong></span>
            <span className="sb-dot-sep">·</span>
            <span>Queue: <strong>{queue.length} cards</strong></span>
          </div>
        </div>
      </Screen>
    );
  }

  function QuizScreen({ params }) {
    const nav = useNav();
    // Subscribe to guides cache so re-render fires when it loads later.
    const guides = window.useGuides ? window.useGuides() : [];
    const [rawGuide, setRawGuide] = useState(() => findGuideById(params.id));
    const [fetching, setFetching] = useState(() => !findGuideById(params.id));

    // Async fallback for cold-load on this route.
    useEffect(() => {
      if (rawGuide) return;
      let cancelled = false;
      if (typeof window.findGuide === 'function') {
        window.findGuide(params.id)
          .then((row) => { if (!cancelled) { if (row) setRawGuide(row); setFetching(false); } })
          .catch(() => { if (!cancelled) setFetching(false); });
      } else {
        setFetching(false);
      }
      return () => { cancelled = true; };
    }, [params.id]);

    // Resync if the cache loads later.
    useEffect(() => {
      const hit = findGuideById(params.id);
      if (hit && hit !== rawGuide) setRawGuide(hit);
    }, [guides.length, params.id]);

    const g = rawGuide ? adaptGuide(rawGuide) : null;
    // Adapt schema { question, options, correctIndex, hint } to mobile UI shape.
    const qs = useMemo(() => {
      const raw = (rawGuide && rawGuide.content && rawGuide.content.quiz) || [];
      return raw.map((q) => ({
        q: q.question,
        opts: q.options || [],
        correct: q.correctIndex,
        hint: q.hint || null,
      }));
    }, [rawGuide]);

    const [qIdx,  setQIdx]  = useState(0);
    const [pick,  setPick]  = useState(null);
    const [score, setScore] = useState(0);
    const [missed, setMissed] = useState([]);
    const [showHint, setShowHint] = useState(false);

    // Live timer
    const [elapsed, setElapsed] = useState(0);
    useEffect(() => {
      const t = setInterval(() => setElapsed((e) => e + 1), 1000);
      return () => clearInterval(t);
    }, []);
    const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
    const ss = String(elapsed % 60).padStart(2, '0');

    if (!rawGuide) {
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={['Quiz']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              {fetching ? 'Loading guide…' : 'Guide not found.'}
            </div>
          </div>
        </Screen>
      );
    }

    if (qs.length === 0) {
      return (
        <Screen>
          <Topbar leading={<BackBtn />} crumbs={[g.title, 'Practice quiz']} actions={null} />
          <div className="sb-page-pad">
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'var(--ink-mute)', fontSize: 13 }}>
              This guide has no quiz questions yet.
            </div>
          </div>
        </Screen>
      );
    }

    const submit = () => {
      if (pick == null) return;
      const correct = pick === qs[qIdx].correct;
      const newScore  = score + (correct ? 1 : 0);
      const newMissed = correct ? missed : [...missed, { i: qIdx, q: qs[qIdx], picked: pick }];
      if (qIdx + 1 < qs.length) {
        setScore(newScore); setMissed(newMissed);
        setQIdx(qIdx + 1); setPick(null); setShowHint(false);
      } else {
        nav.replace('results', { id: g.id, score: newScore, total: qs.length, time: elapsed, missed: newMissed });
      }
    };
    const skip = () => {
      const newMissed = [...missed, { i: qIdx, q: qs[qIdx], picked: -1, skipped: true }];
      if (qIdx + 1 < qs.length) { setMissed(newMissed); setQIdx(qIdx + 1); setPick(null); setShowHint(false); }
      else nav.replace('results', { id: g.id, score, total: qs.length, time: elapsed, missed: newMissed });
    };

    const q = qs[qIdx];
    const pct = ((qIdx + 1) / qs.length) * 100;

    return (
      <Screen>
        <Topbar leading={<BackBtn />} crumbs={[g.title, 'Practice quiz']} actions={<span className="sb-timer">{mm}:{ss}</span>} />
        <div className="sb-page-pad">
          <div className="sb-quiz-progress">
            <div className="sb-fc-pg-row"><span>Question {qIdx + 1} of {qs.length}</span><span>multiple choice</span></div>
            <div className="sb-fc-pg-bar"><div className="sb-fc-pg-fill" style={{ width: `${pct}%` }}></div></div>
          </div>
          <div className="sb-quiz-q-num">{String(qIdx + 1).padStart(2, '0')}.</div>
          <div className="sb-quiz-q">{q.q}</div>
          <div className="sb-quiz-options">
            {q.opts.map((o, i) => (
              <label key={i} className={'sb-quiz-opt' + (pick === i ? ' selected' : '')} onClick={() => setPick(i)}>
                <span className="sb-opt-letter">{'ABCDEFGH'[i]}</span>
                <span className="sb-opt-text">{o}</span>
                {pick === i && <span className="sb-opt-check">✓</span>}
              </label>
            ))}
          </div>
          <div className="sb-quiz-foot">
            <button className="sb-btn sb-ghost" onClick={skip}>Skip</button>
            <button className="sb-btn sb-accent" style={{ flex: 1 }} onClick={submit} disabled={pick == null}>Submit answer →</button>
          </div>
          {q.hint && (
            <div className="sb-quiz-hint" onClick={() => setShowHint((s) => !s)}>
              <Sparkle /> <span>{showHint ? q.hint : 'Tap for a hint.'}</span>
            </div>
          )}
        </div>
      </Screen>
    );
  }

  function ResultsScreen({ params }) {
    const nav = useNav();
    const rawGuide = findGuideById(params.id);
    const g = rawGuide ? adaptGuide(rawGuide) : { title: 'Guide', id: params.id };
    const score = params.score ?? 0;
    const total = params.total ?? 0;
    const time  = params.time ?? 0;
    const missed = params.missed || [];
    const pct = total > 0 ? Math.round((score / total) * 100) : 0;
    const mm = String(Math.floor(time / 60));
    const ss = String(time % 60).padStart(2, '0');

    const blurb = pct >= 90 ? 'crushing it.' : pct >= 75 ? 'solid work.' : pct >= 50 ? 'getting there.' : 'rough one — try again.';

    return (
      <Screen>
        <Topbar leading={<BackBtn />} crumbs={[g.title, 'Results']} actions={<Kebab onClick={() => window._sbToast && window._sbToast('share results coming soon')} />} />
        <div className="sb-page-pad">
          <div className="sb-results-hero">
            <div className="sb-rh-eyebrow">QUIZ COMPLETE</div>
            <div className="sb-rh-score"><span className="sb-rh-num">{score}</span><span className="sb-rh-of">/ {total}</span></div>
            <div className="sb-rh-pct">{pct}% — {blurb}</div>
          </div>

          <div className="sb-result-stats">
            <div className="sb-rs-cell"><span className="sb-rs-num good">{score}</span><span className="sb-rs-lbl">correct</span></div>
            <div className="sb-rs-cell"><span className="sb-rs-num bad">{total - score}</span><span className="sb-rs-lbl">missed</span></div>
            <div className="sb-rs-cell"><span className="sb-rs-num">{mm}:{ss}</span><span className="sb-rs-lbl">time</span></div>
          </div>

          {missed.length > 0 && (
            <>
              <h3 className="sb-section-h">Missed questions</h3>
              {missed.map((m, i) => (
                <div key={i} className="sb-missed">
                  <div className="sb-missed-num">Q{m.i + 1}</div>
                  <div className="sb-missed-q">{m.q.q}</div>
                  <div className="sb-missed-meta">
                    Your answer · {m.skipped ? 'skipped' : (m.q.opts[m.picked] || '—')}
                    <span className="sb-dot-sep">·</span>
                    Correct · {m.q.opts[m.q.correct] || '—'}
                  </div>
                </div>
              ))}
            </>
          )}

          <div className="sb-cta-row">
            <button className="sb-btn sb-block sb-ghost" onClick={() => nav.go('guide', { id: g.id })}>Done</button>
            <button className="sb-btn sb-block sb-accent" onClick={() => nav.replace('quiz', { id: g.id })}>Retry quiz</button>
          </div>
        </div>
      </Screen>
    );
  }

  function UploadScreen({ params }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const fileInput = useRef(null);
    // Each entry holds the real File so it can be parsed in GenerationScreen.
    const [files, setFiles] = useState([]); // [{ file: File, kind: 'pdf'|'img'|'doc'|'txt', meta }]
    const [subjectId, setSubjectId] = useState(params.subjectId || (subjects[0] && subjects[0].id) || '');
    const [title, setTitle] = useState(params.title || '');

    const remove = (i) => setFiles(files.filter((_, j) => j !== i));
    const onPickFiles = (e) => {
      const list = Array.from(e.target.files || []);
      const newOnes = list.map((f) => {
        const ext = (f.name.split('.').pop() || '').toLowerCase();
        const kind = ext === 'pdf' ? 'pdf'
          : (['png','jpg','jpeg','webp','gif'].includes(ext) ? 'img'
          : (['txt','md','markdown'].includes(ext) ? 'txt' : 'doc'));
        return { file: f, name: f.name, kind, meta: `${(f.size/1024/1024).toFixed(2)} MB · ready`, status: 'ready' };
      });
      setFiles((cur) => [...cur, ...newOnes]);
      // Reset the input so re-selecting the same file fires onChange again.
      try { e.target.value = ''; } catch {}
    };
    const continueGen = () => {
      if (files.length === 0) return;
      nav.go('generate', {
        subjectId: subjectId || null,
        title,
        files, // pass real Files through nav params (in-memory only)
      });
    };

    return (
      <Screen>
        <Topbar leading={<CloseBtn />} crumbs={['New guide']} actions={<span className="sb-step-pill">step 1 / 2</span>} />
        <div className="sb-page-pad">
          <div className="sb-eyebrow">§ Create a guide</div>
          <h1 className="sb-page-title">What are we<br /><em>studying</em>?</h1>
          <div className="sb-page-subtitle">Upload PDFs, DOCX, images, or text files. We'll read them and build the guide.</div>

          {!params.title && (
            <>
              <label className="sb-field-label">Guide title (optional)</label>
              <input
                className="sb-input-mb"
                placeholder="e.g. Photosynthesis"
                value={title}
                onChange={(e) => setTitle(e.target.value)}
              />
            </>
          )}
          {subjects.length > 0 && (
            <>
              <label className="sb-field-label">Subject</label>
              <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 14 }}>
                <span className={'sb-chip' + (subjectId === '' ? ' active' : '')} onClick={() => setSubjectId('')}>none</span>
                {subjects.map((s) => (
                  <span key={s.id} className={'sb-chip' + (s.id === subjectId ? ' active' : '')} onClick={() => setSubjectId(s.id)}>
                    <SubjectIcon subject={s} /> {s.name}
                  </span>
                ))}
              </div>
            </>
          )}

          <div className="sb-dropzone" onClick={() => fileInput.current && fileInput.current.click()}>
            <div className="sb-dz-icon">↑</div>
            <div className="sb-dz-title">Tap to browse files</div>
            <div className="sb-dz-sub">PDF, DOCX, TXT, MD, or images</div>
            <input
              ref={fileInput}
              type="file"
              multiple
              accept=".pdf,.docx,.txt,.md,image/*"
              onChange={onPickFiles}
              style={{
                position: 'absolute', opacity: 0, pointerEvents: 'none',
                width: 1, height: 1, overflow: 'hidden',
              }}
            />
          </div>

          <h3 className="sb-section-h">Added sources <span className="sb-section-c">{files.length}</span></h3>
          {files.map((f, i) => (
            <div key={i} className="sb-up-row">
              <span className={'sb-src-icon ' + f.kind}>{f.kind === 'pdf' ? 'PDF' : f.kind === 'img' ? 'IMG' : f.kind === 'txt' ? 'TXT' : 'DOC'}</span>
              <div className="sb-up-body">
                <div className="sb-up-name">{f.name}</div>
                <div className="sb-up-meta">{f.meta}</div>
              </div>
              <button className="sb-icon-btn" onClick={() => remove(i)}>✕</button>
            </div>
          ))}

          <div className="sb-cta-row">
            <button className="sb-btn sb-block sb-ghost" onClick={() => nav.back()}>Cancel</button>
            <button className="sb-btn sb-block sb-accent" onClick={continueGen} disabled={files.length === 0}>Continue →</button>
          </div>
        </div>
      </Screen>
    );
  }

  function GenerationScreen({ params }) {
    const nav = useNav();
    const subjects = window.useSubjects ? window.useSubjects() : MOCK_SUBJECTS;
    const filesIn = Array.isArray(params.files) ? params.files : [];

    // 'parsing' → 'generating' → 'saving' → 'done' / 'error' / 'no-files'
    const [phase, setPhase] = useState(filesIn.length === 0 ? 'no-files' : 'parsing');
    const [pct, setPct] = useState(filesIn.length === 0 ? 0 : 6);
    const [error, setError] = useState(null);
    // Lock pipeline so React strict-mode / re-renders can't fire it twice.
    const fired = useRef(false);

    const subjectNameForGen = useMemo(() => {
      const s = (subjects || []).find((x) => x.id === params.subjectId);
      return s ? s.name : null;
    }, [subjects, params.subjectId]);

    useEffect(() => {
      if (fired.current) return;
      if (filesIn.length === 0) return;
      fired.current = true;
      let cancelled = false;
      (async () => {
        try {
          // Step 1: parse files. parseFile returns { name, kind, text? | image? }.
          // Each file gets its own try/catch so one bad file (e.g. a PPTX, a
          // password-protected PDF) doesn't kill the whole pipeline. We collect
          // per-file failures and show them inline if everything failed.
          setPhase('parsing'); setPct(10);
          const parsed = [];
          const failed = [];
          for (let i = 0; i < filesIn.length; i++) {
            const item = filesIn[i];
            try {
              const p = await window.parseFile(item.file);
              if (cancelled) return;
              parsed.push(p);
            } catch (err) {
              if (cancelled) return;
              failed.push({ name: item.file?.name || item.name || `file ${i + 1}`, error: err?.message || String(err) });
            }
            setPct(10 + Math.round(((i + 1) / filesIn.length) * 25));
          }
          if (parsed.length === 0) {
            setError(`Couldn't read any of the ${filesIn.length} file${filesIn.length === 1 ? '' : 's'}:\n` + failed.map((f) => `• ${f.name}: ${f.error}`).join('\n'));
            setPhase('error');
            return;
          }
          if (failed.length > 0) {
            console.warn(`mobile generation: skipping ${failed.length} unreadable file(s):`, failed);
          }

          // Step 2: call OpenAI via the worker.
          setPhase('generating'); setPct(45);
          const result = await window.generateStudyGuide(parsed, {
            subjectName: subjectNameForGen || params.title || 'this subject',
            notes: '',
            mode: 'full',
          });
          if (cancelled) return;

          // Step 3: insert the guide row.
          setPhase('saving'); setPct(85);
          const newGuide = await window.addGuide({
            subjectId: params.subjectId || null,
            title: params.title || result.title || 'Untitled guide',
            sources: parsed.map((p) => ({ name: p.name, kind: p.kind })),
            content: result,
          });
          if (cancelled) return;

          // Done — route to the new guide.
          setPct(100);
          setPhase('done');
          setTimeout(() => {
            if (!cancelled) nav.replace('guide', { id: newGuide.id });
          }, 500);
        } catch (e) {
          if (cancelled) return;
          console.error('mobile generation failed', e);
          setError(e.message || String(e));
          setPhase('error');
        }
      })();
      return () => { cancelled = true; };
    }, []);

    const phaseDef = [
      { key: 'parsing',    name: 'Reading sources',          meta: `${filesIn.length} file${filesIn.length === 1 ? '' : 's'}` },
      { key: 'generating', name: 'Generating with gpt-4o-mini', meta: 'sections, terms, cards' },
      { key: 'saving',     name: 'Saving guide',             meta: 'persisting to your library' },
      { key: 'done',       name: 'Done',                     meta: 'opening' },
    ];
    const phaseIdx = phase === 'no-files' ? -1
      : phase === 'error' ? phaseDef.findIndex((p) => p.key === 'parsing')
      : phaseDef.findIndex((p) => p.key === phase);

    const onRetry = () => {
      // Send the user back to upload with their staged params so they can
      // either retry or pick different files. Files are already in memory.
      nav.replace('upload', { subjectId: params.subjectId, title: params.title });
    };

    return (
      <Screen>
        <Topbar leading={<CloseBtn />} crumbs={['New guide']} actions={<span className="sb-step-pill">step 2 / 2</span>} />
        <div className="sb-page-pad" style={{ position: 'relative' }}>
          <Doodles />
          <div className="sb-pp-rel">
            <div className="sb-eyebrow">§ Generating your guide</div>
            <h1 className="sb-page-title">
              {phase === 'error' ? <em>Generation failed.</em> :
               phase === 'no-files' ? <em>No files staged.</em> :
               phase === 'done' ? <em>Done.</em> :
               <em>Reading.</em>}
            </h1>
            <div className="sb-page-subtitle">
              {phase === 'no-files'
                ? 'Looks like you reloaded mid-flow. Re-upload to try again.'
                : phase === 'error'
                ? 'Something went wrong. Try again, or reduce file count.'
                : 'Hold tight — usually takes 5–20 seconds.'}
            </div>

            {phase !== 'no-files' && phase !== 'error' && (
              <div className="sb-gen-card">
                <div className="sb-gen-spin"><div className="sb-gen-orb"></div></div>
                <div className="sb-gen-status">{phase === 'parsing' ? 'Reading sources' : phase === 'generating' ? 'Generating' : phase === 'saving' ? 'Saving' : 'Done'}…</div>
                <div className="sb-gen-bar"><div className="sb-gen-bar-fill" style={{ width: `${pct}%` }}></div></div>
                <div className="sb-gen-pct">{Math.floor(pct)}%</div>
              </div>
            )}

            {phase === 'error' && (
              <div style={{
                background: 'var(--brick-tint, rgba(220, 60, 60, 0.06))',
                border: '1px solid var(--brick, #c44)',
                borderRadius: 'var(--r-md)',
                padding: 14,
                marginBottom: 18,
                fontSize: 12.5,
                color: 'var(--brick, #c44)',
                fontFamily: 'var(--mono)',
                whiteSpace: 'pre-wrap',
                wordBreak: 'break-word',
              }}>
                {error}
              </div>
            )}

            {phase !== 'no-files' && phase !== 'error' && (
              <div className="sb-gen-steps">
                {phaseDef.map((p, i) => (
                  <div key={i} className={'sb-gen-step' + (i < phaseIdx ? ' done' : (i === phaseIdx ? ' active' : ''))}>
                    <span className="sb-gs-mark">{i < phaseIdx ? '✓' : (i === phaseIdx ? <span className="sb-spin">◐</span> : '○')}</span>
                    <span className="sb-gs-name">{p.name}</span>
                    <span className="sb-gs-meta">{i < phaseIdx ? 'done' : (i === phaseIdx ? p.meta : 'queued')}</span>
                  </div>
                ))}
              </div>
            )}

            {(phase === 'error' || phase === 'no-files') && (
              <div className="sb-cta-row">
                <button className="sb-btn sb-block sb-ghost" onClick={() => nav.reset ? nav.reset('dashboard') : nav.back()}>Cancel</button>
                <button className="sb-btn sb-block sb-accent" onClick={onRetry}>Try again</button>
              </div>
            )}
          </div>
        </div>
      </Screen>
    );
  }

  function SettingsScreen({ openDrawer }) {
    const auth = window.useAuth ? window.useAuth() : { user: null };
    const profile = (auth && auth.user && window.getProfile) ? window.getProfile(auth.user) : { displayName: '', email: '' };
    const onSignOut = async () => {
      try { await window.signOut(); }
      catch (e) { window._sbToast && window._sbToast('sign-out failed: ' + (e.message || e)); }
    };
    return (
      <Screen>
        <Topbar leading={<Hamburger onClick={openDrawer} />} crumbs={['Settings']} actions={null} />
        <div className="sb-page-pad">
          <div className="sb-eyebrow">§ Settings</div>
          <h1 className="sb-page-title">Settings</h1>
          <div className="sb-page-subtitle">Account + preferences.</div>

          <h3 className="sb-section-h">Account</h3>
          <div className="sb-link-card">
            <div className="sb-lc-head">SIGNED IN</div>
            <div className="sb-lc-title">{profile.displayName || (profile.email || '').split('@')[0] || 'You'}</div>
            <div className="sb-lc-meta">{profile.email || '—'}</div>
            <div className="sb-lc-actions">
              <button className="sb-btn sb-sm sb-ghost" onClick={() => window._sbToast && window._sbToast('edit profile on desktop for now')}>Edit profile</button>
              <button className="sb-btn sb-sm sb-danger"  onClick={onSignOut}>Sign out</button>
            </div>
          </div>

          <h3 className="sb-section-h">Study</h3>
          <div className="sb-plan">
            <div className="sb-plan-row"><span className="sb-plan-task">Review algorithm</span><span className="sb-plan-est">FSRS</span></div>
          </div>

          <h3 className="sb-section-h">App</h3>
          <div className="sb-plan">
            <div className="sb-plan-row"><span className="sb-plan-task">Build</span><span className="sb-plan-est">mobile</span></div>
          </div>
        </div>
      </Screen>
    );
  }

  function OnboardingScreen({ onDone }) {
    return (
      <Screen>
        <div className="sb-onb-skip">
          <span></span>
          <span className="sb-onb-skip-text" onClick={onDone}>skip →</span>
        </div>
        <div className="sb-onb-stage">
          <Doodles />
          <div className="sb-onb-content">
            <div className="sb-onb-mark">T</div>
            <div className="sb-onb-eyebrow">welcome to</div>
            <h1 className="sb-onb-title">Tung.<br /><em>scholar</em></h1>
            <p className="sb-onb-tagline">
              A quiet study companion.<br />
              Upload your sources — we'll build the guide.
            </p>
            <div className="sb-onb-stats">
              <div className="sb-onb-stat"><span>📚</span><span>any subject</span></div>
              <div className="sb-onb-stat"><span>✎</span><span>your notes too</span></div>
              <div className="sb-onb-stat"><span>★</span><span>spaced repetition</span></div>
            </div>
          </div>
          <div className="sb-onb-bottom">
            <div className="sb-onb-dots">
              <span className="sb-onb-dot active"></span>
              <span className="sb-onb-dot"></span>
              <span className="sb-onb-dot"></span>
            </div>
            <button className="sb-btn sb-block sb-accent" style={{ height: 50, fontSize: 15 }} onClick={onDone}>Get started →</button>
            <div className="sb-onb-foot">Already have an account? <span className="sb-onb-foot-link" onClick={onDone}>Sign in</span></div>
          </div>
        </div>
      </Screen>
    );
  }

  // ─── router ────────────────────────────────────────────────────────────

  // Stack-based: history is a list of { key, params }. `back()` pops; `go()`
  // pushes; `replace()` swaps the top. `key` matches the screens map below.

  const SCREENS = {
    dashboard:  DashboardScreen,
    library:    LibraryScreen,
    notes:      NotesIndexScreen,
    folder:     FolderScreen,
    editor:     NoteEditorScreen,
    calendar:   CalendarScreen,
    event:      EventDetailScreen,
    guide:      GuideOverviewScreen,
    source:     SourceViewerScreen,
    flashcards: FlashcardScreen,
    quiz:       QuizScreen,
    results:    ResultsScreen,
    upload:     UploadScreen,
    generate:   GenerationScreen,
    settings:   SettingsScreen,
  };

  function MobileApp() {
    // First-run onboarding flag (sessionStorage so it's per-session for the
    // demo). Skipped when ?mobile=1 is set so desktop testing isn't blocked.
    const [showOnboarding, setShowOnboarding] = useState(() => {
      try {
        const url = new URL(window.location.href);
        if (url.searchParams.get('mobile') === '1') return false;
        return !sessionStorage.getItem('sb-onb-seen');
      } catch { return false; }
    });
    const dismissOnboarding = () => {
      try { sessionStorage.setItem('sb-onb-seen', '1'); } catch {}
      setShowOnboarding(false);
    };

    const [stack, setStack] = useState([{ key: 'dashboard', params: {} }]);
    const cur = stack[stack.length - 1];

    const [drawerOpen, setDrawerOpen] = useState(false);
    // modal: { kind: 'guide' | 'note' | 'event', date?, subjectId? } | null
    const [modal,      setModal]      = useState(null);
    const [toast,      setToast]      = useState(null);

    // Toast helper — exposed globally so deeply nested handlers can call it.
    useEffect(() => {
      window._sbToast = (msg) => {
        setToast(msg);
        clearTimeout(window._sbToastT);
        window._sbToastT = setTimeout(() => setToast(null), 1700);
      };
      return () => { delete window._sbToast; clearTimeout(window._sbToastT); };
    }, []);

    const nav = useMemo(() => ({
      go(key, params = {}) {
        // Translate "folder:bio" shortcut → key=folder, params={subjectId:bio}
        if (key.startsWith('folder:')) {
          const subjectId = key.split(':')[1];
          setStack((s) => [...s, { key: 'folder', params: { subjectId } }]);
          return;
        }
        setStack((s) => [...s, { key, params }]);
      },
      replace(key, params = {}) {
        setStack((s) => [...s.slice(0, -1), { key, params }]);
      },
      back() {
        setStack((s) => s.length > 1 ? s.slice(0, -1) : s);
      },
      reset(key = 'dashboard') {
        setStack([{ key, params: {} }]);
      },
    }), []);

    // (We deliberately don't intercept browser back-button for the mock.
    // Hash routes from the desktop app would interfere; users can use the
    // in-app back chevron. Real backend wiring will revisit this.)

    if (showOnboarding) {
      return (
        <NavCtx.Provider value={nav}>
          <OnboardingScreen onDone={dismissOnboarding} />
        </NavCtx.Provider>
      );
    }

    const ScreenComp = SCREENS[cur.key] || DashboardScreen;
    const drawerActive = cur.key === 'folder' ? 'folder:' + (cur.params.subjectId || '') : cur.key;

    const openNewGuide = (subjectId) => setModal({ kind: 'guide', subjectId: subjectId || null });
    const openNewNote = (subjectId) => setModal({ kind: 'note', subjectId: subjectId || null });
    const openNewEvent = (date) => setModal({ kind: 'event', date: date || null });

    return (
      <NavCtx.Provider value={nav}>
        <ScreenComp
          params={cur.params}
          openDrawer={() => setDrawerOpen(true)}
          openNewGuide={openNewGuide}
          openNewNote={openNewNote}
          openNewEvent={openNewEvent}
        />
        <Drawer
          open={drawerOpen}
          onClose={() => setDrawerOpen(false)}
          active={drawerActive}
          openNewGuide={openNewGuide}
          openNewNote={openNewNote}
          onSelect={(key) => {
            if (key.startsWith('folder:')) nav.go(key);
            else nav.go(key);
          }}
        />
        {modal && modal.kind === 'guide' && <NewGuideModal onClose={() => setModal(null)} defaultSubjectId={modal.subjectId} />}
        {modal && modal.kind === 'note'  && <NewNoteModal  onClose={() => setModal(null)} defaultSubjectId={modal.subjectId} />}
        {modal && modal.kind === 'event' && <NewEventModal onClose={() => setModal(null)} defaultDate={modal.date} />}
        {toast && <div className="sb-toast">{toast}</div>}
      </NavCtx.Provider>
    );
  }

  Object.assign(window, {
    MobileApp,
    isMobileViewport,
  });
})();
