// ============================================================
// The Muslim Arabic Reader — sample reading page (v2)
// ============================================================

const { useState, useEffect, useMemo, useRef, useCallback } = React;

const DIACRITICS = /[\u064B-\u065F\u0670\u0640]/g;
const PUNCT = /[،.؟!:;«»"'.…\u061B\u061F()[\]{}]/g;
const NON_WORD_MARKS = /[﴾﴿۞۩ۣۖۚۗۘۙۛۜ۟۠ۡۢۤۥۦࣰࣱࣲۭۧۨ\u2060*0-9٠-٩۰-۹-]/g;
const DIGITS = /[0-9٠-٩۰-۹]/;
const ARABIC_SCRIPT_RE = /[\u0600-\u06FF]/;
const NON_STANDALONE_KEYS = new Set([
  "ذ",
  "صر",
  "ط",
  "السمو",
  "ت",
  "الر",
  "زقین",
  "زقين",
  "ءیل",
  "ءيل",
  "اسر",
  "الو",
  "رثین",
  "رثين",
  "بو",
  "لدیه",
  "لديه",
  "امو",
  "وكذ",
  "كذ",
  "عمر",
  "ن",
  "ه",
  "صلي الله عليه وسلم",
  "حمين",
]);
const SPLIT_RE = /(\s+|[،.؟!:«»"'.…\u061B\u061F()[\]{}]+)/;
const CONTEXT_WORD_RADIUS = 6;
const ENGLISH_CONTEXT_WORD_RADIUS = 12;
const PROCLITIC_PREFIXES = ["و", "ف", "ب", "ك", "ل", "س"];

function stripDiacritics(s) { return s.replace(DIACRITICS, ""); }
function bareKey(token) {
  return stripDiacritics(token)
    .normalize("NFKC")
    .replace(/[ٱإأآ]/g, "ا")
    .replace(/ی/g, "ي")
    .replace(/ى/g, "ي")
    .replace(PUNCT, "")
    .replace(NON_WORD_MARKS, "");
}
function candidateWordKeys(key) {
  const seen = new Set([key]);
  const queue = [{ key, depth: 0 }];

  for (let index = 0; index < queue.length; index += 1) {
    const current = queue[index];
    if (current.depth >= 3) continue;
    if (current.key.startsWith("ال") && current.key.length > 3) {
      const withoutArticle = current.key.slice(2);
      if (!seen.has(withoutArticle)) {
        seen.add(withoutArticle);
        queue.push({ key: withoutArticle, depth: current.depth + 1 });
      }
    }
    for (const prefix of PROCLITIC_PREFIXES) {
      if (!current.key.startsWith(prefix) || current.key.length <= prefix.length + 1) continue;
      const stripped = current.key.slice(prefix.length);
      if (!seen.has(stripped)) {
        seen.add(stripped);
        queue.push({ key: stripped, depth: current.depth + 1 });
      }
      if (stripped.startsWith("ال") && stripped.length > 3) {
        const withoutArticle = stripped.slice(2);
        if (!seen.has(withoutArticle)) {
          seen.add(withoutArticle);
          queue.push({ key: withoutArticle, depth: current.depth + 1 });
        }
      }
    }
  }

  return [...seen];
}
function lookupVocabKey(story, key) {
  if (!key || !story?.vocab) return key;
  const candidates = candidateWordKeys(key);
  const direct = candidates.find((candidate) => story.vocab[candidate]);
  if (direct) return direct;

  const normalized = new Map(Object.keys(story.vocab).map((vocabKey) => [bareKey(vocabKey), vocabKey]));
  return candidates.map(bareKey).map((candidate) => normalized.get(candidate)).find(Boolean) || key;
}
function wordMatchesTarget(segment, targetKey) {
  if (!targetKey || !isWord(segment)) return false;
  const key = bareKey(segment);
  const target = bareKey(targetKey);
  return key === target || candidateWordKeys(key).some((candidate) => bareKey(candidate) === target);
}
function isWord(seg) {
  if (!seg) return false;
  if (/^\s+$/.test(seg)) return false;
  if (/^[،.؟!:«»"'.…\u061B\u061F()[\]{}]+$/.test(seg)) return false;
  if (DIGITS.test(seg)) return false;
  const key = bareKey(seg);
  if (NON_STANDALONE_KEYS.has(key)) return false;
  return /\p{Script=Arabic}/u.test(key);
}
function tokenize(line) {
  return line.split(SPLIT_RE).filter((p) => p !== "" && p !== undefined);
}
function countArabicWords(text) {
  return tokenize(text || "").filter(isWord).length;
}
function contextWindow(text, targetWordIndex, targetKey, radius = CONTEXT_WORD_RADIUS) {
  if (!text) return { text: "", targetWordIndex: null, sourceTargetWordIndex: null, sourceWordCount: 0 };
  const segs = tokenize(text);
  const wordSegments = [];
  let resolvedTarget = Number.isInteger(targetWordIndex) ? targetWordIndex : null;

  segs.forEach((seg, index) => {
    if (!isWord(seg)) return;
    const wordIndex = wordSegments.length;
    wordSegments.push(index);
    if (resolvedTarget == null && targetKey && wordMatchesTarget(seg, targetKey)) {
      resolvedTarget = wordIndex;
    }
  });

  if (!wordSegments.length || resolvedTarget == null || wordSegments[resolvedTarget] == null) {
    return {
      text,
      targetWordIndex: null,
      sourceTargetWordIndex: null,
      sourceWordCount: wordSegments.length,
    };
  }

  const firstWord = Math.max(0, resolvedTarget - radius);
  const lastWord = Math.min(wordSegments.length - 1, resolvedTarget + radius);
  const firstSegment = wordSegments[firstWord];
  const lastSegment = wordSegments[lastWord];
  const clipped = segs.slice(firstSegment, lastSegment + 1).join("").trim();

  return {
    text: `${firstWord > 0 ? "... " : ""}${clipped}${lastWord < wordSegments.length - 1 ? " ..." : ""}`,
    targetWordIndex: resolvedTarget - firstWord,
    sourceTargetWordIndex: resolvedTarget,
    sourceWordCount: wordSegments.length,
  };
}
function englishContextWindow(text, context, targetKey, radius = ENGLISH_CONTEXT_WORD_RADIUS) {
  if (!text) return "";
  const words = text.trim().split(/\s+/).filter(Boolean);
  if (words.length <= radius * 2 + 6) return text;

  const sourceArabic = context?.sentenceArabic || context?.contextArabic || "";
  const arabicWordCount = countArabicWords(sourceArabic);
  let targetWordIndex = Number.isInteger(context?.clickedWordIndex) ? context.clickedWordIndex : null;

  if (targetWordIndex == null && sourceArabic) {
    const sourceContext = contextWindow(sourceArabic, context?.contextTargetWordIndex, targetKey);
    targetWordIndex = sourceContext.sourceTargetWordIndex;
  }

  const ratio = arabicWordCount > 1 && targetWordIndex != null
    ? Math.max(0, Math.min(1, targetWordIndex / (arabicWordCount - 1)))
    : 0.5;
  const targetEnglishIndex = Math.round(ratio * Math.max(0, words.length - 1));
  const firstWord = Math.max(0, targetEnglishIndex - radius);
  const lastWord = Math.min(words.length, targetEnglishIndex + radius + 1);
  const clipped = words.slice(firstWord, lastWord).join(" ");

  return `${firstWord > 0 ? "... " : ""}${clipped}${lastWord < words.length ? " ..." : ""}`;
}
function displayContext(context, targetKey) {
  if (!context) return null;
  let clipped;
  if (context.contextArabic) {
    clipped = contextWindow(context.contextArabic, context.contextTargetWordIndex, targetKey);
  } else {
    clipped = contextWindow(context.sentenceArabic, context.clickedWordIndex, targetKey);
  }
  return {
    ...clipped,
    meaning: englishContextWindow(context.sentenceMeaning, context, targetKey),
  };
}
function isArabicText(value) {
  return ARABIC_SCRIPT_RE.test(value || "");
}
function getInitialView() {
  const raw = (window.location.hash || "#home").replace("#", "");
  return ["home", "read", "library", "path", "my-words", "about", "contact"].includes(raw) ? raw : "home";
}
function localDateKey(date = new Date()) {
  return [
    date.getFullYear(),
    String(date.getMonth() + 1).padStart(2, "0"),
    String(date.getDate()).padStart(2, "0"),
  ].join("-");
}
function daysBetweenKeys(a, b) {
  if (!a || !b) return null;
  const first = new Date(`${a}T00:00:00`);
  const second = new Date(`${b}T00:00:00`);
  return Math.round((second - first) / (24 * 60 * 60 * 1000));
}
function readStudyStats() {
  try {
    const raw = JSON.parse(localStorage.getItem("tmar:study-stats") || "{}");
    const today = localDateKey();
    return {
      streakDays: Number(raw.streakDays || 0),
      lastStudyDate: raw.lastStudyDate || null,
      todayReviews: raw.lastStudyDate === today ? Number(raw.todayReviews || 0) : 0,
    };
  } catch {
    return { streakDays: 0, lastStudyDate: null, todayReviews: 0 };
  }
}
function recordStudyReview(previous) {
  const today = localDateKey();
  const diff = daysBetweenKeys(previous?.lastStudyDate, today);
  const continuing = diff === 0 || diff === 1;
  return {
    streakDays: diff === 0
      ? Number(previous?.streakDays || 0)
      : continuing
        ? Number(previous?.streakDays || 0) + 1
        : 1,
    lastStudyDate: today,
    todayReviews: diff === 0 ? Number(previous?.todayReviews || 0) + 1 : 1,
  };
}
function reviewedTodayCount(saved, studyStats) {
  const today = localDateKey();
  const savedToday = saved.filter((item) => {
    const reviewedAt = srsOf(item).lastReviewedAt;
    return reviewedAt && localDateKey(new Date(reviewedAt)) === today;
  }).length;
  return Math.max(savedToday, Number(studyStats?.todayReviews || 0));
}
function wordStudyBuckets(saved, studyStats) {
  const due = saved.filter(isDue);
  const learning = saved.filter((item) => {
    const srs = srsOf(item);
    return Number(srs.reviews || 0) > 0 && Number(srs.intervalDays || 0) < 14;
  });
  const mastered = saved.filter((item) => {
    const srs = srsOf(item);
    return Number(srs.reviews || 0) >= 3 && Number(srs.intervalDays || 0) >= 14;
  });
  return {
    due,
    learning,
    mastered,
    reviewedToday: reviewedTodayCount(saved, studyStats),
  };
}
function srsOf(item) {
  return item.srs || {
    dueAt: item.dueAt || item.createdAt || new Date().toISOString(),
    intervalDays: item.intervalDays || 0,
    ease: item.ease || 2.5,
    reviews: item.reviews || 0,
    lapses: item.lapses || 0,
  };
}
function isDue(item) {
  return new Date(srsOf(item).dueAt).getTime() <= Date.now();
}
function addMinutes(date, minutes) {
  return new Date(date.getTime() + minutes * 60 * 1000).toISOString();
}
function addDays(date, days) {
  return new Date(date.getTime() + days * 24 * 60 * 60 * 1000).toISOString();
}
function groupByReading(items) {
  return items.reduce((acc, item) => {
    const title = item.context?.storyTitle || item.storyTitle || "Saved words";
    if (!acc[title]) acc[title] = [];
    acc[title].push(item);
    return acc;
  }, {});
}
function wordKeyOf(item) {
  if (!item) return "";
  if (item.wordKey) return item.wordKey;
  if (typeof item.key === "string" && item.key.includes(":")) {
    return item.key.slice(item.key.indexOf(":") + 1);
  }
  return item.key;
}
function readingSlugOf(item) {
  return item?.readingSlug || item?.context?.readingSlug || null;
}
function clientKeyFor(storySlug, wordKey) {
  return `${storySlug || "reading"}:${wordKey}`;
}
function meaningFromStory(item, story) {
  if (!item || !story?.vocab) return null;
  const readingSlug = readingSlugOf(item);
  if (readingSlug && readingSlug !== story.meta?.slug) return null;
  const key = lookupVocabKey(story, wordKeyOf(item));
  return story.vocab[key]?.en || null;
}
function hydrateSavedMeaning(item, stories = []) {
  if (item?.meaning) return item;
  const meaning = stories.map((story) => meaningFromStory(item, story)).find(Boolean);
  return meaning ? { ...item, meaning } : item;
}
function getSupabaseClient() {
  const config = window.TMAR_SUPABASE;
  const factory = window.supabase?.createClient;
  if (!config?.url || !config?.publishableKey || !factory) return null;
  return factory(config.url, config.publishableKey);
}
function makeSavedItem({ key, display, context, vocab, story }) {
  const now = new Date().toISOString();
  const readingSlug = story.meta.slug;
  const clippedContext = displayContext(context, key);
  return {
    key: clientKeyFor(readingSlug, key),
    wordKey: key,
    display,
    meaning: vocab?.en || null,
    root: vocab?.r || null,
    readingSlug,
    readingId: story.meta.remoteId || null,
    story: story.meta.storyNumber,
    storyTitle: story.meta.englishTitle,
    storyTitleArabic: story.meta.arabicTitle.full,
    context: context ? {
      readingSlug,
      readingId: story.meta.remoteId || null,
      storyId: story.meta.storyNumber,
      storyTitle: story.meta.englishTitle,
      storyTitleArabic: story.meta.arabicTitle.full,
      blockIndex: context.blockIndex,
      sentenceIndex: context.sentenceIndex,
      sentenceArabic: context.sentenceArabic,
      sentenceDisplayArabic: context.sentenceDisplayArabic,
      contextArabic: clippedContext?.text || context.sentenceArabic,
      contextTargetWordIndex: clippedContext?.targetWordIndex,
      sentenceMeaning: clippedContext?.meaning || context.sentenceMeaning,
    } : null,
    createdAt: now,
    srs: {
      dueAt: now,
      intervalDays: 0,
      ease: 2.5,
      reviews: 0,
      lapses: 0,
    },
  };
}
function savedItemToRow(item, userId) {
  const srs = srsOf(item);
  return {
    user_id: userId,
    client_key: item.key,
    reading_id: item.readingId || item.context?.readingId || null,
    display_ar: item.display,
    meaning_en: item.meaning || null,
    root_ar: item.root || null,
    context_ar: item.context?.contextArabic || item.context?.sentenceArabic || item.display,
    context_en: item.context?.sentenceMeaning || null,
    saved_at: item.createdAt || new Date().toISOString(),
    due_at: srs.dueAt,
    interval_days: Number(srs.intervalDays || 0),
    ease: Number(srs.ease || 2.5),
    reviews: Number(srs.reviews || 0),
    lapses: Number(srs.lapses || 0),
    suspended: false,
  };
}
function rowToSavedItem(row) {
  const wordKey = row.client_key?.includes(":")
    ? row.client_key.slice(row.client_key.indexOf(":") + 1)
    : row.client_key || row.display_ar;
  const readingSlug = row.client_key?.includes(":")
    ? row.client_key.slice(0, row.client_key.indexOf(":"))
    : null;
  return {
    key: row.client_key || row.display_ar,
    wordKey,
    display: row.display_ar,
    meaning: row.meaning_en,
    root: row.root_ar,
    remoteId: row.id,
    readingSlug,
    readingId: row.reading_id || null,
    storyTitle: row.readings?.title_en || readingSlug || "Reading",
    storyTitleArabic: row.readings?.title_ar_full || "",
    context: {
      readingSlug,
      readingId: row.reading_id || null,
      storyTitle: row.readings?.title_en || readingSlug || "Reading",
      storyTitleArabic: row.readings?.title_ar_full || "",
      sentenceArabic: row.context_ar,
      contextArabic: row.context_ar,
      sentenceMeaning: row.context_en,
    },
    createdAt: row.saved_at || row.created_at,
    srs: {
      dueAt: row.due_at,
      intervalDays: row.interval_days,
      ease: Number(row.ease || 2.5),
      reviews: Number(row.reviews || 0),
      lapses: Number(row.lapses || 0),
    },
  };
}
function applySrsRating(item, rating) {
  const srs = srsOf(item);
  const now = new Date();
  const easeDelta = rating === "easy" ? 0.15 : rating === "again" ? -0.25 : rating === "hard" ? -0.1 : 0;
  const nextEase = Math.max(1.3, Math.min(3.2, Number(srs.ease || 2.5) + easeDelta));
  let nextInterval = Number(srs.intervalDays || 0);
  let dueAt;

  if (rating === "again") {
    nextInterval = 0;
    dueAt = addMinutes(now, 10);
  } else if (rating === "hard") {
    nextInterval = Math.max(1, Math.round(nextInterval * 1.2) || 1);
    dueAt = addDays(now, nextInterval);
  } else if (rating === "easy") {
    nextInterval = nextInterval === 0 ? 4 : Math.max(4, Math.round(nextInterval * (nextEase + 0.4)));
    dueAt = addDays(now, nextInterval);
  } else {
    nextInterval = nextInterval === 0 ? 1 : Math.max(1, Math.round(nextInterval * nextEase));
    dueAt = addDays(now, nextInterval);
  }

  return {
    ...item,
    srs: {
      dueAt,
      intervalDays: nextInterval,
      ease: nextEase,
      reviews: Number(srs.reviews || 0) + 1,
      lapses: Number(srs.lapses || 0) + (rating === "again" ? 1 : 0),
      lastRating: rating,
      lastReviewedAt: now.toISOString(),
    },
  };
}

// build root-letters → array of bare forms appearing in the story
function buildRootIndex(story) {
  const idx = {};      // bareForm → rootLetters
  const families = {}; // rootLetters → Set<bareForm>
  for (const root of story.roots) {
    families[root.letters] = new Set(root.forms);
    for (const f of root.forms) idx[f] = root.letters;
  }
  // also include vocab.r → bareForm mappings (more comprehensive)
  for (const [k, v] of Object.entries(story.vocab)) {
    if (!v.r) continue;
    if (!families[v.r]) families[v.r] = new Set();
    families[v.r].add(k);
    if (!(k in idx)) idx[k] = v.r;
  }
  return { idx, families };
}

// ============================================================
// Word
// ============================================================
function Word({ raw, story, rootIndex, selectedKey, savedSet, onPick, highlightedRoot, context }) {
  const rawKey = bareKey(raw);
  const key = lookupVocabKey(story, rawKey);
  const vocab = story.vocab[key];
  const rootOfWord = rootIndex.idx[key] || rootIndex.idx[rawKey];
  const isInHighlightedRoot = highlightedRoot && rootIndex.families[highlightedRoot]?.has(key);

  const cls = ["w"];
  if (vocab) cls.push("known");
  if (vocab?.key) cls.push("key-vocab");
  if (selectedKey === key || selectedKey === rawKey) cls.push("active");
  if (savedSet.has(key) || savedSet.has(rawKey)) cls.push("saved-mark");
  if (isInHighlightedRoot) cls.push("root-hl");

  return (
    <span
      className={cls.join(" ")}
      role="button"
      tabIndex={0}
      onClick={(e) => { e.stopPropagation(); onPick(key, raw, context); }}
      onKeyDown={(e) => {
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault(); onPick(key, raw, context);
        }
      }}
    >
      {raw}
    </span>
  );
}

// ============================================================
// Sentence
// ============================================================
function Sentence({ text, pull, context, ...wordProps }) {
  const segs = useMemo(() => tokenize(text), [text]);
  let wordIndex = -1;
  return (
    <p className={"sentence" + (pull ? " pull" : "")}>
      {segs.map((s, i) => {
        if (!isWord(s)) return <span className="t" key={i}>{s}</span>;
        wordIndex += 1;
        return (
          <Word
            key={i}
            raw={s}
            context={{ ...context, clickedWordIndex: wordIndex }}
            {...wordProps}
          />
        );
      })}
    </p>
  );
}

// ============================================================
// Story block (with optional English reveal under the Arabic)
// ============================================================
function StoryBlock({ block, idx, mode, revealed, onToggleMeaning, wordProps, showBreakBefore = false }) {
  return (
    <>
      {showBreakBefore && <div className="story-break" aria-hidden="true"><span /></div>}
      <div className="story-block">
        {block.sentences.map((s, si) => (
          <Sentence
            key={si}
            text={s[mode]}
            pull={s.pull}
            context={{
              blockIndex: idx,
              sentenceIndex: si,
              sentenceArabic: s.full,
              sentenceDisplayArabic: s[mode],
              sentenceMeaning: s.en || block.en,
            }}
            {...wordProps}
          />
        ))}
        <div className="block-tools">
          <button
            className="reveal-en"
            type="button"
            aria-expanded={revealed}
            aria-label={revealed ? "Hide English meaning" : "Show English meaning"}
            onClick={() => onToggleMeaning(idx)}
          >
            <span>EN</span>
            <span className="chev" aria-hidden="true">‹</span>
          </button>
          {revealed && <div className="block-en">{block.en}</div>}
        </div>
      </div>
    </>
  );
}

// ============================================================
// VowelToggle
// ============================================================
function VowelToggle({ mode, onChange }) {
  const opts = [
    { id: "full",  label: "Full" },
    { id: "light", label: "Light" },
    { id: "bare",  label: "None" },
  ];
  return (
    <div className="vowel-toggle" role="tablist" aria-label="Vowel density">
      {opts.map((o) => (
        <button
          key={o.id}
          role="tab"
          aria-pressed={mode === o.id}
          onClick={() => onChange(o.id)}
        >{o.label}</button>
      ))}
    </div>
  );
}

// ============================================================
// Reading controls
// ============================================================
function ReadingControls({ mode, setMode, onListen, isPlaying, duration }) {
  return (
    <div className="controls">
      <div className="ctrl-group">
        <VowelToggle mode={mode} onChange={setMode} />
      </div>
      <div className="ctrl-group" style={{ flex: 1, justifyContent: "center" }}>
        <button className="icon-btn" aria-pressed={isPlaying} onClick={onListen}>
          <span className="play-dot">
            {isPlaying ? (
              <svg viewBox="0 0 8 10" fill="currentColor"><rect width="3" height="10"/><rect x="5" width="3" height="10"/></svg>
            ) : (
              <svg viewBox="0 0 10 10" fill="currentColor"><path d="M1 0 L1 10 L9 5 Z"/></svg>
            )}
          </span>
          <span>{isPlaying ? "Pause" : "Listen"}</span>
          <span className="time">{duration}</span>
        </button>
      </div>
    </div>
  );
}

// ============================================================
// Word panel
// ============================================================
function ContextSentence({ text, targetKey, targetWordIndex = null }) {
  const segs = useMemo(() => tokenize(text), [text]);
  let wordIndex = -1;
  return (
    <div className="ctx-ar" dir="rtl">
      {segs.map((s, i) => {
        const word = isWord(s);
        if (word) wordIndex += 1;
        const hit = word && (
          Number.isInteger(targetWordIndex)
            ? wordIndex === targetWordIndex
            : wordMatchesTarget(s, targetKey)
        );
        return hit
          ? <span className="ctx-hit" key={i}>{s}</span>
          : <span key={i}>{s}</span>;
      })}
    </div>
  );
}

function WordPanel({ selection, story, rootIndex, savedSet, onClose, onSave, onUnsave, onPickRoot, onPickRelated, highlightedRoot }) {
  if (!selection) return null;
  const { key: selectionKey, display, context, savedClientKey, savedMeaning, savedRoot } = selection;
  const key = lookupVocabKey(story, selectionKey);
  const vocab = story.vocab[key] || (savedMeaning ? { en: savedMeaning, r: savedRoot } : null);
  const displayWord = vocab?.display || display;
  const isSaved = Boolean(savedClientKey) || savedSet.has(key) || savedSet.has(selectionKey);
  const savedKey = savedClientKey || (savedSet.has(key) ? key : selectionKey);
  const panelContext = displayContext(context, key);

  // related forms (same root, appearing in this story)
  let related = [];
  if (vocab?.r && rootIndex.families[vocab.r]) {
    related = [...rootIndex.families[vocab.r]].filter((f) => f !== key);
  }

  const rootInfo = vocab?.r ? story.roots.find((r) => r.letters === vocab.r) : null;

  return (
    <>
      <div className="word-panel-scrim" onClick={onClose}/>
      <div className="word-panel" role="dialog" aria-label="Word details">
        <div className="wp-top">
          <span className="wp-ar" dir="rtl">{displayWord}</span>
          <button className="wp-close" onClick={onClose} aria-label="Close">×</button>
        </div>

        {vocab ? (
          <>
            <div className="wp-en">{vocab.en}</div>
            {vocab.r && (
              <div className="wp-root">
                <span className="lbl">Root</span>
                <span
                  className="letters"
                  onClick={() => onPickRoot(vocab.r)}
                  role="button" tabIndex={0}
                  onKeyDown={(e) => { if (e.key === "Enter") onPickRoot(vocab.r); }}
                  title="Highlight this root family in the story"
                >{vocab.r}</span>
                {rootInfo && <span className="meaning">{rootInfo.meaning}</span>}
              </div>
            )}
            {related.length > 0 && (
              <div className="wp-related">
                <div className="lbl">Also in this story</div>
                <div className="wp-related-row">
                  {related.map((f) => (
                    <span
                      key={f}
                      onClick={() => onPickRelated(f)}
                      role="button" tabIndex={0}
                    >{f}</span>
                  ))}
                </div>
              </div>
            )}
            {vocab.note && <div className="wp-note">{vocab.note}</div>}
          </>
        ) : (
          <div className="wp-empty-en">
            No meaning prepared for this word yet.
            Save it and add support later.
          </div>
        )}

        {context && (
          <div className="wp-context">
            <div className="lbl">Context</div>
            <ContextSentence
              text={panelContext.text}
              targetKey={key}
              targetWordIndex={panelContext.targetWordIndex}
            />
            {(panelContext.meaning || context.sentenceMeaning) && (
              <div className="ctx-en">{panelContext.meaning || context.sentenceMeaning}</div>
            )}
          </div>
        )}

        <div className="wp-actions">
          <button
            className={isSaved ? "saved" : "primary"}
            onClick={() => isSaved ? onUnsave(savedKey) : onSave(key, displayWord, context, vocab)}
          >
            {isSaved ? (
              <>
                <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M2.5 6.5l2.4 2.4 4.6-5"/></svg>
                Saved
              </>
            ) : (
              <>
                <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M3 2v8l3-2 3 2V2H3z"/></svg>
                Save for flashcards
              </>
            )}
          </button>
        </div>
      </div>
    </>
  );
}

// ============================================================
// Quiz carousel — flippable cards with side arrows + swipe
// ============================================================
function QuizCarousel({ questions }) {
  const [i, setI] = useState(0);
  const [answers, setAnswers] = useState({});
  const [flipped, setFlipped] = useState({});
  const [done, setDone] = useState(false);
  const stageRef = useRef(null);

  const handlePick = (qi, oi) => {
    if (answers[qi] != null) return;
    setAnswers((a) => ({ ...a, [qi]: oi }));
    setTimeout(() => setFlipped((f) => ({ ...f, [qi]: true })), 280);
  };

  const next = () => {
    if (i < questions.length - 1) setI(i + 1);
    else setI(0);                  // loop
  };
  const prev = () => {
    if (i > 0) setI(i - 1);
    else setI(questions.length - 1);  // loop
  };
  const allAnswered = Object.keys(answers).length === questions.length;

  // swipe handling
  useEffect(() => {
    const el = stageRef.current;
    if (!el) return;
    let startX = null, startY = null;
    const onStart = (e) => {
      const t = e.touches ? e.touches[0] : e;
      startX = t.clientX; startY = t.clientY;
    };
    const onEnd = (e) => {
      if (startX == null) return;
      const t = e.changedTouches ? e.changedTouches[0] : e;
      const dx = t.clientX - startX;
      const dy = t.clientY - startY;
      startX = null;
      if (Math.abs(dx) < 50 || Math.abs(dy) > Math.abs(dx)) return;
      if (dx < 0) next(); else prev();
    };
    el.addEventListener("touchstart", onStart, { passive: true });
    el.addEventListener("touchend", onEnd, { passive: true });
    return () => {
      el.removeEventListener("touchstart", onStart);
      el.removeEventListener("touchend", onEnd);
    };
  }, [i, questions.length]);

  // keyboard arrows
  useEffect(() => {
    const onKey = (e) => {
      if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
      if (e.key === "ArrowLeft") prev();
      else if (e.key === "ArrowRight") next();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [i, questions.length]);

  const correctCount = Object.entries(answers)
    .filter(([qi, pick]) => questions[+qi].answer === pick).length;
  const reset = () => { setAnswers({}); setFlipped({}); setI(0); setDone(false); };

  if (done) {
    const msg = correctCount === questions.length
      ? "Excellent work. All correct."
      : correctCount >= 3
      ? "A good start. Re-read the story once more."
      : "Re-read the story. The answers are inside it.";
    return (
      <div className="quiz-complete">
        <div className="lbl">You answered</div>
        <div className="score">{correctCount} / {questions.length}</div>
        <div className="msg">{msg}</div>
        <button className="restart" onClick={reset}>Try again</button>
      </div>
    );
  }

  const q = questions[i];
  const picked = answers[i];
  const isFlipped = !!flipped[i];

  return (
    <div className="quiz-shell">
      <div className="quiz-progress">
        <span>Question {String(i + 1).padStart(2, "0")} of {String(questions.length).padStart(2, "0")}</span>
        <div className="dots">
          {questions.map((_, qi) => (
            <span
              key={qi}
              className={"dot" + (qi === i ? " cur" : "") + (answers[qi] != null && qi !== i ? " done" : "")}
              onClick={() => setI(qi)}
            />
          ))}
        </div>
      </div>

      <div className="quiz-stage" ref={stageRef}>
        <button
          className="quiz-arrow prev"
          onClick={prev}
          aria-label="Previous question"
        >
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M8.5 2.5 L3.5 7 L8.5 11.5"/></svg>
        </button>

        <div className={"quiz-card" + (isFlipped ? " flipped" : "")}>
          <div className="quiz-face front">
            <div className="q-num">№ {String(i + 1).padStart(2, "0")}</div>
            <h3 className="q-prompt" dir={q.dir || (isArabicText(q.q) ? "rtl" : "ltr")}>{q.q}</h3>
            <div className="q-options">
              {q.options.map((opt, oi) => (
                <button
                  key={oi}
                  className="q-opt"
                  onClick={() => handlePick(i, oi)}
                  disabled={picked != null}
                  style={picked === oi ? { borderColor: "var(--accent)", background: "var(--accent-bg-2)" } : {}}
                >
                  <span className="letter">{String.fromCharCode(65 + oi)}.</span>
                  <span dir={q.dir || (isArabicText(opt) ? "rtl" : "ltr")}>{opt}</span>
                </button>
              ))}
            </div>
          </div>
          <div className="quiz-face back">
            {picked != null && (() => {
              const right = picked === q.answer;
              return (
                <>
                  <div className={"q-back-verdict " + (right ? "correct" : "wrong")}>
                    {right ? "Yes." : "Not quite."}
                  </div>
                  <div className="q-back-line">
                    {right ? "From the story:" : `The answer is "${q.options[q.answer]}".`}
                  </div>
                  {q.why && q.why.match(/[\u0600-\u06FF]/) && (
                    <div className="q-back-ar" dir="rtl">{q.why.split(" — ")[0]}</div>
                  )}
                  <div className="q-back-why">
                    {q.why && q.why.includes("—") ? q.why.split(" — ").slice(1).join(" — ") : q.why}
                  </div>
                  <button className="q-back-next" onClick={() => {
                    if (Object.keys(answers).length === questions.length) {
                      setDone(true);
                    } else {
                      next();
                    }
                  }}>
                    {Object.keys(answers).length === questions.length
                      ? "See your score"
                      : "Next question →"}
                  </button>
                </>
              );
            })()}
          </div>
        </div>

        <button
          className="quiz-arrow next"
          onClick={next}
          aria-label="Next question"
        >
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6"><path d="M5.5 2.5 L10.5 7 L5.5 11.5"/></svg>
        </button>
      </div>
    </div>
  );
}

// ============================================================
// Product shell
// ============================================================
const LEVELS = [
  {
    name: "Beginner 1",
    code: "beginner_1",
    number: "01",
    numeral: "I",
    status: "Active",
    stage: "Foundation",
    outcome: "Finish short supported scenes and feel the reward of reaching the end.",
    libraryNote: "Start with complete readings that reward you quickly.",
  },
  {
    name: "Beginner 2",
    code: "beginner_2",
    number: "02",
    numeral: "II",
    status: "Active",
    stage: "Foundation",
    outcome: "Follow connected moments and begin returning to familiar characters.",
    libraryNote: "The readings get longer, but the thread stays clear.",
  },
  {
    name: "Pre-Intermediate 1",
    code: "pre_intermediate_1",
    number: "03",
    numeral: "III",
    status: "Active",
    stage: "Comfort",
    outcome: "Read smoother stories with fewer stops and more confidence between supports.",
    libraryNote: "Scenes begin to breathe. You are reading, not decoding every line.",
  },
  {
    name: "Pre-Intermediate 2",
    code: "pre_intermediate_2",
    number: "04",
    numeral: "IV",
    status: "Active",
    stage: "Comfort",
    outcome: "Build enough stamina to feel the first doorway toward Mutun.",
    libraryNote: "Longer readings prepare you for the next path without rushing you there.",
  },
  {
    name: "Intermediate 1",
    code: "intermediate_1",
    number: "05",
    numeral: "V",
    status: "Active",
    stage: "Fluency",
    outcome: "Mutun begins here, alongside fuller stories and steadier reading stamina.",
    libraryNote: "Mutun begins alongside fuller stories.",
    unlocksTextPath: true,
  },
  {
    name: "Intermediate 2",
    code: "intermediate_2",
    number: "06",
    numeral: "VI",
    status: "Active",
    stage: "Fluency",
    outcome: "Read longer passages with more independence and less fear of dense lines.",
    libraryNote: "The story path and Mutun both ask for more sustained attention.",
  },
  {
    name: "Upper-Intermediate 1",
    code: "upper_intermediate_1",
    number: "07",
    numeral: "VII",
    status: "Coming soon",
    stage: "Depth",
    outcome: "Move through serious reading with support that appears only where it is useful.",
    libraryNote: "Readings become more authentic while still protecting understanding.",
  },
  {
    name: "Upper-Intermediate 2",
    code: "upper_intermediate_2",
    number: "08",
    numeral: "VIII",
    status: "Coming soon",
    stage: "Depth",
    outcome: "Stay with longer Mutun passages and recover quickly when the Arabic gets demanding.",
    libraryNote: "The distance between graded reading and authentic reading gets smaller.",
  },
  {
    name: "Advanced 1",
    code: "advanced_1",
    number: "09",
    numeral: "IX",
    status: "Coming soon",
    stage: "Mastery",
    outcome: "Approach demanding Arabic with a long history of finished pages behind you.",
    libraryNote: "Support becomes lighter, and your own reading stamina carries more weight.",
  },
  {
    name: "Advanced 2",
    code: "advanced_2",
    number: "10",
    numeral: "X",
    status: "Coming soon",
    stage: "Mastery",
    outcome: "Read with enough range to choose harder material without feeling locked out.",
    libraryNote: "The platform becomes a bridge into your own independent reading life.",
  },
];

const SERIES = {
  morning: { en: "After Fajr", ar: "بَعْدَ الفَجْرِ" },
  quarter: { en: "Old Quarter", ar: "الحَيُّ القَدِيمُ" },
  letters: { en: "Letters From Home", ar: "رَسَائِلُ البَيْتِ" },
  parables: { en: "Short Parables", ar: "حِكَايَاتٌ قَصِيرَةٌ" },
};

const LIBRARY_READINGS = [
  {
    id: "after-fajr",
    level: "beginner_1",
    kind: "series",
    series: "morning",
    seriesIndex: 1,
    path: "Story",
    status: "Available",
    ar: "بَعْدَ الفَجْرِ",
    en: "After Fajr",
    hook: "Finish the first complete scene and save the words that matter.",
    words: 124,
    minutes: 4,
    progress: 1,
    saved: 0,
    current: true,
  },
  {
    id: "after-fajr-small-door",
    level: "beginner_1",
    kind: "series",
    series: "morning",
    seriesIndex: 2,
    path: "Story",
    status: "Available",
    ar: "الْبَابُ الصَّغِيرُ",
    en: "The Small Door",
    hook: "Ahmad receives the promised story: two men stand before a small door, afraid.",
    words: 92,
    minutes: 4,
    progress: 0,
    saved: 0,
  },
  {
    id: "after-fajr-behind-the-door",
    level: "beginner_1",
    kind: "series",
    series: "morning",
    seriesIndex: 3,
    path: "Story",
    status: "Available",
    ar: "خَلْفَ الْبَابِ",
    en: "Behind the Door",
    hook: "The door opens, but the harder question is whether the first man will enter.",
    words: 101,
    minutes: 4,
    progress: 0,
    saved: 0,
  },
  {
    id: "after-fajr-tomorrow",
    level: "beginner_1",
    kind: "series",
    series: "morning",
    seriesIndex: 4,
    path: "Story",
    status: "Available",
    ar: "غَدًا",
    en: "Tomorrow",
    hook: "Ahmad sees himself in the man who keeps saying tomorrow.",
    words: 79,
    minutes: 3,
    progress: 0,
    saved: 0,
  },
  {
    id: "after-fajr-with-fear",
    level: "beginner_1",
    kind: "series",
    series: "morning",
    seriesIndex: 5,
    path: "Story",
    status: "Available",
    ar: "مَعَ الْخَوْفِ",
    en: "With Fear",
    hook: "Ahmad reads the ending alone and finds the line meant for him.",
    words: 92,
    minutes: 4,
    progress: 0,
    saved: 0,
  },
  {
    id: "small-promise",
    level: "beginner_1",
    kind: "story",
    path: "Story",
    status: "Available",
    ar: "وَعْدٌ صَغِيرٌ",
    en: "A Small Promise",
    hook: "A short standalone reading built to feel complete, not like an exercise.",
    words: 132,
    minutes: 4,
    progress: 0,
    saved: 0,
  },
  {
    id: "small-message",
    level: "beginner_2",
    kind: "series",
    series: "letters",
    seriesIndex: 1,
    path: "Story",
    status: "Available",
    ar: "الرِّسَالَةُ الصَّغِيرَةُ",
    en: "The Small Message",
    hook: "A connected story that rewards you for remembering earlier words.",
    words: 220,
    minutes: 7,
    progress: 0,
    saved: 0,
  },
  {
    id: "neighbors-trust",
    level: "beginner_2",
    kind: "series",
    series: "quarter",
    seriesIndex: 1,
    path: "Story",
    status: "Available",
    ar: "أَمَانَةُ الجَارِ",
    en: "The Neighbor's Trust",
    hook: "Long enough to feel like a real reading, clear enough to keep moving.",
    words: 260,
    minutes: 8,
    progress: 0,
    saved: 0,
  },
  {
    id: "guest-at-door",
    level: "beginner_2",
    kind: "story",
    path: "Story",
    status: "Available",
    ar: "الضَّيْفُ عِنْدَ البَابِ",
    en: "The Guest at the Door",
    hook: "A simple arrival turns into a warmer, fuller scene.",
    words: 240,
    minutes: 7,
    progress: 0,
    saved: 0,
  },
  {
    id: "second-page",
    level: "beginner_2",
    kind: "story",
    path: "Story",
    status: "Available",
    ar: "الصَّفْحَةُ الثَّانِيَةُ",
    en: "The Second Page",
    hook: "A familiar character returns, and the Arabic starts to feel less new.",
    words: 252,
    minutes: 8,
    progress: 0,
    saved: 0,
  },
  {
    id: "shepherd-wolf",
    level: "pre_intermediate_1",
    kind: "series",
    series: "parables",
    seriesIndex: 1,
    path: "Story",
    status: "Available",
    ar: "الرَّاعِي وَالذِّئْبُ",
    en: "The Shepherd and the Wolf",
    hook: "A familiar kind of story, written so you can follow the turn.",
    words: 420,
    minutes: 10,
    progress: 0,
    saved: 0,
  },
  {
    id: "father-son",
    level: "pre_intermediate_1",
    kind: "series",
    series: "parables",
    seriesIndex: 2,
    path: "Story",
    status: "Available",
    ar: "الأَبُ وَالوَلَدُ",
    en: "The Father and the Son",
    hook: "A longer exchange that still keeps the meaning within reach.",
    words: 390,
    minutes: 10,
    progress: 0,
    saved: 0,
  },
  {
    id: "second-letter",
    level: "pre_intermediate_1",
    kind: "series",
    series: "letters",
    seriesIndex: 2,
    path: "Story",
    status: "Available",
    ar: "رِسَالَةٌ ثَانِيَةٌ",
    en: "A Second Letter",
    hook: "You return to the same thread with more Arabic under your feet.",
    words: 370,
    minutes: 9,
    progress: 0,
    saved: 0,
  },
  {
    id: "three-words",
    level: "pre_intermediate_1",
    kind: "story",
    path: "Story",
    status: "Available",
    ar: "ثَلَاثُ كَلِمَاتٍ",
    en: "Three Words",
    hook: "A compact reading with enough weight to make the ending matter.",
    words: 340,
    minutes: 9,
    progress: 0,
    saved: 0,
  },
  {
    id: "long-road",
    level: "pre_intermediate_2",
    kind: "story",
    path: "Story",
    status: "Available",
    ar: "الطَّرِيقُ الطَّوِيلُ",
    en: "The Long Road",
    hook: "A fuller reading that asks you to stay with the Arabic longer.",
    words: 610,
    minutes: 15,
    progress: 0,
    saved: 0,
  },
  {
    id: "night-lantern",
    level: "pre_intermediate_2",
    kind: "series",
    series: "quarter",
    seriesIndex: 2,
    path: "Story",
    status: "Available",
    ar: "فَانُوسٌ فِي اللَّيْلِ",
    en: "A Lantern in the Night",
    hook: "You read for atmosphere, not just information.",
    words: 560,
    minutes: 14,
    progress: 0,
    saved: 0,
  },
  {
    id: "shopkeeper",
    level: "pre_intermediate_2",
    kind: "story",
    path: "Story",
    status: "Available",
    ar: "صَاحِبُ الدُّكَّانِ",
    en: "The Shopkeeper",
    hook: "A longer scene where trust becomes the reason to keep reading.",
    words: 620,
    minutes: 15,
    progress: 0,
    saved: 0,
  },
  {
    id: "first-glimpse",
    level: "pre_intermediate_2",
    kind: "glimpse",
    path: "المُتُونُ",
    status: "Available",
    ar: "أَوَّلُ نَظْرَةٍ",
    en: "A First Glimpse",
    hook: "A small preview before Mutun truly opens.",
    words: 120,
    minutes: 6,
    progress: 0,
    saved: 0,
  },
  {
    id: "i1-story",
    level: "intermediate_1",
    kind: "story",
    path: "Story",
    status: "Coming soon",
    ar: "صَوْتُ المَوْجِ",
    en: "The Sound of the Waves",
  },
  {
    id: "i1-text",
    level: "intermediate_1",
    kind: "text",
    path: "المُتُونُ",
    status: "Coming soon",
    ar: "مِنَ الأَرْبَعِينَ",
    en: "A Mutun Selection",
  },
  {
    id: "i2-story",
    level: "intermediate_2",
    kind: "story",
    path: "Story",
    status: "Coming soon",
    ar: "حَدِيقَةُ الجَدَّةِ",
    en: "The Grandmother's Garden",
  },
  {
    id: "i2-text",
    level: "intermediate_2",
    kind: "text",
    path: "المُتُونُ",
    status: "Coming soon",
    ar: "مِنْ رِيَاضِ الصَّالِحِينَ",
    en: "A Mutun Selection",
  },
  {
    id: "u1-story",
    level: "upper_intermediate_1",
    kind: "story",
    path: "Story",
    status: "Coming soon",
    ar: "ظِلُّ النَّخِيلِ",
    en: "The Shade of the Palms",
  },
  {
    id: "u1-text",
    level: "upper_intermediate_1",
    kind: "text",
    path: "المُتُونُ",
    status: "Coming soon",
    ar: "مُقَدِّمَةٌ قَصِيرَةٌ",
    en: "A Mutun Selection",
  },
  {
    id: "u2-story",
    level: "upper_intermediate_2",
    kind: "story",
    path: "Story",
    status: "Coming soon",
    ar: "المَكْتَبَةُ القَدِيمَةُ",
    en: "The Old Library",
  },
  {
    id: "u2-text",
    level: "upper_intermediate_2",
    kind: "text",
    path: "المُتُونُ",
    status: "Coming soon",
    ar: "مِنَ الوَرَقَاتِ",
    en: "A Mutun Selection",
  },
  {
    id: "a1-story",
    level: "advanced_1",
    kind: "story",
    path: "Story",
    status: "Coming soon",
    ar: "أَخْبَارُ المُسَافِرِ",
    en: "The Traveller's Reports",
  },
  {
    id: "a1-text",
    level: "advanced_1",
    kind: "text",
    path: "المُتُونُ",
    status: "Coming soon",
    ar: "مِنْ إِحْيَاءِ عُلُومِ الدِّينِ",
    en: "A Mutun Selection",
  },
  {
    id: "a2-story",
    level: "advanced_2",
    kind: "story",
    path: "Story",
    status: "Coming soon",
    ar: "رِحْلَةٌ طَوِيلَةٌ",
    en: "A Long Journey",
  },
  {
    id: "a2-text",
    level: "advanced_2",
    kind: "text",
    path: "المُتُونُ",
    status: "Coming soon",
    ar: "مِنَ المُوَطَّإِ",
    en: "A Mutun Selection",
  },
];

const LOCAL_MUTUN_READINGS = [
  {
    id: "qaaida-fi-sabr-01-sabr-wa-shukr",
    level: "intermediate_1",
    kind: "glimpse",
    shelf: "mutun",
    origin: "classic_import",
    sourceCollectionId: "mutun-qaaida-fi-sabr",
    sourceTitleAr: "قاعدة في الصبر",
    sourceTitleEn: "A Principle on Patience",
    sourceAuthorAr: "أحمد بن عبد الحليم ابن تيمية",
    sourceAuthorEn: "Ibn Taymiyyah",
    seriesIndex: 1,
    path: "المُتُونُ",
    status: "Available",
    ar: "الصَّبْرُ وَالشُّكْرُ",
    en: "Patience and Gratitude",
    hook: "The believer's whole affair is between patience and gratitude.",
    words: 200,
    minutes: 5,
    progress: 0,
    saved: 0,
  },
  {
    id: "qaaida-fi-sabr-02-anwa-al-sabr",
    level: "intermediate_1",
    kind: "glimpse",
    shelf: "mutun",
    origin: "classic_import",
    sourceCollectionId: "mutun-qaaida-fi-sabr",
    sourceTitleAr: "قاعدة في الصبر",
    sourceTitleEn: "A Principle on Patience",
    sourceAuthorAr: "أحمد بن عبد الحليم ابن تيمية",
    sourceAuthorEn: "Ibn Taymiyyah",
    seriesIndex: 2,
    path: "المُتُونُ",
    status: "Available",
    ar: "أَنْوَاعُ الصَّبْرِ",
    en: "The Types of Patience",
    hook: "Patience in obedience, away from sin, and through calamity.",
    words: 244,
    minutes: 7,
    progress: 0,
    saved: 0,
  },
  {
    id: "qaaida-fi-sabr-03-sabr-ala-adha-al-nas",
    level: "intermediate_1",
    kind: "glimpse",
    shelf: "mutun",
    origin: "classic_import",
    sourceCollectionId: "mutun-qaaida-fi-sabr",
    sourceTitleAr: "قاعدة في الصبر",
    sourceTitleEn: "A Principle on Patience",
    sourceAuthorAr: "أحمد بن عبد الحليم ابن تيمية",
    sourceAuthorEn: "Ibn Taymiyyah",
    seriesIndex: 3,
    path: "المُتُونُ",
    status: "Available",
    ar: "الصَّبْرُ عَلَى أَذَى النَّاسِ",
    en: "Patience with Harm from People",
    hook: "The hardest test: harm from people and the choice not to answer with revenge.",
    words: 302,
    minutes: 8,
    progress: 0,
    saved: 0,
  },
  {
    id: "qaaida-fi-sabr-04-ma-yueen-ala-sabr-1",
    level: "intermediate_1",
    kind: "glimpse",
    shelf: "mutun",
    origin: "classic_import",
    sourceCollectionId: "mutun-qaaida-fi-sabr",
    sourceTitleAr: "قاعدة في الصبر",
    sourceTitleEn: "A Principle on Patience",
    sourceAuthorAr: "أحمد بن عبد الحليم ابن تيمية",
    sourceAuthorEn: "Ibn Taymiyyah",
    seriesIndex: 4,
    path: "المُتُونُ",
    status: "Available",
    ar: "مَا يُعِينُ عَلَى الصَّبْرِ ١",
    en: "What Helps Patience 1",
    hook: "The first helps: seeing decree, returning to repentance, hoping for reward, and clearing the heart.",
    words: 410,
    minutes: 11,
    progress: 0,
    saved: 0,
  },
  {
    id: "qaaida-fi-sabr-05-ma-yueen-ala-sabr-2",
    level: "intermediate_1",
    kind: "glimpse",
    shelf: "mutun",
    origin: "classic_import",
    sourceCollectionId: "mutun-qaaida-fi-sabr",
    sourceTitleAr: "قاعدة في الصبر",
    sourceTitleEn: "A Principle on Patience",
    sourceAuthorAr: "أحمد بن عبد الحليم ابن تيمية",
    sourceAuthorEn: "Ibn Taymiyyah",
    seriesIndex: 5,
    path: "المُتُونُ",
    status: "Available",
    ar: "مَا يُعِينُ عَلَى الصَّبْرِ ٢",
    en: "What Helps Patience 2",
    hook: "More helps: honor through forgiveness, reward matching the deed, the Prophet's example, and Allah's nearness.",
    words: 496,
    minutes: 13,
    progress: 0,
    saved: 0,
  },
  {
    id: "qaaida-fi-sabr-06-ma-yueen-ala-sabr-3",
    level: "intermediate_1",
    kind: "glimpse",
    shelf: "mutun",
    origin: "classic_import",
    sourceCollectionId: "mutun-qaaida-fi-sabr",
    sourceTitleAr: "قاعدة في الصبر",
    sourceTitleEn: "A Principle on Patience",
    sourceAuthorAr: "أحمد بن عبد الحليم ابن تيمية",
    sourceAuthorEn: "Ibn Taymiyyah",
    seriesIndex: 6,
    path: "المُتُونُ",
    status: "Available",
    ar: "مَا يُعِينُ عَلَى الصَّبْرِ ٣",
    en: "What Helps Patience 3",
    hook: "The closing helps: mastering the self, Allah's support, avoiding escalation, and letting one good deed lead to another.",
    words: 502,
    minutes: 13,
    progress: 0,
    saved: 0,
  },
];

function mergeLocalPlannedReadings(readings) {
  const existing = new Set(readings.map((reading) => reading.id));
  return [
    ...readings,
    ...LOCAL_MUTUN_READINGS.filter((reading) => !existing.has(reading.id)),
  ];
}

const FALLBACK_PRODUCT_DATA = {
  levels: LEVELS,
  readings: mergeLocalPlannedReadings(LIBRARY_READINGS),
  series: SERIES,
  source: "local",
};

function normalizeLevelStatus(status) {
  return status === "active" ? "Active" : "Coming soon";
}

function normalizeReadingStatus(status) {
  if (status === "published") return "Available";
  if (status === "coming_soon") return "Coming soon";
  if (status === "planned") return "Planned";
  if (status === "absorbed") return "Absorbed";
  return "Planned";
}

const MUTUN_HINTS = [
  "mutun",
  "المتون",
  "المُتُون",
  "qaaida-fi-sabr",
  "qaida-fi-sabr",
  "qaidah",
  "principle on patience",
  "قاعدة في الصبر",
  "most beautiful names",
  "asmaul husna",
  "asma al husna",
  "أسماء الله الحسنى",
  "fiqh asma",
  "mukhtasar fiqh",
];

function hasMutunSignal(...parts) {
  const haystack = stripDiacritics(parts.filter(Boolean).join(" ").toLowerCase());
  return MUTUN_HINTS.some((hint) => haystack.includes(stripDiacritics(hint).toLowerCase()));
}

function isMutunReading(reading = {}) {
  return reading.shelf === "mutun"
    || reading.kind === "text"
    || reading.path === "المُتُونُ"
    || hasMutunSignal(
      reading.sourceCollectionId,
      reading.series,
      reading.seriesTitle,
      reading.seriesTitleAr,
      reading.sourceTitleEn,
      reading.sourceTitleAr,
      reading.en,
      reading.ar,
    );
}

function estimateMinutes(words) {
  if (!words) return null;
  return Math.max(4, Math.ceil(Number(words) / 40));
}

function buildSeriesMap(seriesRows = []) {
  const map = {
    ...SERIES,
    old_quarter: SERIES.quarter,
    letters_home: SERIES.letters,
    short_parables: SERIES.parables,
  };

  seriesRows.forEach((series) => {
    map[series.id] = {
      en: series.title_en,
      ar: series.title_ar,
    };
  });

  return map;
}

function buildSourceCollectionMap(sourceRows = []) {
  return sourceRows.reduce((map, source) => {
    map[source.id] = {
      titleAr: source.title_ar,
      titleEn: source.title_en,
      authorAr: source.author_ar,
      authorEn: source.author_en,
    };
    return map;
  }, {});
}

function buildLevelFromRow(row) {
  const fallback = LEVELS.find((level) => level.code === row.code) || {};
  return {
    ...fallback,
    name: row.display_name || fallback.name,
    code: row.code,
    cefrRange: row.cefr_range || fallback.cefrRange,
    sortOrder: row.sort_order || Number(fallback.number) || 999,
    number: fallback.number || String(row.sort_order || "").padStart(2, "0"),
    numeral: fallback.numeral || row.sort_order,
    status: normalizeLevelStatus(row.status),
    stage: row.stage || fallback.stage,
    unlocksTextPath: Boolean(row.unlocks_text_path),
    cumulativeTargetWords: row.cumulative_target_words,
    readingLengthMin: row.reading_length_min,
    readingLengthMax: row.reading_length_max,
    newWordsMin: row.new_words_min,
    newWordsMax: row.new_words_max,
    defaultVowelMode: row.default_vowel_mode,
    outcome: fallback.outcome,
    libraryNote: fallback.libraryNote,
  };
}

function buildReadingFromRow(row, seriesMap, sourceMap) {
  const fallback = LIBRARY_READINGS.find((reading) => (
    reading.id === row.slug ||
    reading.en === row.title_en ||
    reading.ar === row.title_ar_full
  )) || {};
  const status = normalizeReadingStatus(row.status);
  const source = sourceMap?.[row.source_collection_id] || null;
  const isTextPath = row.path === "texts"
    || hasMutunSignal(
      row.display_shelf,
      row.path,
      row.source_collection_id,
      row.series_id,
      row.title_en,
      row.title_ar_full,
      source?.titleEn,
      source?.titleAr,
    );
  const shelf = row.display_shelf || (isTextPath ? "mutun" : "original_stories");
  const kind = shelf === "classic_stories"
    ? "classic"
    : shelf === "dialogues"
      ? "dialogue"
      : isTextPath || shelf === "mutun"
        ? (row.status === "planned" ? "glimpse" : "text")
    : (row.series_id ? "series" : "story");
  const series = row.series_id || fallback.series;
  const seriesTitle = seriesMap[series]?.en;
  const words = row.word_count || fallback.words || null;
  const pathLabel = shelf === "classic_stories"
    ? "Book"
    : shelf === "dialogues"
      ? "Dialogue"
      : isTextPath || shelf === "mutun"
        ? "المُتُونُ"
        : "Story";

  return {
    ...fallback,
    id: row.slug,
    level: row.level_code,
    kind,
    shelf,
    origin: row.origin || "original",
    sourceCollectionId: row.source_collection_id || null,
    sourceTitleAr: source?.titleAr || null,
    sourceTitleEn: source?.titleEn || null,
    sourceAuthorAr: source?.authorAr || null,
    sourceAuthorEn: source?.authorEn || null,
    series,
    seriesTitle,
    seriesTitleAr: seriesMap[series]?.ar,
    seriesIndex: row.series_order || fallback.seriesIndex,
    path: pathLabel,
    status,
    ar: row.title_ar_full,
    en: row.title_en,
    hook: row.summary_en || fallback.hook || "",
    words,
    minutes: fallback.minutes || estimateMinutes(words),
    progress: fallback.progress || 0,
    saved: fallback.saved || 0,
    current: row.slug === "after-fajr" || fallback.current || false,
  };
}

function buildProductData({ levelRows, readingRows, seriesRows, sourceRows } = {}) {
  if (!levelRows?.length || !readingRows?.length) return FALLBACK_PRODUCT_DATA;

  const series = buildSeriesMap(seriesRows);
  const sources = buildSourceCollectionMap(sourceRows);
  const remoteLevels = new Map(levelRows.map((row) => [row.code, buildLevelFromRow(row)]));
  const fallbackCodes = new Set(LEVELS.map((level) => level.code));
  const levels = [
    ...LEVELS.map((level) => remoteLevels.get(level.code) || level),
    ...[...remoteLevels.entries()]
      .filter(([code]) => !fallbackCodes.has(code))
      .map(([, level]) => level),
  ].sort((a, b) => (a.sortOrder || Number(a.number) || 999) - (b.sortOrder || Number(b.number) || 999));
  const orderByLevel = new Map(levels.map((level, index) => [level.code, index]));
  const readings = mergeLocalPlannedReadings([...readingRows]
    .map((row) => buildReadingFromRow(row, series, sources)))
    .sort((a, b) => {
      const levelDiff = (orderByLevel.get(a.level) ?? 999) - (orderByLevel.get(b.level) ?? 999);
      if (levelDiff) return levelDiff;
      const pathDiff = (a.path === "Story" ? 0 : 1) - (b.path === "Story" ? 0 : 1);
      if (pathDiff) return pathDiff;
      const shelfDiff = (a.shelf || "").localeCompare(b.shelf || "");
      if (shelfDiff) return shelfDiff;
      return (a.seriesIndex || 999) - (b.seriesIndex || 999) || a.en.localeCompare(b.en);
    });

  return { levels, readings, series, sources, source: "supabase" };
}

function buildRemoteVocab(baseStory, entries = [], forms = [], curatedKeys = new Set(), occurrenceGlosses = []) {
  const vocab = { ...baseStory.vocab };
  const entriesById = new Map(entries.map((entry) => [entry.id, entry]));

  entries.forEach((entry) => {
    vocab[entry.lemma_stripped] = {
      ...(vocab[entry.lemma_stripped] || {}),
      en: entry.meaning_en,
      r: entry.root_ar,
      note: entry.learner_note,
      key: curatedKeys.has(entry.lemma_stripped),
    };
  });

  forms.forEach((form) => {
    const entry = entriesById.get(form.vocab_entry_id);
    if (!entry) return;
    vocab[form.form_stripped] = {
      ...(vocab[form.form_stripped] || {}),
      display: form.form_ar || vocab[form.form_stripped]?.display || null,
      en: form.meaning_en || entry.meaning_en,
      r: entry.root_ar,
      note: form.learner_note || entry.learner_note,
      key: curatedKeys.has(form.form_stripped),
    };
  });

  occurrenceGlosses.forEach((occurrence) => {
    const form = occurrence.word_forms;
    if (!form) return;
    const entry = form.vocab_entries || {};
    const key = occurrence.surface_stripped || form.form_stripped || entry.lemma_stripped;
    if (!key) return;
    const meaning = form.meaning_en || entry.meaning_en;
    if (!meaning) return;

    vocab[key] = {
      ...(vocab[key] || {}),
      display: form.form_ar || vocab[key]?.display || null,
      en: meaning,
      r: entry.root_ar || vocab[key]?.r || null,
      note: form.learner_note || entry.learner_note || vocab[key]?.note || null,
      key: Boolean(occurrence.is_curated || vocab[key]?.key),
    };

    if (form.form_stripped && form.form_stripped !== key) {
      vocab[form.form_stripped] = {
        ...(vocab[form.form_stripped] || {}),
        display: form.form_ar || vocab[form.form_stripped]?.display || null,
        en: meaning,
        r: entry.root_ar || vocab[form.form_stripped]?.r || null,
        note: form.learner_note || entry.learner_note || vocab[form.form_stripped]?.note || null,
        key: Boolean(occurrence.is_curated || vocab[form.form_stripped]?.key),
      };
    }
  });

  return vocab;
}

function buildStoryFromRemote({ baseStory, reading, sentences, questions, patterns, levels, vocabEntries, wordForms, curatedKeys, occurrenceGlosses, source }) {
  if (!reading || !sentences?.length) return baseStory;
  const level = levels?.find((item) => item.code === reading.level_code);
  const sortedSentences = [...sentences].sort((a, b) => (
    a.block_index - b.block_index || a.sentence_index - b.sentence_index
  ));
  const blocks = [];

  sortedSentences.forEach((sentence) => {
    if (!blocks[sentence.block_index]) {
      blocks[sentence.block_index] = { en: "", meanings: [], sentences: [] };
    }
    const block = blocks[sentence.block_index];
    block.meanings.push(sentence.meaning_en);
    block.sentences.push({
      full: sentence.ar_full,
      light: sentence.ar_light || sentence.ar_full,
      bare: sentence.ar_none || stripDiacritics(sentence.ar_full),
      en: sentence.meaning_en,
      pull: sentence.is_pull_quote,
    });
  });

  return {
    ...baseStory,
    meta: {
      ...baseStory.meta,
      remoteId: reading.id,
      arabicTitle: {
        full: reading.title_ar_full,
        light: reading.title_ar_light || reading.title_ar_full,
        bare: reading.title_ar_none || stripDiacritics(reading.title_ar_full),
      },
      slug: reading.slug,
      englishTitle: reading.title_en,
      level: level?.name || baseStory.meta.level,
      cefr: level?.cefrRange || baseStory.meta.cefr,
      wordCount: reading.word_count || baseStory.meta.wordCount,
      newWords: reading.new_word_count || baseStory.meta.newWords,
      storyNumber: String(reading.series_order || reading.source_order || baseStory.meta.storyNumber || "01").padStart(2, "0"),
      readingMinutes: estimateMinutes(reading.word_count) || baseStory.meta.readingMinutes,
      listenMinutes: reading.audio_normal_url || reading.audio_slow_url ? baseStory.meta.listenMinutes : "soon",
      source: source ? {
        titleAr: source.title_ar,
        titleEn: source.title_en,
        authorAr: source.author_ar,
        authorEn: source.author_en,
      } : null,
    },
    blocks: blocks.filter(Boolean).map((block) => ({
      en: block.meanings.filter(Boolean).join(" "),
      sentences: block.sentences,
    })),
    vocab: buildRemoteVocab(baseStory, vocabEntries, wordForms, curatedKeys, occurrenceGlosses),
    questions: questions?.length ? [...questions]
      .sort((a, b) => a.sort_order - b.sort_order)
      .map((question) => ({
        q: question.prompt_ar || question.prompt_en,
        options: question.options_ar || question.options_en,
        dir: question.prompt_ar || question.options_ar ? "rtl" : "ltr",
        answer: question.correct_option_index,
        why: [
          question.explanation_ar,
          question.explanation_en,
        ].filter(Boolean).join(" — "),
      })) : baseStory.questions,
    notes: patterns?.length ? [...patterns]
      .sort((a, b) => a.sort_order - b.sort_order)
      .map((pattern) => ({
        title: pattern.title_en,
        arabic: pattern.source_ar,
        pattern: pattern.pattern_ar,
        body: pattern.meaning_en,
        substitutions: Array.isArray(pattern.substitutions) ? pattern.substitutions : [],
      })) : baseStory.notes,
    benefit: reading.lesson_ar || reading.lesson_en
      ? {
        full: reading.lesson_ar || "",
        english: reading.lesson_en || "",
      }
      : reading.slug === "after-fajr"
        ? baseStory.benefit
        : null,
    nextUp: reading.next_up_ar || reading.next_up_en || reading.next_up_label || reading.next_up_meta
      ? {
        label: reading.next_up_label || "Next",
        ar: reading.next_up_ar || "",
        en: reading.next_up_en || "",
        meta: reading.next_up_meta || "",
      }
      : reading.slug === "after-fajr"
        ? baseStory.nextUp
        : null,
  };
}

function ProductNav({ view, onView, savedCount, dueCount }) {
  const items = [
    { id: "home", label: "Home" },
    { id: "read", label: "Read" },
    { id: "library", label: "Library" },
    { id: "my-words", label: "My Words", count: dueCount || savedCount },
  ];
  return (
    <nav className="product-nav" aria-label="Primary">
      {items.map((item) => (
        <button
          key={item.id}
          className={view === item.id ? "active" : ""}
          onClick={() => onView(item.id)}
        >
          <span>{item.label}</span>
          {item.count > 0 && <span className="nav-count">{item.count}</span>}
        </button>
      ))}
    </nav>
  );
}

function PageHero({ eyebrow, title, subtitle, children }) {
  return (
    <section className="page-hero">
      <div className="section-eyebrow">{eyebrow}</div>
      <h1>{title}</h1>
      {subtitle && <p>{subtitle}</p>}
      {children}
    </section>
  );
}

function EmptyWords({ onRead }) {
  return (
    <div className="empty-state">
      <div className="empty-ar" dir="rtl">العِلْمُ يَحْتَاجُ إِلَى صَبْرٍ.</div>
      <h3>No saved words yet</h3>
      <p>Save words from a reading and they will appear here with their sentence.</p>
      <button className="solid-btn" onClick={onRead}>Read the sample</button>
    </div>
  );
}

function ReadingLoading({ reading }) {
  return (
    <main className="page reading-loading">
      <div className="section-eyebrow">Opening reading</div>
      <div className="loading-ar" dir="rtl">{reading?.ar || "جَارِي التَّحْمِيلِ"}</div>
      <h1>{reading?.en || "Loading your page"}</h1>
      <p>Preparing the reading now.</p>
    </main>
  );
}

function HomePage({ story, saved, studyStats, productData, onRead, onLibrary, onWords, onPath }) {
  const buckets = wordStudyBuckets(saved, studyStats);
  const levels = productData?.levels || LEVELS;
  const levelOrder = new Map(levels.map((level, index) => [level.code, index]));
  const readings = sortReadings((productData?.readings || LIBRARY_READINGS).filter(canOpenReading), levelOrder);
  const activeLevelCount = levels.filter((level) => level.status === "Active").length;
  const currentReading = readings.find((reading) => reading.progress > 0 && reading.progress < 1)
    || readings.find((reading) => reading.current && (reading.progress ?? 0) < 1)
    || readings.find((reading) => (reading.progress ?? 0) < 1)
    || readings.find((reading) => reading.current)
    || readings[0]
    || null;
  const nextReading = readings.find((reading) => reading.id !== currentReading?.id) || currentReading;
  const currentLevel = levels.find((level) => level.code === currentReading?.level)
    || levels.find((level) => level.status === "Active")
    || levels[0];
  const mutunStartLevel = levels.find((level) => level.unlocksTextPath) || levels[4];
  const progress = currentReading?.progress ?? (currentReading?.current ? 0.45 : 0);
  const progressLabel = progress >= 1
    ? "Complete"
    : progress > 0
      ? `${Math.round(progress * 100)}% read`
      : "Ready to start";

  return (
    <main className="page app-page home-page">
      <section className="home-greet">
        <div>
          <div className="eyebrow">
            <span className="ar-mark" dir="rtl">السَّلَامُ عَلَيْكُمْ</span>
            <span>Home</span>
          </div>
          <h1>Welcome back. <em>One page is enough.</em></h1>
        </div>
        <div className="home-date">
          {buckets.reviewedToday} reviewed today<br />
          <span>{studyStats.streakDays || 0}-day streak</span>
        </div>
      </section>

      <section className="home-continue">
        <button className="continue-card" onClick={() => onRead(currentReading)}>
          <div className="continue-copy">
            <div className="continue-label">
              <span dir="rtl">تَابِعْ</span>
              Continue reading
            </div>
            <div className="continue-series">{currentReading?.seriesTitle || story.meta.source?.titleEn || "Current reading"}</div>
            <div className="continue-ar" dir="rtl">{currentReading?.ar || story.meta.arabicTitle.full}</div>
            <div className="continue-en">{currentReading?.en || story.meta.englishTitle}</div>
            <div className="continue-progress">
              <div><span style={{ width: `${Math.max(6, Math.round(progress * 100))}%` }}></span></div>
              <p>{progressLabel} · {currentLevel?.name || story.meta.level} · {currentReading?.minutes || story.meta.readingMinutes} min</p>
            </div>
          </div>
          <div className="continue-action">
            <span>→</span>
            <small>Resume</small>
          </div>
        </button>
      </section>

      <section className="home-summary">
        <button className="summary-card" onClick={onWords}>
          <div><span>Words due</span><span dir="rtl">كَلِمَاتِي</span></div>
          <strong>{buckets.due.length}</strong>
          <p>{saved.length} saved · {buckets.learning.length} learning · {buckets.mastered.length} mastered</p>
          <em>Review now →</em>
        </button>
        <button className="summary-card" onClick={onPath}>
          <div><span>Your level</span><span dir="rtl">المَسَار</span></div>
          <strong>{currentLevel?.numeral || "I"}</strong>
          <p>{currentLevel?.name || "Beginner 1"} · {activeLevelCount} levels open</p>
          <em>View full path →</em>
        </button>
        <button className="summary-card" onClick={onWords}>
          <div><span>Your streak</span><span dir="rtl">المُوَاظَبَة</span></div>
          <strong>{studyStats.streakDays || 0}</strong>
          <p>{buckets.reviewedToday} reviewed today</p>
          <em>{buckets.reviewedToday ? "Kept today ✓" : "Review to keep it →"}</em>
        </button>
      </section>

      <section className="home-section-title">
        <span dir="rtl">التَّالِي</span>
        <h2>Up <em>next</em></h2>
        <div></div>
      </section>
      <button className="upnext-row" onClick={() => onRead(nextReading)}>
        <span className="upnext-ar" dir="rtl">{nextReading?.ar || story.meta.arabicTitle.full}</span>
        <span className="upnext-copy">
          <strong>{nextReading?.en || story.meta.englishTitle}</strong>
          <small>{nextReading?.seriesTitle || "Recommended"} · {nextReading?.words || story.meta.wordCount} words</small>
        </span>
        <em>Start reading →</em>
      </button>

      <section className="home-section-title">
        <span dir="rtl">مُتُونٌ</span>
        <h2>The <em>Mutun</em> path</h2>
        <div></div>
      </section>
      <button className="mutun-horizon" type="button" onClick={onPath}>
        <div className="mutun-mark" dir="rtl">مُتُونٌ</div>
        <div>
          <span>Starts at {mutunStartLevel?.name || "Intermediate 1"}</span>
          <h3>Stories first, then Mutun.</h3>
          <p>Mutun is available as its own path once the story-reading foundation is strong enough. It stays visible without becoming the main daily workspace.</p>
        </div>
        <span className="text-btn">View full path</span>
      </button>
    </main>
  );
}

function ReviewCard({ item, revealed, onReveal, onRate }) {
  const srs = srsOf(item);
  const targetKey = wordKeyOf(item);
  const clippedContext = displayContext(item.context, targetKey);
  return (
    <article className="review-card">
      <div className="review-topline">
        <span>{item.context?.storyTitle || item.storyTitle || "Reading"}</span>
        <span>{srs.reviews || 0} reviews</span>
      </div>
      <div className="review-word" dir="rtl">{item.display}</div>
      <div className="review-context">
        {clippedContext?.text ? (
          <ContextSentence
            text={clippedContext.text}
            targetKey={targetKey}
            targetWordIndex={clippedContext.targetWordIndex}
          />
        ) : (
          <div className="ctx-ar" dir="rtl">{item.display}</div>
        )}
      </div>

      {!revealed ? (
        <button className="solid-btn review-reveal" onClick={onReveal}>Reveal</button>
      ) : (
        <div className="review-back">
          <div className="review-meaning">{item.meaning || "Meaning pending"}</div>
          {item.root && (
            <div className="review-root">
              <span>Root</span>
              <strong dir="rtl">{item.root}</strong>
            </div>
          )}
          {(clippedContext?.meaning || item.context?.sentenceMeaning) && (
            <p>{clippedContext?.meaning || item.context.sentenceMeaning}</p>
          )}
          <div className="rating-row">
            {["again", "hard", "good", "easy"].map((rating) => (
              <button key={rating} onClick={() => onRate(item.key, rating)}>
                {rating}
              </button>
            ))}
          </div>
        </div>
      )}
    </article>
  );
}

function WordListCard({ item, onOpen, onRemove }) {
  const targetKey = wordKeyOf(item);
  const clippedContext = displayContext(item.context, targetKey);
  return (
    <article className="word-list-card">
      <button className="word-list-main" onClick={() => onOpen(item)}>
        <span className="word-list-ar" dir="rtl">{item.display}</span>
        <span className="word-list-meta">
          <strong>{item.meaning || "Meaning pending"}</strong>
          <span>{item.context?.storyTitle || item.storyTitle || "Reading"}</span>
        </span>
      </button>
      {clippedContext?.text && (
        <ContextSentence
          text={clippedContext.text}
          targetKey={targetKey}
          targetWordIndex={clippedContext.targetWordIndex}
        />
      )}
      <button className="text-btn" onClick={() => onRemove(item.key)}>Remove</button>
    </article>
  );
}

function AuthPanel({
  session,
  authEmail,
  authPassword,
  authMessage,
  authBusy,
          syncStatus,
  onEmail,
  onPassword,
  onSignIn,
  onSignUp,
  onSignOut,
}) {
  return (
    <section className={"auth-panel" + (session ? " signed-in" : "")}>
      <div>
        <span className="auth-kicker">{session ? "Cloud sync" : "Local mode"}</span>
        <h3>{session ? "Your words are syncing" : "Sign in to keep words across devices"}</h3>
        <p>
          {session
            ? `Signed in as ${session.user.email}. ${syncStatus || "Saved words sync when you add, remove, or review them."}`
            : "You can keep using the prototype locally. Sign in when you want My Words to follow you."}
        </p>
        {authMessage && <div className="auth-message">{authMessage}</div>}
      </div>
      {session ? (
        <button className="text-btn" onClick={onSignOut}>Sign out</button>
      ) : (
        <form className="auth-form" onSubmit={(event) => { event.preventDefault(); onSignIn(); }}>
          <input
            type="email"
            value={authEmail}
            onChange={(event) => onEmail(event.target.value)}
            placeholder="Email"
            autoComplete="email"
          />
          <input
            type="password"
            value={authPassword}
            onChange={(event) => onPassword(event.target.value)}
            placeholder="Password"
            autoComplete="current-password"
          />
          <div className="auth-actions">
            <button className="solid-btn" type="submit" disabled={authBusy}>
              {authBusy ? "Working..." : "Sign in"}
            </button>
            <button className="text-btn" type="button" onClick={onSignUp} disabled={authBusy}>
              Create account
            </button>
          </div>
        </form>
      )}
    </section>
  );
}

function MyWordsPage({ saved, activeTab, onTab, onRate, onOpen, onRemove, onRead, auth, studyStats }) {
  const buckets = wordStudyBuckets(saved, studyStats);
  const due = buckets.due;
  const groups = groupByReading(saved);
  const [revealedKey, setRevealedKey] = useState(null);
  const currentDue = due[0];
  const statusOf = (item) => {
    const srs = srsOf(item);
    if (isDue(item)) return "due";
    if (Number(srs.reviews || 0) >= 3 && Number(srs.intervalDays || 0) >= 14) return "mastered";
    if (Number(srs.reviews || 0) > 0) return "learning";
    return "saved";
  };

  useEffect(() => {
    setRevealedKey(null);
  }, [currentDue?.key, activeTab]);

  return (
    <main className="page app-page words-page">
      <section className="words-hero">
        <div className="eyebrow">
          <span className="ar-mark" dir="rtl">كَلِمَاتِي</span>
          <span>Words</span>
        </div>
        <h1>The words you <em>chose to keep.</em></h1>
        <p>Review what is due, then return to saved words by the reading where you met them.</p>
      </section>

      <section className="word-stat-strip">
        <div className="due"><strong>{due.length}</strong><span>Due today</span><small dir="rtl">لِلْمُرَاجَعَةِ</small></div>
        <div><strong>{buckets.reviewedToday}</strong><span>Reviewed today</span><small dir="rtl">اليَوْمَ</small></div>
        <div><strong>{buckets.learning.length}</strong><span>Learning</span><small dir="rtl">أَتَعَلَّمُ</small></div>
        <div><strong>{buckets.mastered.length}</strong><span>Mastered</span><small dir="rtl">أَتْقَنْتُ</small></div>
        <div><strong>{studyStats.streakDays || 0}</strong><span>Day streak</span><small dir="rtl">مُوَاظَبَةٌ</small></div>
      </section>

      {saved.length === 0 ? (
        <EmptyWords onRead={onRead} />
      ) : (
        <>
          <section className="word-review-section">
            <div className="review-section-head">
              <div>
                <span dir="rtl">المُرَاجَعَةُ</span>
                <h2>Review due words</h2>
              </div>
              <strong>{currentDue ? `1 of ${due.length}` : "Session clear"}</strong>
            </div>
            {due.length > 0 && (
              <div className="deck-progress">
                {due.slice(0, 12).map((item, index) => (
                  <span key={item.key} className={index === 0 ? "cur" : ""}></span>
                ))}
              </div>
            )}
            {currentDue ? (
              <ReviewCard
                item={currentDue}
                revealed={revealedKey === currentDue.key}
                onReveal={() => setRevealedKey(currentDue.key)}
                onRate={onRate}
              />
            ) : (
              <div className="deck-done">
                <div dir="rtl">أَحْسَنْتَ</div>
                <h3>All caught up.</h3>
                <p>Your saved words are resting until their next review.</p>
              </div>
            )}
          </section>

          <section className="saved-words-section">
            <div className="saved-words-head">
              <span dir="rtl">كَلِمَاتِي</span>
              <h2>Your words, <em>by reading</em></h2>
            </div>
            <p className="saved-words-intro">Every saved word stays beside the story it came from. Tap any word to open its context.</p>

            <div className="word-legend">
              <span><i className="due"></i>Due</span>
              <span><i className="learning"></i>Learning</span>
              <span><i className="mastered"></i>Mastered</span>
            </div>

            <div className="saved-word-groups">
              {Object.entries(groups).map(([title, items]) => (
                <section className="saved-word-group" key={title}>
                  <div className="saved-word-group-head">
                    <h3>{title}</h3>
                    <span>{items.length} saved · {items.filter((item) => statusOf(item) === "due").length} due</span>
                  </div>
                  <div className="saved-word-grid">
                    {items.map((item) => (
                      <article className="saved-word-tile" key={item.key}>
                        <button className="saved-word-open" onClick={() => onOpen(item)}>
                          <i className={statusOf(item)}></i>
                          <span className="ar" dir="rtl">{item.display}</span>
                          <span className="en">{item.meaning || "Meaning pending"}</span>
                        </button>
                        <button
                          className="saved-word-remove"
                          onClick={() => onRemove(item.key)}
                          aria-label={`Remove ${item.display}`}
                        >
                          Remove
                        </button>
                      </article>
                    ))}
                  </div>
                </section>
              ))}
            </div>
          </section>
        </>
      )}
    </main>
  );
}

function ContactPage() {
  return (
    <main className="page app-page info-page">
      <PageHero
        eyebrow="Contact"
        title={<>Email <em>Ibarah.</em></>}
        subtitle="Corrections, reading requests, bugs, and useful ideas."
      />
      <section className="contact-panel">
        <a href="mailto:guled@buildbothworlds.com">guled@buildbothworlds.com</a>
        <p>If you are reporting a word issue, include the reading title and the Arabic word you clicked.</p>
      </section>
    </main>
  );
}

function AboutPage() {
  return (
    <main className="page app-page info-page">
      <PageHero
        eyebrow="About"
        title={<>Read more Arabic. <em>With less friction.</em></>}
        subtitle="Ibarah is a graded reading library for learners who need steady exposure, not another wall between them and the text."
      />
      <section className="about-lede">
        <p>
          Most learners do not need to stop at every line. They need enough understandable Arabic to keep going.
        </p>
      </section>

      <section className="about-points" aria-label="Why extensive reading helps">
        <div>
          <strong>Easy pages build speed.</strong>
          <span>When the text is mostly clear, reading becomes practice instead of survival.</span>
        </div>
        <div>
          <strong>Words need repeated meetings.</strong>
          <span>Vocabulary sticks when it appears again and again inside real sentences.</span>
        </div>
        <div>
          <strong>Grammar becomes familiar.</strong>
          <span>Patterns start to feel natural when the learner sees them in context many times.</span>
        </div>
      </section>

      <section className="about-split">
        <div>
          <span>The gap</span>
          <h2>Arabic has too little in the middle.</h2>
        </div>
        <p>
          Learners often jump from textbook sentences to texts that are too hard to read in volume. There are grammar books, vocabulary lists, and advanced works, but far fewer graded pages that feel readable, enjoyable, and worth returning to.
        </p>
      </section>

      <section className="about-build">
        <div>
          <span>What Ibarah is building</span>
          <h2>The missing bridge between lessons and the books you came for.</h2>
        </div>
        <ul>
          <li>Graded Arabic readings that can be finished.</li>
          <li>Gentle meanings and context when support is needed.</li>
          <li>Saved words that return in study, not just a list.</li>
          <li>A clear path from stories to more serious texts.</li>
        </ul>
      </section>
    </main>
  );
}

function readingLabel(reading) {
  if (isMutunReading(reading)) return { label: "Mutun", tone: "text" };
  if (reading.kind === "classic" && reading.seriesTitle) {
    return {
      label: `Book · ${reading.seriesTitle}`,
      tone: "classic",
    };
  }
  if (reading.kind === "series") {
    return {
      label: `Series · ${reading.seriesTitle || SERIES[reading.series]?.en || "Series"}`,
      tone: "series",
    };
  }
  if (reading.kind === "classic") return { label: "Book Story", tone: "classic" };
  if (reading.kind === "glimpse") return { label: "First glimpse", tone: "glimpse" };
  if (reading.kind === "text") return { label: "Mutun", tone: "text" };
  return { label: "Story", tone: "story" };
}

function FilterChip({ active, onClick, label, count }) {
  return (
    <button className={"lib-chip" + (active ? " active" : "")} onClick={onClick}>
      <span>{label}</span>
      {count != null && <span className="count">{count}</span>}
    </button>
  );
}

function canOpenReading(reading) {
  return Boolean(reading?.current || reading?.status === "Available");
}

function sortReadings(readings, levelOrder = new Map()) {
  return [...readings].sort((a, b) => {
    const levelDiff = (levelOrder.get(a.level) ?? 999) - (levelOrder.get(b.level) ?? 999);
    if (levelDiff) return levelDiff;
    const openDiff = Number(canOpenReading(b)) - Number(canOpenReading(a));
    if (openDiff) return openDiff;
    const seriesDiff = (a.series || "").localeCompare(b.series || "");
    if (seriesDiff) return seriesDiff;
    return (a.seriesIndex || 999) - (b.seriesIndex || 999) || (a.en || "").localeCompare(b.en || "");
  });
}

function readingSearchText(reading, seriesMap) {
  return [
    reading.en,
    reading.ar,
    reading.seriesTitle || seriesMap?.[reading.series]?.en,
    reading.seriesTitleAr || seriesMap?.[reading.series]?.ar,
    reading.sourceTitleAr,
    reading.sourceTitleEn,
    reading.sourceAuthorAr,
    reading.sourceAuthorEn,
  ].filter(Boolean).join(" ").toLowerCase();
}

function buildSeriesGroups(readings, levels, seriesMap, showPlanned) {
  const levelOrder = new Map(levels.map((level, index) => [level.code, index]));
  const levelByCode = new Map(levels.map((level) => [level.code, level]));
  const groups = new Map();

  readings.forEach((reading) => {
    const id = reading.series
      || (reading.sourceCollectionId ? `source:${reading.sourceCollectionId}` : `single:${reading.level}`);
    const isSourceGroup = !reading.series && reading.sourceCollectionId;
    const isSingleGroup = !reading.series && !reading.sourceCollectionId;
    const existing = groups.get(id);
    if (!existing) {
      groups.set(id, {
        id,
        title: reading.seriesTitle
          || seriesMap?.[reading.series]?.en
          || reading.sourceTitleEn
          || (isSingleGroup ? "Single Readings" : reading.en),
        titleAr: reading.seriesTitleAr
          || seriesMap?.[reading.series]?.ar
          || reading.sourceTitleAr
          || reading.ar,
        kind: reading.kind === "classic" || isSourceGroup ? "classic" : "series",
        sourceTitleAr: reading.sourceTitleAr,
        sourceAuthorAr: reading.sourceAuthorAr,
        readings: [],
      });
    }
    groups.get(id).readings.push(reading);
  });

  return [...groups.values()].map((group) => {
    const sorted = sortReadings(group.readings, levelOrder);
    const visible = showPlanned ? sorted : sorted.filter(canOpenReading);
    const available = sorted.filter(canOpenReading);
    const levelNames = [...new Set(sorted.map((reading) => levelByCode.get(reading.level)?.name).filter(Boolean))];
    const isMutun = sorted.some(isMutunReading);
    return {
      ...group,
      isMutun,
      readings: visible,
      allReadings: sorted,
      availableCount: available.length,
      plannedCount: sorted.length - available.length,
      totalCount: sorted.length,
      levelNames,
      firstAvailable: available[0] || null,
      sample: sorted[0] || null,
    };
  }).filter((group) => group.totalCount > 0)
    .sort((a, b) => {
      const openDiff = b.availableCount - a.availableCount;
      if (openDiff) return openDiff;
      return a.title.localeCompare(b.title);
    });
}

function ReadingCard({ reading, onRead, view = "grid" }) {
  const canOpen = canOpenReading(reading);
  const label = readingLabel(reading);
  const statusClass = reading.status.toLowerCase().replace(/\s+/g, "-");
  const openReading = () => onRead(reading);

  if (view === "list") {
    return (
      <article className={"read-row" + (!canOpen ? " locked" : "")}>
        <div className="row-ar" dir="rtl">{reading.ar}</div>
        <div className="row-copy">
          <h3>{reading.en}</h3>
          {reading.series && (
            <span className="row-series">
              {reading.seriesTitle || SERIES[reading.series]?.en} · Part {reading.seriesIndex}
            </span>
          )}
          {reading.kind === "classic" && reading.sourceTitleAr && (
            <span className="row-source">
              <span>Source</span>
              <strong dir="rtl">{reading.sourceTitleAr}</strong>
            </span>
          )}
        </div>
        <div className={"row-meta " + label.tone}>
          <span className="path-dot"></span>
          <span>{label.label}</span>
        </div>
        <div className={"status-pill " + statusClass}>{reading.status}</div>
        <div className="row-cta">
          {canOpen ? <button onClick={openReading}>Open</button> : <span>In planning</span>}
        </div>
      </article>
    );
  }

  return (
    <article className={"read-card " + label.tone + (reading.current ? " current" : "") + (!canOpen ? " locked" : "")}>
      <div className="read-card-top">
        <span className={"corner " + label.tone}>
          <span className="path-dot"></span>
          {label.label}
          {reading.kind === "series" && <span> · {reading.seriesIndex}</span>}
        </span>
        <span className={"status-pill " + statusClass}>{reading.status}</span>
      </div>
      <div className="read-card-ar" dir="rtl">{reading.ar}</div>
      <h3>{reading.en}</h3>
      <p>{reading.hook}</p>
      {reading.kind === "classic" && reading.sourceTitleAr && (
        <div className="source-lines">
          <div className="source-line">
            <span>Source</span>
            <strong dir="rtl">{reading.sourceTitleAr}</strong>
          </div>
        </div>
      )}
      <div className="read-card-foot">
        {reading.words && <span>{reading.words} words</span>}
        {reading.minutes && <span>{reading.minutes} min</span>}
        {reading.progress > 0 && reading.progress < 1 && <span>{Math.round(reading.progress * 100)}% read</span>}
        {reading.progress === 1 && <span className="complete">Read</span>}
      </div>
      {canOpen ? (
        <button className="solid-btn" onClick={openReading}>Open reading</button>
      ) : (
        <span className="text-chip">In planning</span>
      )}
    </article>
  );
}

function MoreComing() {
  return (
    <div className="read-card more-coming">
      <span className="mc-mark">...</span>
      <div className="mc-line">More readings are being written for this level.</div>
      <div className="mc-sub">More pages are on the way.</div>
    </div>
  );
}

function LibraryLevel({ level, readings, onRead, view }) {
  const active = level.status === "Active";
  const seriesCount = new Set(readings.filter((reading) => reading.series).map((reading) => reading.series)).size;
  return (
    <section className={"lvl-group " + (active ? "active" : "soon")}>
      <div className={"lvl-head" + (!active ? " coming" : "")}>
        <span className="lvl-num">{level.numeral}</span>
        <h2 className="lvl-name">{level.name}</h2>
        <span className="lvl-tag">{level.stage}</span>
        <span className="lvl-meta">
          {active
            ? `${readings.length} reading${readings.length === 1 ? "" : "s"}${seriesCount ? ` · ${seriesCount} series` : ""}`
            : "Coming soon"}
        </span>
      </div>
      <p className="lvl-note">{level.libraryNote}</p>

      {view === "grid" ? (
        <div className="lib-grid">
          {readings.length > 0 ? (
            readings.map((reading) => (
              <ReadingCard key={reading.id} reading={reading} onRead={onRead} view={view} />
            ))
          ) : (
            <div className="coming-panel">Readings are being prepared for this level.</div>
          )}
          {active && readings.length > 0 && <MoreComing />}
        </div>
      ) : (
        <div className="lib-list">
          {readings.map((reading) => (
            <ReadingCard key={reading.id} reading={reading} onRead={onRead} view={view} />
          ))}
        </div>
      )}
    </section>
  );
}

function SeriesCard({ group, onOpen, onRead, compact = false }) {
  const statusText = group.availableCount > 0
    ? `${group.availableCount} reading${group.availableCount === 1 ? "" : "s"}${group.plannedCount ? ` · ${group.plannedCount} planned` : ""}`
    : "Coming soon";
  const levelText = group.levelNames.length > 2
    ? `${group.levelNames[0]} to ${group.levelNames[group.levelNames.length - 1]}`
    : group.levelNames.join(" · ");
  const sample = group.firstAvailable || group.sample;

  return (
    <article className={"series-card " + group.kind + (group.isMutun ? " mutun" : "") + (compact ? " compact" : "")}>
      <div className="series-card-top">
        <span>{group.isMutun ? "Mutun" : group.kind === "classic" ? "Book series" : "Story series"}</span>
        <span>{statusText}</span>
      </div>
      <div className="series-ar" dir="rtl">{group.titleAr}</div>
      <h3>{group.title}</h3>
      <p>{levelText || "Prepared across the path"}</p>
      {sample && (
        <div className="series-next">
          <span>{group.availableCount > 0 ? "Next readable page" : "First planned page"}</span>
          <strong dir="rtl">{sample.ar}</strong>
          <em>{sample.en}</em>
        </div>
      )}
      {group.sourceTitleAr && (
        <div className="series-source">
          <span>Source</span>
          <strong dir="rtl">{group.sourceTitleAr}</strong>
        </div>
      )}
      <div className="series-actions">
        <button className="text-btn" onClick={() => onOpen(group.id)}>
          {group.isMutun ? "View Mutun" : "View series"}
        </button>
        {group.firstAvailable && (
          <button className="solid-btn" onClick={() => onRead(group.firstAvailable)}>
            Start reading
          </button>
        )}
      </div>
    </article>
  );
}

function LevelShelfCard({ level, readings, onOpen }) {
  const availableCount = readings.filter(canOpenReading).length;
  const seriesCount = new Set(readings.filter((reading) => reading.series).map((reading) => reading.series)).size;
  const status = level.status === "Active"
    ? `${availableCount} reading${availableCount === 1 ? "" : "s"}${seriesCount ? ` · ${seriesCount} series` : ""}`
    : "Coming soon";

  return (
    <article
      className={"level-shelf-card " + (level.status === "Active" ? "active" : "soon")}
      onClick={() => onOpen(level.code)}
      onKeyDown={(event) => {
        if (event.key === "Enter" || event.key === " ") {
          event.preventDefault();
          onOpen(level.code);
        }
      }}
      role="button"
      tabIndex={0}
      aria-label={`Open ${level.name}`}
    >
      <div className="level-shelf-top">
        <span>{level.numeral}</span>
        <span>{level.stage}</span>
      </div>
      <h3>{level.name}</h3>
      <p>{level.libraryNote}</p>
      <div className="level-shelf-foot">
        <span>{status}</span>
        <button
          className="text-btn"
          onClick={(event) => {
            event.stopPropagation();
            onOpen(level.code);
          }}
          onKeyDown={(event) => event.stopPropagation()}
        >
          Open level
        </button>
      </div>
    </article>
  );
}

function LibraryDetailHeader({ label, title, subtitle, onBack, children }) {
  return (
    <section className="library-detail-head">
      <button className="back-chip" onClick={onBack}>Back to library</button>
      <div>
        <span>{label}</span>
        <h2>{title}</h2>
        {subtitle && <p>{subtitle}</p>}
      </div>
      {children}
    </section>
  );
}

function LibrarySeriesDetail({ group, onBack, onRead }) {
  if (!group) return null;
  return (
    <div className="library-detail">
      <LibraryDetailHeader
        label={group.isMutun ? "Mutun" : group.kind === "classic" ? "Book series" : "Story series"}
        title={group.title}
        subtitle={`${group.availableCount} reading${group.availableCount === 1 ? "" : "s"} ready out of ${group.totalCount}`}
        onBack={onBack}
      >
        <div className="detail-ar" dir="rtl">{group.titleAr}</div>
      </LibraryDetailHeader>

      {group.readings.length === 0 ? (
        <div className="lib-empty">This series is planned, but no readable pages are open yet.</div>
      ) : (
        <div className="lib-list">
          {group.readings.map((reading) => (
            <ReadingCard key={reading.id} reading={reading} onRead={onRead} view="list" />
          ))}
        </div>
      )}
    </div>
  );
}

function LibraryLevelDetail({ level, readings, totalReadings, onBack, onRead }) {
  if (!level) return null;
  const availableCount = readings.filter(canOpenReading).length;
  const totalCount = totalReadings?.length ?? readings.length;
  return (
    <div className="library-detail">
      <LibraryDetailHeader
        label="Level"
        title={level.name}
        subtitle={`${availableCount} reading${availableCount === 1 ? "" : "s"} ready out of ${totalCount}`}
        onBack={onBack}
      >
        <div className="detail-ar">{level.stage}</div>
      </LibraryDetailHeader>

      {readings.length === 0 ? (
        <div className="lib-empty" dir="rtl">لا شَيْءَ هُنَا بَعْدُ</div>
      ) : (
        <div className="lib-list">
          {readings.map((reading) => (
            <ReadingCard key={reading.id} reading={reading} onRead={onRead} view="list" />
          ))}
        </div>
      )}
    </div>
  );
}

function LibraryPage({
  story,
  savedCount,
  onRead,
  onWords,
  productData,
  initialLevelCode,
  onInitialLevelHandled,
}) {
  const [section, setSection] = useState("home");
  const [selectedSeriesId, setSelectedSeriesId] = useState(null);
  const [selectedLevelCode, setSelectedLevelCode] = useState(null);
  const [detailReturnSection, setDetailReturnSection] = useState("home");
  const [query, setQuery] = useState("");
  const [showPlanned, setShowPlanned] = useState(false);
  const libraryHistoryId = useRef(0);
  const detailReturnSectionRef = useRef("home");
  const levels = productData?.levels || LEVELS;
  const levelCodes = new Set(levels.map((level) => level.code));
  const readings = (productData?.readings || LIBRARY_READINGS).filter((reading) => (
    reading.status !== "Coming soon" && levelCodes.has(reading.level)
  ));
  const seriesMap = productData?.series || SERIES;
  const levelOrder = useMemo(() => new Map(levels.map((level, index) => [level.code, index])), [levels]);
  const visibleReadings = useMemo(() => (
    sortReadings(showPlanned ? readings : readings.filter(canOpenReading), levelOrder)
  ), [readings, showPlanned, levelOrder]);
  const seriesGroups = useMemo(() => (
    buildSeriesGroups(readings, levels, seriesMap, showPlanned)
  ), [readings, levels, seriesMap, showPlanned]);
  const selectedSeries = seriesGroups.find((group) => group.id === selectedSeriesId);
  const selectedLevel = levels.find((level) => level.code === selectedLevelCode);
  const selectedLevelTotalReadings = useMemo(() => (
    sortReadings(readings.filter((reading) => reading.level === selectedLevelCode), levelOrder)
  ), [readings, selectedLevelCode, levelOrder]);
  const selectedLevelReadings = useMemo(() => (
    sortReadings(visibleReadings.filter((reading) => reading.level === selectedLevelCode), levelOrder)
  ), [visibleReadings, selectedLevelCode, levelOrder]);

  const totals = useMemo(() => {
    const available = readings.filter(canOpenReading);
    const mutun = available.filter((reading) => reading.shelf === "mutun");
    const stories = available.filter((reading) => reading.shelf !== "mutun");
    return {
      all: readings.length,
      series: readings.filter((reading) => reading.series).length,
      available: available.length,
      stories: stories.length,
      mutun: mutun.length,
    };
  }, [readings]);

  const filtered = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return [];
    return sortReadings(readings.filter((reading) => {
      if (!showPlanned && !canOpenReading(reading)) return false;
      return readingSearchText(reading, seriesMap).includes(q);
    }), levelOrder);
  }, [query, readings, seriesMap, showPlanned, levelOrder]);

  const resetLibrary = (nextSection = "home") => {
    setSection(nextSection);
    setSelectedSeriesId(null);
    setSelectedLevelCode(null);
  };

  const pushLibraryDetailState = () => {
    if (getInitialView() !== "library") return;
    if (selectedSeriesId || selectedLevelCode || query.trim()) return;
    libraryHistoryId.current += 1;
    window.history.pushState(
      { tmarLibraryDetail: libraryHistoryId.current },
      "",
      window.location.href,
    );
  };

  const rememberDetailReturn = (returnSection) => {
    detailReturnSectionRef.current = returnSection;
    setDetailReturnSection(returnSection);
  };

  const openSeries = (seriesId, returnSection = section === "series" ? "series" : "home") => {
    pushLibraryDetailState();
    rememberDetailReturn(returnSection);
    setSelectedSeriesId(seriesId);
    setSelectedLevelCode(null);
    setSection("series");
  };

  const openLevel = (levelCode, returnSection = section === "levels" ? "levels" : "home") => {
    pushLibraryDetailState();
    rememberDetailReturn(returnSection);
    setSelectedLevelCode(levelCode);
    setSelectedSeriesId(null);
    setSection("levels");
  };

  useEffect(() => {
    const onPopState = () => {
      if (getInitialView() !== "library") return;
      if (selectedLevelCode) {
        resetLibrary(detailReturnSectionRef.current);
      } else if (selectedSeriesId) {
        resetLibrary(detailReturnSectionRef.current);
      } else if (query.trim()) {
        setQuery("");
      }
    };
    window.addEventListener("popstate", onPopState);
    return () => window.removeEventListener("popstate", onPopState);
  }, [selectedLevelCode, selectedSeriesId, query, detailReturnSection]);

  useEffect(() => {
    if (!initialLevelCode) return;
    openLevel(initialLevelCode, "levels");
    onInitialLevelHandled?.();
  }, [initialLevelCode, onInitialLevelHandled]);

  const levelCards = levels.map((level) => ({
    level,
    readings: readings.filter((reading) => reading.level === level.code),
  }));

  return (
    <main className="page app-page">
      <PageHero
        eyebrow="Library"
        title={<>Choose a path, then open <em>the right reading.</em></>}
        subtitle="Stories build the reading base. Mutun starts at Intermediate 1 and stays in its own lane."
      />

      <div className="lib-toolbar">
        <div className="lib-filters">
          <FilterChip active={section === "home" && !selectedSeriesId && !selectedLevelCode} onClick={() => resetLibrary("home")} label="Home" />
          <FilterChip active={section === "series" && !selectedSeriesId} onClick={() => resetLibrary("series")} label="Series" count={seriesGroups.length} />
          <FilterChip active={section === "levels" && !selectedLevelCode} onClick={() => resetLibrary("levels")} label="Levels" count={levels.length} />
          <FilterChip active={section === "all"} onClick={() => resetLibrary("all")} label="All readings" count={showPlanned ? totals.all : totals.available} />
        </div>
        <label className="lib-search">
          <span aria-hidden="true">⌕</span>
          <input
            value={query}
            onChange={(event) => setQuery(event.target.value)}
            placeholder="Search a title"
          />
        </label>
        <button
          className={"lib-chip planned-toggle" + (showPlanned ? " active" : "")}
          onClick={() => setShowPlanned((value) => !value)}
        >
          {showPlanned ? "Showing planned" : "Available only"}
        </button>
      </div>

      {query.trim() ? (
        <section className="library-detail">
          <LibraryDetailHeader
            label="Search"
            title={`${filtered.length} result${filtered.length === 1 ? "" : "s"}`}
            subtitle="Search checks Arabic titles, English titles, series, and source names."
            onBack={() => setQuery("")}
          />
          {filtered.length === 0 ? (
            <div className="lib-empty" dir="rtl">لا شَيْءَ هُنَا بَعْدُ</div>
          ) : (
            <div className="lib-list">
              {filtered.map((reading) => (
                <ReadingCard key={reading.id} reading={reading} onRead={onRead} view="list" />
              ))}
            </div>
          )}
        </section>
      ) : selectedSeries ? (
        <LibrarySeriesDetail
          group={selectedSeries}
          onBack={() => resetLibrary(detailReturnSection)}
          onRead={onRead}
        />
      ) : selectedLevel ? (
        <LibraryLevelDetail
          level={selectedLevel}
          readings={selectedLevelReadings}
          totalReadings={selectedLevelTotalReadings}
          onBack={() => resetLibrary(detailReturnSection)}
          onRead={onRead}
        />
      ) : section === "series" ? (
        <section className="library-section-block">
          <div className="library-section-head">
            <div>
              <span>Series</span>
              <h2>Stay with one thread.</h2>
            </div>
          </div>
          <div className="series-grid">
            {seriesGroups.map((group) => (
              <SeriesCard key={group.id} group={group} onOpen={openSeries} onRead={onRead} />
            ))}
          </div>
        </section>
      ) : section === "levels" ? (
        <section className="library-section-block">
          <div className="library-section-head">
            <div>
              <span>Levels</span>
              <h2>Open the shelf that fits today.</h2>
            </div>
          </div>
          <div className="level-shelf-grid">
            {levelCards.map(({ level, readings: levelReadings }) => (
              <LevelShelfCard key={level.code} level={level} readings={levelReadings} onOpen={openLevel} />
            ))}
          </div>
        </section>
      ) : section === "all" ? (
        <section className="library-detail">
          <LibraryDetailHeader
            label="All readings"
            title={`${visibleReadings.length} readable page${visibleReadings.length === 1 ? "" : "s"}`}
            subtitle={showPlanned ? "Planned readings are included." : "Planned readings are hidden."}
            onBack={() => resetLibrary("home")}
          />
          <div className="lib-list">
            {visibleReadings.map((reading) => (
              <ReadingCard key={reading.id} reading={reading} onRead={onRead} view="list" />
            ))}
          </div>
        </section>
      ) : (
        <PathOverview
          story={story}
          savedCount={savedCount}
          onRead={onRead}
          onWords={onWords}
          onOpenLevel={(levelCode) => openLevel(levelCode, "home")}
          productData={productData}
          showHero={false}
        />
      )}
    </main>
  );
}

function PathOverview({
  story,
  savedCount = 0,
  onRead,
  onWords = () => {},
  onOpenLevel,
  productData,
  showHero = true,
  heroEyebrow = "Path",
}) {
  const levels = productData?.levels || LEVELS;
  const activeLevelCount = levels.filter((level) => level.status === "Active").length;
  const levelCodes = new Set(levels.map((level) => level.code));
  const readings = (productData?.readings || LIBRARY_READINGS).filter((reading) => (
    reading.status !== "Coming soon" && levelCodes.has(reading.level)
  ));
  const availableReadings = readings.filter(canOpenReading);
  const storyReadings = availableReadings.filter((reading) => reading.shelf !== "mutun");
  const mutunReadings = availableReadings.filter((reading) => reading.shelf === "mutun");
  return (
    <>
      {showHero && (
        <PageHero
          eyebrow={heroEyebrow}
          title={<>From your first full sentence to <em>the books you came for.</em></>}
          subtitle="Stories build the reading base. Mutun starts at Intermediate 1 and stays in its own lane."
        />
      )}

      <section className="paths-pair">
        <article className="path-card story">
          <div className="path-top">
            <span className="path-eyebrow">Path I · Available now</span>
            <span className="path-ar" dir="rtl">قِصَصٌ</span>
          </div>
          <h2 className="path-name">Story Path</h2>
          <p className="path-desc">Build fluency through original stories you can actually finish.</p>
          <div className="path-stats">
            <div><span className="n">{storyReadings.length}</span>Story readings</div>
            <div><span className="n">10</span>Levels</div>
            <div><span className="n">{activeLevelCount}</span>Active now</div>
          </div>
        </article>

        <article
          className="path-card texts"
          onClick={() => onOpenLevel("intermediate_1")}
          onKeyDown={(event) => {
            if (event.key === "Enter" || event.key === " ") {
              event.preventDefault();
              onOpenLevel("intermediate_1");
            }
          }}
          role="button"
          tabIndex={0}
          aria-label="Open the Mutun path in the library"
        >
          <div className="path-top">
            <span className="path-eyebrow">Path II · Starts at Intermediate 1</span>
            <span className="path-ar" dir="rtl">مُتُونٌ</span>
          </div>
          <h2 className="path-name">Mutun Path</h2>
          <p className="path-desc">Read Mutun separately once the story-reading base is ready.</p>
          <div className="path-stats">
            <div><span className="n">{mutunReadings.length}</span>Readings available</div>
            <div><span className="n">Level 5</span>Starts here</div>
          </div>
        </article>
      </section>

      <section className="journey">
        <div className="journey-headline">
          <h2>The <em>launch</em> journey</h2>
          <div className="legend">
            <span><span className="swatch now"></span>Available now</span>
          </div>
        </div>

        <div className="tl-current-banner">
          <span className="stamp" dir="rtl">اِبْدَأْ هُنَا</span>
          <div>
            <div className="lbl">Start here</div>
            <div className="line">
              {story.meta.englishTitle} is the first complete reading. Save words as you read, then review them in context.
            </div>
          </div>
          <button className="solid-btn" onClick={onRead}>Open reading</button>
        </div>

        <div className="timeline">
          <div className="spine"></div>
          {levels.map((level) => {
            const levelReadings = readings.filter((reading) => reading.level === level.code);
            const stories = levelReadings.filter((reading) => reading.kind === "story" || reading.kind === "series");
            const texts = levelReadings.filter((reading) => reading.kind === "text");
            const glimpses = levelReadings.filter((reading) => reading.kind === "glimpse");
            const seriesCount = new Set(stories.filter((reading) => reading.kind === "series").map((reading) => reading.series)).size;
            const active = level.status === "Active";

            return (
              <article className={"tl-row " + (active ? "active" : "soon") + (level.unlocksTextPath ? " unlock" : "")} key={level.code}>
                <div className="tl-stage">{level.stage}</div>
                <div className="tl-node"><div className="ring"></div></div>
                <div
                  className="tl-card"
                  onClick={() => onOpenLevel(level.code)}
                  onKeyDown={(event) => {
                    if (event.key === "Enter" || event.key === " ") {
                      event.preventDefault();
                      onOpenLevel(level.code);
                    }
                  }}
                  role="button"
                  tabIndex={0}
                  aria-label={`Open ${level.name} in the library`}
                >
                  <div className="tl-head">
                    <span className="lvl-no">Level {level.numeral}</span>
                    <h3>{level.name}</h3>
                    <span className="status">{active ? "Available" : "In planning"}</span>
                  </div>
                  <p className="tl-desc">{level.outcome}</p>
                  {level.unlocksTextPath && (
                    <div className="tl-unlock">
                      <span dir="rtl">مُتُونٌ</span>
                      <strong>Mutun starts here.</strong>
                    </div>
                  )}
                  <div className="tl-tracks">
                    {stories.length > 0 && (
                      <div className="tl-track story">
                        <span className="dot"></span>
                        <span className="label">Story{seriesCount ? ` · ${seriesCount} series` : ""}</span>
                        <span className="picks" dir="rtl">
                          {stories.slice(0, 3).map((reading) => <span key={reading.id}>{reading.ar}</span>)}
                        </span>
                      </div>
                    )}
                    {glimpses.length > 0 && (
                      <div className="tl-track glimpse">
                        <span className="dot"></span>
                        <span className="label">First glimpse</span>
                        <span className="picks" dir="rtl">
                          {glimpses.map((reading) => <span key={reading.id}>{reading.ar}</span>)}
                        </span>
                      </div>
                    )}
                    {texts.length > 0 && (
                      <div className="tl-track text">
                        <span className="dot"></span>
                        <span className="label">Mutun Path</span>
                        <span className="picks" dir="rtl">
                          {texts.slice(0, 3).map((reading) => <span key={reading.id}>{reading.ar}</span>)}
                        </span>
                      </div>
                    )}
                  </div>
                  {level.code === "beginner_1" && (
                    <div className="journey-actions">
                      <button
                        className="text-btn"
                        onClick={(event) => {
                          event.stopPropagation();
                          onWords();
                        }}
                        onKeyDown={(event) => event.stopPropagation()}
                      >
                        {savedCount} saved words
                      </button>
                    </div>
                  )}
                </div>
              </article>
            );
          })}
        </div>
      </section>
    </>
  );
}

function PathPage(props) {
  return (
    <main className="page app-page">
      <PathOverview {...props} />
    </main>
  );
}

function AppFooter({ onView }) {
  return (
    <footer className="site-footer">
      <div className="footer-brand">
        <img src="uploads/ibarah-logo-transparent.png" alt="" aria-hidden="true" />
        <strong>Ibarah</strong>
        <p>Arabic, one finished page at a time.</p>
      </div>
      <nav aria-label="Footer">
        <button onClick={() => onView("home")}>Home</button>
        <button onClick={() => onView("library")}>Library</button>
        <button onClick={() => onView("my-words")}>Words</button>
        <button onClick={() => onView("about")}>About</button>
        <button onClick={() => onView("contact")}>Contact</button>
      </nav>
    </footer>
  );
}

function ReadingNavCard({ direction, reading, fallback, onRead, onLibrary }) {
  const isPrevious = direction === "previous";

  if (reading) {
    const meta = [
      reading.seriesTitle,
      reading.seriesIndex ? `Part ${reading.seriesIndex}` : null,
      reading.words ? `${reading.words} words` : null,
    ].filter(Boolean).join(" · ");

    return (
      <article className={"reading-nav-card " + direction}>
        <div className="promise">
          <div className="label">{isPrevious ? "Previous reading" : "Next reading"}</div>
          <div className="promise-ar" dir="rtl">{reading.ar}</div>
          <div className="promise-en">{reading.en}</div>
        </div>
        <div className="next-actions">
          {meta && <div className="num">{meta}</div>}
          <button className={isPrevious ? "text-btn" : "solid-btn"} onClick={() => onRead(reading)}>
            {isPrevious ? "Go back" : "Open next"}
          </button>
        </div>
      </article>
    );
  }

  if (!isPrevious && fallback) {
    return (
      <article className="reading-nav-card next fallback">
        <div className="promise">
          <div className="label">{fallback.label}</div>
          {fallback.ar && <div className="promise-ar" dir="rtl">{fallback.ar}</div>}
          {fallback.en && <div className="promise-en">{fallback.en}</div>}
        </div>
        {fallback.meta && <div className="num">{fallback.meta}</div>}
      </article>
    );
  }

  if (isPrevious) {
    return <article className="reading-nav-card muted"><div className="label">Start of thread</div></article>;
  }

  return (
    <article className="reading-nav-card next end-of-series">
      <div className="promise">
        <div className="label">End of series</div>
        <div className="promise-en">You have reached the last readable page in this thread.</div>
      </div>
      <button className="text-btn" onClick={onLibrary}>Back to library</button>
    </article>
  );
}

function ReadingNextUp({ previousReading, nextReading, fallback, onRead, onLibrary }) {
  return (
    <section className="reading-nav-rail">
      <ReadingNavCard
        direction="previous"
        reading={previousReading}
        onRead={onRead}
        onLibrary={onLibrary}
      />
      <ReadingNavCard
        direction="next"
        reading={nextReading}
        fallback={fallback}
        onRead={onRead}
        onLibrary={onLibrary}
      />
    </section>
  );
}

// ============================================================
// Saved tray
// ============================================================
function SavedTray({ saved, story, onOpen, onRemove }) {
  return (
    <section className="section">
      <div className="section-eyebrow">Section II</div>
      <h2 className="section-title">My <em>Words</em></h2>
      <div className="saved-tray">
        {saved.length === 0 ? (
          <div className="empty">
            Saved words will gather here with their sentence.
          </div>
        ) : (
          saved.map((s) => {
            const v = story.vocab[lookupVocabKey(story, wordKeyOf(s))];
            const gloss = s.meaning || v?.en;
            return (
              <span className="saved-chip" key={s.key} onClick={() => onOpen(s)}>
                <span>{s.display}</span>
                {gloss && <span className="gloss" dir="ltr">{gloss}</span>}
                <span
                  className="x"
                  role="button"
                  onClick={(e) => { e.stopPropagation(); onRemove(s.key); }}
                  aria-label="Remove"
                >×</span>
              </span>
            );
          })
        )}
      </div>
    </section>
  );
}

// ============================================================
// Audio bar
// ============================================================
function AudioBar({ visible, isPlaying, onToggle, onClose, progress, time, duration }) {
  return (
    <div className={"audio-bar" + (visible ? " visible" : "")}>
      <button className="play-btn" onClick={onToggle}>
        {isPlaying ? (
          <svg width="10" height="12" viewBox="0 0 8 10" fill="currentColor"><rect width="3" height="10"/><rect x="5" width="3" height="10"/></svg>
        ) : (
          <svg width="10" height="10" viewBox="0 0 10 10" fill="currentColor"><path d="M1 0 L1 10 L9 5 Z"/></svg>
        )}
      </button>
      <div className="progress"><div style={{ width: `${progress}%` }}/></div>
      <span className="time">{time} / {duration}</span>
      <button className="close" onClick={onClose} aria-label="Close">×</button>
    </div>
  );
}

function SyncPill({ session, syncStatus }) {
  return (
    <button
      className={"sync-pill" + (session ? " synced" : "")}
      onClick={() => { window.location.hash = "my-words"; }}
      title={session ? "Saved words sync to your account" : "Saved words are local until you sign in"}
    >
      {session ? "Synced" : "Local"}
      {syncStatus && <span>{syncStatus}</span>}
    </button>
  );
}

// ============================================================
// App
// ============================================================
function App() {
  const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
    "palette": "parchment",
    "showNotes": true,
    "showQuiz": true,
    "defaultVowels": "full",
    "showVowelHint": true
  }/*EDITMODE-END*/;
  const [t] = useState(TWEAK_DEFAULTS);

  const baseStory = window.STORY;
  const [story, setStory] = useState(baseStory);
  const rootIndex = useMemo(() => buildRootIndex(story), [story]);
  const supabaseClient = useMemo(() => getSupabaseClient(), []);
  const [productData, setProductData] = useState(FALLBACK_PRODUCT_DATA);
  const [activeReadingSlug, setActiveReadingSlug] = useState("after-fajr");
  const [loadingReadingSlug, setLoadingReadingSlug] = useState(null);

  const [mode, setMode] = useState(t.defaultVowels || "full");
  const [view, setView] = useState(getInitialView);
  const [libraryLevelCode, setLibraryLevelCode] = useState(null);
  const [wordsTab, setWordsTab] = useState("due");
  const [session, setSession] = useState(null);
  const [authEmail, setAuthEmail] = useState("");
  const [authPassword, setAuthPassword] = useState("");
  const [authMessage, setAuthMessage] = useState("");
  const [authBusy, setAuthBusy] = useState(false);
  const [syncStatus, setSyncStatus] = useState("");
  const [selection, setSelection] = useState(null); // {key, display}
  const [revealedMeanings, setRevealedMeanings] = useState(() => new Set());
  const [highlightedRoot, setHighlightedRoot] = useState(null);
  const [saved, setSaved] = useState(() => {
    try {
      const s = JSON.parse(localStorage.getItem("tmar:saved") || "[]");
      return Array.isArray(s) ? s : [];
    } catch { return []; }
  });
  const displaySaved = useMemo(() => (
    saved.map((item) => hydrateSavedMeaning(item, [story, baseStory]))
  ), [saved, story, baseStory]);
  const [studyStats, setStudyStats] = useState(readStudyStats);
  const dueCount = useMemo(() => saved.filter(isDue).length, [saved]);
  const appLevelOrder = useMemo(() => (
    new Map((productData?.levels || []).map((level, index) => [level.code, index]))
  ), [productData?.levels]);
  const currentReading = useMemo(() => {
    const readings = productData?.readings || [];
    return readings.find((reading) => reading.id === activeReadingSlug)
      || readings.find((reading) => reading.id === story.meta.slug)
      || null;
  }, [productData?.readings, activeReadingSlug, story.meta.slug]);
  const loadedReading = useMemo(() => (
    currentReading?.id === story.meta.slug ? currentReading : null
  ), [currentReading, story.meta.slug]);
  const currentReadingSaved = useMemo(() => {
    return displaySaved.filter((item) => {
      const slug = readingSlugOf(item);
      return slug
        ? slug === story.meta.slug
        : (item.context?.storyTitle || item.storyTitle) === story.meta.englishTitle;
    });
  }, [displaySaved, story.meta.slug, story.meta.englishTitle]);
  const currentReadingSavedSet = useMemo(
    () => new Set(currentReadingSaved.map(wordKeyOf)),
    [currentReadingSaved],
  );
  const currentSeriesReadings = useMemo(() => {
    if (!loadedReading?.series) return null;
    return sortReadings(
      (productData?.readings || []).filter((reading) => (
        reading.series === loadedReading.series && canOpenReading(reading)
      )),
      appLevelOrder,
    );
  }, [loadedReading, productData?.readings, appLevelOrder]);
  const previousReading = useMemo(() => {
    if (!currentSeriesReadings) return null;
    const currentIndex = currentSeriesReadings.findIndex((reading) => reading.id === loadedReading.id);
    if (currentIndex <= 0) return null;
    return currentSeriesReadings[currentIndex - 1] || null;
  }, [loadedReading, currentSeriesReadings]);
  const nextReading = useMemo(() => {
    if (!currentSeriesReadings) return null;
    const currentIndex = currentSeriesReadings.findIndex((reading) => reading.id === loadedReading.id);
    if (currentIndex < 0) return null;
    return currentSeriesReadings[currentIndex + 1] || null;
  }, [loadedReading, currentSeriesReadings]);
  const recommendedReading = useMemo(() => {
    const openReadings = sortReadings((productData?.readings || []).filter(canOpenReading), appLevelOrder);
    return openReadings.find((reading) => reading.progress > 0 && reading.progress < 1)
      || openReadings.find((reading) => reading.current && (reading.progress ?? 0) < 1)
      || openReadings.find((reading) => (reading.progress ?? 0) < 1)
      || openReadings.find((reading) => reading.current)
      || openReadings[0]
      || null;
  }, [productData?.readings, appLevelOrder]);
  const requestedReading = useMemo(() => {
    const targetSlug = loadingReadingSlug || activeReadingSlug;
    return (productData?.readings || []).find((reading) => reading.id === targetSlug) || null;
  }, [productData?.readings, loadingReadingSlug, activeReadingSlug]);

  const loadCloudWords = useCallback(async (activeSession, { mergeLocal = false } = {}) => {
    if (!supabaseClient || !activeSession?.user) return;
    setSyncStatus("Loading...");
    const { data, error } = await supabaseClient
      .from("user_saved_words")
      .select("*, readings(slug,title_en,title_ar_full)")
      .eq("suspended", false)
      .order("saved_at", { ascending: true });

    if (error) {
      setSyncStatus("Sync error");
      setAuthMessage(error.message);
      return;
    }

    const cloudItems = (data || []).map(rowToSavedItem);
    setSaved((current) => {
      const merged = mergeLocal ? [...cloudItems] : cloudItems;
      if (mergeLocal) {
        const keys = new Set(merged.map((item) => item.key));
        current.forEach((item) => {
          if (!keys.has(item.key)) merged.push(item);
        });
      }
      return merged;
    });
    setSyncStatus("Synced");
  }, [supabaseClient]);

  const syncLocalWords = useCallback(async (activeSession, items) => {
    if (!supabaseClient || !activeSession?.user || items.length === 0) return;
    const rows = items
      .filter((item) => !item.remoteId)
      .map((item) => savedItemToRow(item, activeSession.user.id));
    if (rows.length === 0) return;

    setSyncStatus("Saving...");
    for (const row of rows) {
      const { data, error } = await supabaseClient
        .from("user_saved_words")
        .insert(row)
        .select()
        .single();
      if (error && error.code !== "23505") {
        setSyncStatus("Sync error");
        setAuthMessage(error.message);
        return;
      }
      if (data) {
        setSaved((current) => current.map((item) => (
          item.key === row.client_key ? { ...item, remoteId: data.id } : item
        )));
      }
    }
    setSyncStatus("Synced");
  }, [supabaseClient]);

  const goView = useCallback((nextView) => {
    setView(nextView);
    setSelection(null);
    window.location.hash = nextView;
    window.scrollTo({ top: 0, behavior: "smooth" });
  }, []);

  const openReading = useCallback((reading) => {
    const target = reading || recommendedReading || currentReading;
    if (target?.id) {
      setActiveReadingSlug(target.id);
      setLoadingReadingSlug(supabaseClient && target.id !== story.meta.slug ? target.id : null);
    }
    setSelection(null);
    setRevealedMeanings(new Set());
    goView("read");
  }, [goView, recommendedReading, currentReading, story.meta.slug, supabaseClient]);
  const handleView = useCallback((nextView) => {
    if (nextView === "read") {
      openReading();
      return;
    }
    goView(nextView);
  }, [goView, openReading]);
  const openLibraryLevel = useCallback((levelCode) => {
    setLibraryLevelCode(levelCode);
    goView("library");
  }, [goView]);
  const clearLibraryLevelRequest = useCallback(() => {
    setLibraryLevelCode(null);
  }, []);

  useEffect(() => {
    const onHash = () => setView(getInitialView());
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, []);

  useEffect(() => {
    if (!supabaseClient) return;
    let cancelled = false;

    async function loadProductData() {
      const [levelsResult, readingsResult, seriesResult, sourcesResult] = await Promise.all([
        supabaseClient
          .from("levels")
          .select("code,display_name,cefr_range,sort_order,status,stage,unlocks_text_path,cumulative_target_words,reading_length_min,reading_length_max,new_words_min,new_words_max,default_vowel_mode")
          .order("sort_order", { ascending: true }),
        supabaseClient
          .from("readings")
          .select("slug,level_code,status,path,display_shelf,origin,source_collection_id,series_id,series_order,title_ar_full,title_en,summary_en,word_count,new_word_count,published_at")
          .in("status", ["published", "planned", "coming_soon"]),
        supabaseClient
          .from("reading_series")
          .select("id,title_en,title_ar"),
        supabaseClient
          .from("source_collections")
          .select("id,title_ar,title_en,author_ar,author_en"),
      ]);

      const error = levelsResult.error || readingsResult.error || seriesResult.error || sourcesResult.error;
      if (error) throw error;
      if (!cancelled) {
        setProductData(buildProductData({
          levelRows: levelsResult.data,
          readingRows: readingsResult.data,
          seriesRows: seriesResult.data,
          sourceRows: sourcesResult.data,
        }));
      }
    }

    loadProductData().catch((error) => {
      console.warn("Using local product data fallback.", error);
      if (!cancelled) setProductData(FALLBACK_PRODUCT_DATA);
    });

    return () => { cancelled = true; };
  }, [supabaseClient]);

  useEffect(() => {
    if (!supabaseClient) return;
    let cancelled = false;

    async function loadReadingStory() {
      const readingResult = await supabaseClient
        .from("readings")
        .select("id,slug,level_code,series_order,source_order,source_collection_id,title_ar_full,title_ar_light,title_ar_none,title_en,word_count,new_word_count,audio_normal_url,audio_slow_url,lesson_ar,lesson_en,next_up_label,next_up_ar,next_up_en,next_up_meta")
        .eq("slug", activeReadingSlug)
        .single();

      if (readingResult.error) throw readingResult.error;
      const reading = readingResult.data;
      let source = null;
      if (reading.source_collection_id) {
        const sourceResult = await supabaseClient
          .from("source_collections")
          .select("title_ar,title_en,author_ar,author_en")
          .eq("id", reading.source_collection_id)
          .maybeSingle();
        if (sourceResult.error) throw sourceResult.error;
        source = sourceResult.data || null;
      }

      const loadQuestions = async () => {
        const result = await supabaseClient
          .from("reading_questions")
          .select("sort_order,prompt_en,prompt_ar,options_en,options_ar,correct_option_index,explanation_ar,explanation_en")
          .eq("reading_id", reading.id)
          .order("sort_order", { ascending: true });
        if (!result.error) return result;

        const message = result.error.message || "";
        if (!message.includes("prompt_ar") && !message.includes("options_ar")) return result;

        return supabaseClient
          .from("reading_questions")
          .select("sort_order,prompt_en,options_en,correct_option_index,explanation_ar,explanation_en")
          .eq("reading_id", reading.id)
          .order("sort_order", { ascending: true });
      };

      const [sentencesResult, questionsResult, patternsResult, glossesResult] = await Promise.all([
        supabaseClient
          .from("reading_sentences")
          .select("block_index,sentence_index,ar_full,ar_light,ar_none,meaning_en,is_pull_quote")
          .eq("reading_id", reading.id)
          .order("block_index", { ascending: true })
          .order("sentence_index", { ascending: true }),
        loadQuestions(),
        supabaseClient
          .from("reading_patterns")
          .select("sort_order,title_en,source_ar,pattern_ar,meaning_en,substitutions")
          .eq("reading_id", reading.id)
          .order("sort_order", { ascending: true }),
        supabaseClient
          .from("reading_word_occurrences")
          .select("surface_stripped,is_curated,word_forms(form_ar,form_stripped,meaning_en,learner_note,vocab_entries(lemma_stripped,root_ar,meaning_en,learner_note))")
          .eq("reading_id", reading.id),
      ]);

      const error = sentencesResult.error || questionsResult.error || patternsResult.error || glossesResult.error;
      if (error) throw error;
      if (!cancelled) {
        const nextStory = buildStoryFromRemote({
          baseStory,
          reading,
          sentences: sentencesResult.data,
          questions: questionsResult.data,
          patterns: patternsResult.data,
          levels: productData.levels,
          vocabEntries: [],
          wordForms: [],
          curatedKeys: new Set((glossesResult.data || []).filter((item) => item.is_curated).map((item) => item.surface_stripped)),
          occurrenceGlosses: glossesResult.data || [],
          source,
        });
        setStory(nextStory);
        setLoadingReadingSlug((slug) => (slug === reading.slug ? null : slug));
      }
    }

    loadReadingStory().catch((error) => {
      console.warn("Using local reading fallback.", error?.message || error);
      if (!cancelled) {
        setStory(baseStory);
        setLoadingReadingSlug(null);
      }
    });

    return () => { cancelled = true; };
  }, [supabaseClient, productData.levels, activeReadingSlug]);

  useEffect(() => {
    if (!supabaseClient) return;
    let mounted = true;

    supabaseClient.auth.getSession().then(({ data }) => {
      if (!mounted) return;
      const activeSession = data?.session || null;
      setSession(activeSession);
      if (activeSession) loadCloudWords(activeSession, { mergeLocal: true });
    });

    const { data: subscription } = supabaseClient.auth.onAuthStateChange((_event, nextSession) => {
      setSession(nextSession);
      if (nextSession) {
        loadCloudWords(nextSession, { mergeLocal: true });
      } else {
        setSyncStatus("");
      }
    });

    return () => {
      mounted = false;
      subscription?.subscription?.unsubscribe?.();
    };
  }, [supabaseClient, loadCloudWords]);

  useEffect(() => {
    try { localStorage.setItem("tmar:saved", JSON.stringify(saved)); } catch {}
  }, [saved]);
  useEffect(() => {
    try { localStorage.setItem("tmar:study-stats", JSON.stringify(studyStats)); } catch {}
  }, [studyStats]);

  useEffect(() => { setMode(t.defaultVowels || "full"); }, [t.defaultVowels]);

  // palette
  useEffect(() => {
    const r = document.documentElement;
    if (t.palette === "ink") {
      r.style.setProperty("--bg",     "oklch(0.20 0.012 60)");
      r.style.setProperty("--bg-2",   "oklch(0.235 0.014 60)");
      r.style.setProperty("--bg-deep","oklch(0.27 0.015 60)");
      r.style.setProperty("--rule",   "oklch(0.32 0.012 60)");
      r.style.setProperty("--rule-soft","oklch(0.28 0.010 60)");
      r.style.setProperty("--ink",    "oklch(0.93 0.012 78)");
      r.style.setProperty("--ink-2",  "oklch(0.84 0.012 78)");
      r.style.setProperty("--ink-mute","oklch(0.70 0.010 78)");
      r.style.setProperty("--ink-faint","oklch(0.56 0.010 78)");
      r.style.setProperty("--accent", "oklch(0.74 0.10 65)");
      r.style.setProperty("--accent-bg","oklch(0.42 0.06 60 / 0.5)");
      r.style.setProperty("--accent-bg-2","oklch(0.40 0.06 60 / 0.4)");
    } else if (t.palette === "olive") {
      r.style.setProperty("--bg",     "oklch(0.955 0.018 110)");
      r.style.setProperty("--bg-2",   "oklch(0.93 0.022 110)");
      r.style.setProperty("--rule",   "oklch(0.84 0.022 110)");
      r.style.setProperty("--rule-soft","oklch(0.89 0.018 110)");
      r.style.setProperty("--ink",    "oklch(0.22 0.020 110)");
      r.style.setProperty("--ink-2",  "oklch(0.34 0.018 110)");
      r.style.setProperty("--ink-mute","oklch(0.48 0.016 110)");
      r.style.setProperty("--ink-faint","oklch(0.62 0.012 110)");
      r.style.setProperty("--accent", "oklch(0.50 0.08 130)");
    } else {
      ["--bg","--bg-2","--bg-deep","--rule","--rule-soft","--ink","--ink-2","--ink-mute","--ink-faint","--accent","--accent-bg","--accent-bg-2"].forEach(v => r.style.removeProperty(v));
    }
  }, [t.palette]);

  // word pick
  const pick = useCallback((key, display, context) => {
    setSelection({ key, display, context });
  }, []);
  const closeWord = useCallback(() => setSelection(null), []);

  const save = async (key, display, context, vocab) => {
    const clientKey = clientKeyFor(story.meta.slug, key);
    if (saved.some((entry) => entry.key === clientKey)) return;
    const item = makeSavedItem({ key, display, context, vocab, story });
    setSaved((prev) => {
      if (prev.find((p) => p.key === clientKey)) return prev;
      return [...prev, item];
    });

    if (supabaseClient && session?.user) {
      setSyncStatus("Saving...");
      const { data, error } = await supabaseClient
        .from("user_saved_words")
        .insert(savedItemToRow(item, session.user.id))
        .select()
        .single();

      if (error && error.code !== "23505") {
        setSyncStatus("Sync error");
        setAuthMessage(error.message);
        return;
      }

      if (data) {
        setSaved((prev) => prev.map((entry) => (
          entry.key === clientKey ? { ...entry, remoteId: data.id } : entry
        )));
      }
      setSyncStatus("Synced");
    }
  };
  const unsave = async (key) => {
    const scopedKey = key.includes(":") ? key : clientKeyFor(story.meta.slug, key);
    const item = saved.find((entry) => entry.key === scopedKey)
      || saved.find((entry) => entry.key === key);
    const removeKey = item?.key || scopedKey;
    setSaved((prev) => prev.filter((p) => p.key !== removeKey));

    if (supabaseClient && session?.user && item) {
      setSyncStatus("Saving...");
      const query = supabaseClient.from("user_saved_words").delete();
      const { error } = item.remoteId
        ? await query.eq("id", item.remoteId)
        : await query.eq("client_key", removeKey);
      if (error) {
        setSyncStatus("Sync error");
        setAuthMessage(error.message);
        return;
      }
      setSyncStatus("Synced");
    }
  };

  const openSavedItem = (item) => setSelection({
    key: wordKeyOf(item),
    display: item.display,
    context: item.context,
    savedClientKey: item.key,
    savedMeaning: item.meaning,
    savedRoot: item.root,
  });

  const rateSavedWord = async (key, rating) => {
    const original = saved.find((item) => item.key === key);
    if (!original) return;
    const updated = applySrsRating(original, rating);
    const prevSrs = srsOf(original);
    const nextSrs = srsOf(updated);

    setSaved((prev) => prev.map((item) => item.key === key ? updated : item));
    setStudyStats((current) => recordStudyReview(current));

    if (supabaseClient && session?.user) {
      setSyncStatus("Saving...");
      const updateQuery = supabaseClient
        .from("user_saved_words")
        .update({
          due_at: nextSrs.dueAt,
          interval_days: nextSrs.intervalDays,
          ease: nextSrs.ease,
          reviews: nextSrs.reviews,
          lapses: nextSrs.lapses,
        });

      const { error: updateError } = updated.remoteId
        ? await updateQuery.eq("id", updated.remoteId)
        : await updateQuery.eq("client_key", key);

      if (updateError) {
        setSyncStatus("Sync error");
        setAuthMessage(updateError.message);
        return;
      }

      if (updated.remoteId) {
        await supabaseClient.from("user_word_reviews").insert({
          user_id: session.user.id,
          user_saved_word_id: updated.remoteId,
          rating,
          previous_due_at: prevSrs.dueAt,
          next_due_at: nextSrs.dueAt,
          previous_interval_days: prevSrs.intervalDays,
          next_interval_days: nextSrs.intervalDays,
        });
      }
      setSyncStatus("Synced");
    }
  };

  // related: find a token in the story whose bareKey matches the form
  const pickRelated = (formKey) => {
    // find first occurrence's raw text (to preserve diacritics)
    for (let bi = 0; bi < story.blocks.length; bi++) {
      const block = story.blocks[bi];
      for (let si = 0; si < block.sentences.length; si++) {
        const sent = block.sentences[si];
        for (const tok of tokenize(sent[mode] || sent.full)) {
          if (isWord(tok) && bareKey(tok) === formKey) {
            setSelection({
              key: formKey,
              display: tok,
              context: {
                blockIndex: bi,
                sentenceIndex: si,
                sentenceArabic: sent.full,
                sentenceDisplayArabic: sent[mode],
                sentenceMeaning: sent.en || block.en,
              },
            });
            return;
          }
        }
      }
    }
    setSelection({ key: formKey, display: formKey });
  };

  const toggleRoot = (root) => setHighlightedRoot((cur) => cur === root ? null : root);

  const toggleMeaning = useCallback((idx) => {
    setRevealedMeanings((prev) => {
      if (prev.has(idx)) return new Set();
      return new Set([idx]);
    });
  }, []);

  useEffect(() => {
    if (revealedMeanings.size === 0) return undefined;
    const closeMeaningOnOutsideClick = (event) => {
      if (event.target?.closest?.(".block-tools")) return;
      setRevealedMeanings(new Set());
    };

    document.addEventListener("pointerdown", closeMeaningOnOutsideClick);
    return () => document.removeEventListener("pointerdown", closeMeaningOnOutsideClick);
  }, [revealedMeanings.size]);

  const signIn = async () => {
    if (!supabaseClient) {
      setAuthMessage("Supabase is not configured for this prototype.");
      return;
    }
    if (!authEmail || !authPassword) {
      setAuthMessage("Enter an email and password.");
      return;
    }

    setAuthBusy(true);
    setAuthMessage("");
    const localWords = saved;
    const { data, error } = await supabaseClient.auth.signInWithPassword({
      email: authEmail,
      password: authPassword,
    });
    setAuthBusy(false);

    if (error) {
      setAuthMessage(error.message);
      return;
    }

    if (data.session) {
      setSession(data.session);
      await syncLocalWords(data.session, localWords);
      await loadCloudWords(data.session, { mergeLocal: true });
      setAuthMessage("Signed in.");
    }
  };

  const signUp = async () => {
    if (!supabaseClient) {
      setAuthMessage("Supabase is not configured for this prototype.");
      return;
    }
    if (!authEmail || !authPassword) {
      setAuthMessage("Enter an email and password.");
      return;
    }

    setAuthBusy(true);
    setAuthMessage("");
    const localWords = saved;
    const { data, error } = await supabaseClient.auth.signUp({
      email: authEmail,
      password: authPassword,
    });
    setAuthBusy(false);

    if (error) {
      setAuthMessage(error.message);
      return;
    }

    if (data.session) {
      setSession(data.session);
      await syncLocalWords(data.session, localWords);
      await loadCloudWords(data.session, { mergeLocal: true });
      setAuthMessage("Account created and synced.");
    } else {
      setAuthMessage("Account created. Check your email if confirmation is enabled.");
    }
  };

  const signOut = async () => {
    if (!supabaseClient) return;
    await supabaseClient.auth.signOut();
    setSession(null);
    setSyncStatus("");
    setAuthMessage("Signed out. Your browser still keeps local saved words.");
  };

  // mock audio
  const [audio, setAudio] = useState({ playing: false, visible: false, progress: 0 });
  useEffect(() => {
    if (!audio.playing) return;
    const id = setInterval(() => {
      setAudio((a) => {
        if (!a.playing) return a;
        const np = a.progress + 0.5;
        if (np >= 100) return { ...a, playing: false, progress: 100 };
        return { ...a, progress: np };
      });
    }, 80);
    return () => clearInterval(id);
  }, [audio.playing]);
  const onListen = () => {
    setAudio((a) => ({
      ...a,
      visible: true,
      playing: !a.playing,
      progress: a.progress >= 100 ? 0 : a.progress,
    }));
  };
  const audioTime = useMemo(() => {
    const total = 168;
    const s = Math.floor((audio.progress / 100) * total);
    return `${Math.floor(s/60)}:${String(s%60).padStart(2,"0")}`;
  }, [audio.progress]);

  // close panel on Escape
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") {
        if (selection) closeWord();
        else if (highlightedRoot) setHighlightedRoot(null);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [selection, highlightedRoot, closeWord]);

  useEffect(() => {
    if (!selection) return;
    const onPointerDown = (event) => {
      const target = event.target;
      if (target.closest?.(".word-panel") || target.closest?.(".w")) return;
      closeWord();
    };
    window.addEventListener("pointerdown", onPointerDown);
    return () => window.removeEventListener("pointerdown", onPointerDown);
  }, [selection, closeWord]);

  const wordProps = {
    story,
    rootIndex,
    selectedKey: selection?.key,
    savedSet: currentReadingSavedSet,
    onPick: pick,
    highlightedRoot,
  };
  const benefitLabel = story.meta.slug?.startsWith("juha-daahik-") ? "The Point" : "The Lesson";

  return (
    <>
      <header className="masthead">
        <div className="masthead-inner">
          <button className="wordmark" onClick={() => goView("home")} aria-label="Go to Home">
            <img className="wordmark-logo" src="uploads/ibarah-logo-header.png" alt="Ibarah" />
            <span className="wordmark-name">The Muslim <em>Arabic</em> Reader</span>
          </button>
          <ProductNav
            view={view}
            onView={handleView}
            savedCount={saved.length}
            dueCount={dueCount}
          />
          <div className="mast-right">
            <span className="pill">{story.meta.level}</span>
          </div>
        </div>
      </header>

      {view === "home" ? (
        <HomePage
          story={story}
          saved={displaySaved}
          studyStats={studyStats}
          session={session}
          syncStatus={syncStatus}
          productData={productData}
          onRead={openReading}
          onLibrary={() => goView("library")}
          onWords={() => goView("my-words")}
          onPath={() => goView("library")}
        />
      ) : view === "read" && loadingReadingSlug && loadingReadingSlug !== story.meta.slug ? (
        <ReadingLoading reading={requestedReading} />
      ) : view === "read" ? (
      <main className="page">
        {/* ---- header ---- */}
        <div className="story-header">
          <div className="eyebrow">
            <span>
              {loadedReading?.seriesTitle
                ? `${loadedReading.seriesTitle} · Chapter ${loadedReading.seriesIndex || story.meta.storyNumber}`
                : `Story ${story.meta.storyNumber}`}
            </span>
            <span className="dot"/>
            <span>{story.meta.level}</span>
            <span className="sep">·</span>
            <span>{story.meta.cefr}</span>
          </div>
          <h1 className="title-ar" dir="rtl">{story.meta.arabicTitle[mode]}</h1>
          <div className="title-en">{story.meta.englishTitle}</div>
          {story.meta.source && (
            <div className="story-source">
              <div className="story-source-item">
                <span>Source</span>
                <strong dir="rtl">{story.meta.source.titleAr}</strong>
              </div>
              {story.meta.source.authorAr && (
                <div className="story-source-item">
                  <span>Author</span>
                  <strong dir="rtl">{story.meta.source.authorAr}</strong>
                </div>
              )}
            </div>
          )}
          <div className="story-meta">
            <span><span className="meta-num">{story.meta.wordCount}</span>&nbsp;&nbsp;words</span>
            <span><span className="meta-num">~{story.meta.newWords}</span>&nbsp;&nbsp;new</span>
            <span><span className="meta-num">{story.meta.readingMinutes}</span>&nbsp;&nbsp;min read</span>
            <span><span className="meta-num">{story.meta.listenMinutes}</span>&nbsp;&nbsp;listen</span>
          </div>
        </div>

        {/* ---- controls ---- */}
        <ReadingControls
          mode={mode}
          setMode={setMode}
          onListen={onListen}
          isPlaying={audio.playing}
          duration={story.meta.listenMinutes}
        />

        {t.showVowelHint && (
          <div className={"vowel-hint" + (mode === "bare" ? " show" : "")}>
            Reading without vowels is something to grow into.
            In the early levels, full or light vowels are just as worthy.
          </div>
        )}

        {/* ---- reading body ---- */}
        <div className="story-column">
          {story.blocks.map((b, bi) => (
            <StoryBlock
              key={bi}
              block={b}
              idx={bi}
              mode={mode}
              revealed={revealedMeanings.has(bi)}
              onToggleMeaning={toggleMeaning}
              wordProps={wordProps}
              showBreakBefore={story.meta.slug?.startsWith("juha-daahik-") && bi > 0}
            />
          ))}
        </div>

        {/* ---- fa'idah ---- */}
        {story.benefit && (
          <section className="faida">
            <div className="label">{benefitLabel}</div>
            {story.benefit.full && <div className="ar" dir="rtl">{story.benefit.full}</div>}
            {story.benefit.english && <div className="en">{story.benefit.english}</div>}
          </section>
        )}

        {/* ---- saved words ---- */}
        <SavedTray
          saved={currentReadingSaved}
          story={story}
          onOpen={openSavedItem}
          onRemove={unsave}
        />

        {/* ---- what you noticed ---- */}
        {t.showNotes && (
          <section className="section">
            <div className="section-eyebrow">Section III</div>
            <h2 className="section-title">Patterns you <em>can use</em></h2>
            <div className={`noticed-grid cols-${Math.min(Math.max(story.notes.length, 1), 3)}`}>
              {story.notes.map((n, i) => (
                <div className="noticed-card" key={i}>
                  <div className="ar" dir="rtl">{n.arabic}</div>
                  <h4>{n.title}</h4>
                  {n.pattern && <div className="pattern-line" dir="rtl">{n.pattern}</div>}
                  <p>{n.body}</p>
                  {n.substitutions?.length > 0 && (
                    <div className="pattern-swaps" dir="rtl">
                      {n.substitutions.map((substitution) => (
                        <span key={substitution}>{substitution}</span>
                      ))}
                    </div>
                  )}
                </div>
              ))}
            </div>
          </section>
        )}

        {/* ---- quiz ---- */}
        {t.showQuiz && (
          <section className="section">
            <div className="section-eyebrow">Section IV</div>
            <h2 className="section-title">Five small <em>questions</em></h2>
            <QuizCarousel questions={story.questions}/>
          </section>
        )}

        {/* ---- next up ---- */}
        <ReadingNextUp
          previousReading={previousReading}
          nextReading={nextReading}
          fallback={story.nextUp}
          onRead={openReading}
          onLibrary={() => goView("library")}
        />
      </main>
      ) : view === "my-words" ? (
        <MyWordsPage
          saved={displaySaved}
          activeTab={wordsTab}
          onTab={setWordsTab}
          onRate={rateSavedWord}
          onOpen={openSavedItem}
          onRemove={unsave}
          onRead={() => openReading()}
          studyStats={studyStats}
          auth={{
            session,
            authEmail,
            authPassword,
            authMessage,
            authBusy,
            syncStatus,
            onEmail: setAuthEmail,
            onPassword: setAuthPassword,
            onSignIn: signIn,
            onSignUp: signUp,
            onSignOut: signOut,
          }}
        />
      ) : view === "library" ? (
        <LibraryPage
          story={story}
          savedCount={saved.length}
          productData={productData}
          onRead={openReading}
          onWords={() => goView("my-words")}
          initialLevelCode={libraryLevelCode}
          onInitialLevelHandled={clearLibraryLevelRequest}
        />
      ) : view === "about" ? (
        <AboutPage />
      ) : view === "contact" ? (
        <ContactPage />
      ) : (
        <PathPage
          story={story}
          productData={productData}
          savedCount={saved.length}
          onRead={() => openReading()}
          onWords={() => goView("my-words")}
          onOpenLevel={openLibraryLevel}
        />
      )}

      <AppFooter onView={handleView} />

      <WordPanel
        selection={selection}
        story={story}
        rootIndex={rootIndex}
        savedSet={currentReadingSavedSet}
        onClose={closeWord}
        onSave={save}
        onUnsave={unsave}
        onPickRoot={toggleRoot}
        onPickRelated={pickRelated}
        highlightedRoot={highlightedRoot}
      />

      <AudioBar
        visible={audio.visible}
        isPlaying={audio.playing}
        onToggle={() => setAudio((a) => ({ ...a, playing: !a.playing }))}
        onClose={() => setAudio({ playing: false, visible: false, progress: 0 })}
        progress={audio.progress}
        time={audioTime}
        duration={story.meta.listenMinutes}
      />

    </>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
