/* global React, Icons, Button, Badge, Progress, BrandLogo, navigate, DATA, ContentBlocks, PageSources, Rich, CourseCtx, CountUp, CourseStore, useCourseProgress, courseItems, courseProgress, nextItem, itemHref, unlockedIds, blockingItem, cx */
/* ============================================================
   Course reader — study layout for structured courses.
   Route: #/curso/<courseId>/leccion[/<itemId>]
   Left: course outline · Centre: one page of blocks, a unit test or the final exam.
   ============================================================ */

const itemLabel = it => (it.num ? `${it.num} · ${it.title}` : it.title);
const itemKind = it => (it.kind === "exam" ? "Examen final" : it.kind === "quiz" ? `Test · Unidad ${it.unit.n}` : it.unit ? `Página ${it.num}` : "Presentación");

const CourseReaderPage = ({ courseId, itemId, openNotes = false }) => {
  useCourseProgress();
  const course = DATA.COURSES.find(c => c.id === courseId);
  CourseStore.ensure(course.id);
  const [navOpen, setNavOpen] = React.useState(false);
  // ".../apuntes" (from Mi espacio) opens the notebook beside the page.
  const [notesOpen, setNotesOpen] = React.useState(openNotes);
  const [notesVisited, setNotesVisited] = React.useState(openNotes);
  const [notesMode, setNotesMode] = React.useState("default");
  const readingBeforeFull = React.useRef({ page: 0, pane: 0 });
  const { items, pct, state } = courseProgress(course);
  const enrolled = CourseStore.isEnrolled(course.id);

  const item = items.find(it => it.id === itemId) || null;
  const open = unlockedIds(course, state);
  const openKey = [...open].join();
  const locked = !!item && !open.has(item.id);
  React.useEffect(() => {
    if (!item && enrolled) navigate(itemHref(course, nextItem(course)));
  }, [itemId, enrolled]);
  React.useEffect(() => {
    if (item && enrolled) CourseStore.setLast(course.id, item.id);
    setNavOpen(false);
    window.scrollTo({ top: 0 });
    document.querySelector(".rd-main")?.scrollTo({ top: 0 });
  }, [item?.id, enrolled]);

  // Glossary drawer: opened from the bar, from any glossary card, or with the G key.
  const [glossary, setGlossary] = React.useState(null); // null = closed, string = focus term
  const [quizActive, setQuizActive] = React.useState(false); // an attempt is running: keep the student in the test
  React.useEffect(() => {
    const onOpen = e => setGlossary(e.detail || "");
    window.addEventListener("bh-glossary", onOpen);
    return () => window.removeEventListener("bh-glossary", onOpen);
  }, []);

  const idx = item ? items.indexOf(item) : -1;
  const prev = items[idx - 1];
  const next = items[idx + 1];

  // Keyboard: ← → move between pages, G opens the glossary.
  React.useEffect(() => {
    const onKey = e => {
      if (e.metaKey || e.ctrlKey || e.altKey || glossary != null || item?.kind !== "page" || (notesOpen && notesMode === "full")) return;
      if (/^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName) || e.target.isContentEditable) return;
      if (e.key === "ArrowLeft" && prev) navigate(itemHref(course, prev));
      else if (e.key === "ArrowRight" && next && open.has(next.id)) navigate(itemHref(course, next));
      else if (e.key.toLowerCase() === "g" && course.glossary) setGlossary("");
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [prev?.id, next?.id, glossary, item?.id, notesOpen, notesMode, openKey]);

  const ctx = React.useMemo(() => item && ({
    course, itemId: item.id, unitN: item.unit?.n, ids: open, // cross-links only to items already open
  }), [item?.id, openKey]);
  const changeNotes = nextOpen => {
    setNotesVisited(true);
    setNavOpen(false);
    const mobile = window.matchMedia("(max-width: 760px)").matches;
    const position = notesOpen && notesMode === "full" ? (mobile ? readingBeforeFull.current.pane : readingBeforeFull.current.page)
      : notesOpen ? document.querySelector(".rd-main")?.scrollTop || 0 : window.scrollY;
    setNotesOpen(nextOpen);
    if (mobile) requestAnimationFrame(() => {
      if (nextOpen) {
        window.scrollTo({ top: 0 });
        document.querySelector(".rd-main")?.scrollTo({ top: position });
      } else window.scrollTo({ top: position });
    });
    else if (notesOpen && notesMode === "full" && !nextOpen) requestAnimationFrame(() => window.scrollTo({ top: position }));
  };
  const resizeNotes = mode => {
    if (mode === "full" && notesMode !== "full") readingBeforeFull.current = {
      page: window.scrollY, pane: document.querySelector(".rd-main")?.scrollTop || 0,
    };
    setNavOpen(false);
    setNotesMode(mode);
    if (notesMode === "full" && mode !== "full") requestAnimationFrame(() => {
      window.scrollTo({ top: readingBeforeFull.current.page });
      document.querySelector(".rd-main")?.scrollTo({ top: readingBeforeFull.current.pane });
    });
  };

  const status = CourseStore.status(course.id);
  if (status === "error") return <div className="rd-gate"><div className="rd-quiz-state"><p>No se ha podido cargar tu curso.</p><Button variant="outline" onClick={() => CourseStore.load(course.id)}>Reintentar</Button></div></div>;
  if (!enrolled && (status === "loading" || status === "idle")) return <div className="rd-gate"><div className="rd-quiz-state"><span className="rd-spinner" /> Cargando tu curso…</div></div>;
  if (!enrolled) return <ReaderEnrollGate course={course} />;
  if (!item) return null;

  return (
    <CourseCtx.Provider value={ctx}>
    <div className="rd" data-screen-label="05 Lector del curso" data-notes-open={notesOpen} data-notes-mode={notesMode}>
      <header className="rd-bar">
        <button className="rd-bar-menu" onClick={() => setNavOpen(o => !o)} aria-label="Contenido del curso" aria-expanded={navOpen}>
          {navOpen ? <Icons.X size={18} /> : <Icons.Bars size={18} />}
        </button>
        <a href={`#/curso/${course.id}`} className="rd-bar-back"><Icons.ChevronLeft size={16} /> <span>Volver al curso</span></a>
        <div className="rd-bar-title">
          <span className="rd-bar-course">{course.title}</span>
          <span className="rd-bar-item">{item.unit ? `Unidad ${item.unit.n} · ` : ""}{itemLabel(item)}</span>
        </div>
        {course.glossary && (
          <button className="rd-bar-tool" onClick={() => setGlossary("")} title="Glosario (G)">
            <Icons.Layers size={16} /><span>Glosario</span>
          </button>
        )}
        <button className="rd-bar-tool" onClick={() => changeNotes(!notesOpen)} title="Mis apuntes" aria-label="Mis apuntes"
                aria-expanded={notesOpen} aria-controls="course-notes-panel" data-active={notesOpen}>
          <Icons.Notes size={16} /><span>Mis apuntes</span>
        </button>
        <div className="rd-bar-progress" title="Progreso del curso">
          <span>{pct}%</span>
          <Progress value={pct} />
        </div>
        <ReadingProgress key={`${item.id}:${notesOpen}`} />
      </header>

      <div className="rd-body" data-notes-open={notesOpen} data-notes-mode={notesMode}>
        <aside className="rd-nav" data-open={navOpen}>
          <ReaderOutline course={course} items={items} state={state} open={open} activeId={item.id} />
        </aside>
        <div className="rd-scrim" data-open={navOpen} onClick={() => setNavOpen(false)} />

        <main className="rd-main">
          <article className="rd-article" key={item.id} data-kind={locked ? "locked" : item.kind}>
            {locked
              ? <LockedItem course={course} item={item} />
              : item.kind === "page"
                ? <ReaderPage course={course} item={item} />
                : <QuizRunner course={course} item={item} next={next} onActive={setQuizActive} />}

            {item.kind === "page" && !locked && <PageEnd course={course} item={item} next={next} done={!!state?.done?.[item.id]} />}

            {!quizActive && <>
            <nav className="rd-pager" aria-label="Navegación entre páginas">
              {prev ? <PagerLink course={course} item={prev} dir="prev" /> : <span className="rd-pager-empty" />}
              {next ? <PagerLink course={course} item={next} dir="next" locked={!open.has(next.id)} /> : <span className="rd-pager-empty" />}
            </nav>
            <div className="rd-shortcuts" aria-hidden="true">
              <span><kbd>←</kbd><kbd>→</kbd> navegar entre páginas</span>
              {course.glossary && <span><kbd>G</kbd> abrir el glosario</span>}
            </div>
            </>}
          </article>
        </main>
        {notesVisited && <CourseNotesPanel key={`${window.CampusAuth?.user?.id}:${course.id}`}
          course={course} item={item} open={notesOpen} mode={notesMode} onModeChange={resizeNotes}
          onClose={() => { changeNotes(false); document.querySelector('[aria-controls="course-notes-panel"]')?.focus(); }} />}
      </div>
      {glossary != null && <GlossaryDrawer course={course} focus={glossary} currentId={item.id} open={open} onClose={() => setGlossary(null)} />}
    </div>
    </CourseCtx.Provider>
  );
};

/* Separate React/Tiptap runtime inside an in-flow iframe: the Studio's editor
   and the campus's React globals keep their existing dependencies and styles. */
const NotesSizeIcon = ({ mode }) => {
  const SizeIcon = mode === "default" ? Icons.NotesLayout : mode === "wide" ? Icons.NotesExpand : Icons.NotesMaximize;
  return <SizeIcon size={16} aria-hidden="true" />;
};
const CourseNotesPanel = ({ course, item, open, mode, onModeChange, onClose }) => {
  const frame = React.useRef(null);
  const context = React.useRef(null);
  const [dirty, setDirty] = React.useState(false);
  const userId = window.CampusAuth?.user?.id;
  context.current = {
    lessonId: item.id, lessonTitle: itemLabel(item),
    href: `${window.location.origin}${window.location.pathname}${itemHref(course, item)}`,
  };
  const connect = () => {
    const bridge = frame.current?.contentWindow?.CampusCourseNotes;
    if (!bridge) return;
    const guarded = async operation => {
      if (window.CampusAuth?.user?.id !== userId) throw new Error("not_authenticated");
      const result = await operation();
      if (window.CampusAuth?.user?.id !== userId) throw new Error("not_authenticated");
      return result;
    };
    bridge.connect({
      courseId: course.id, userId,
      context: () => context.current,
      onDirty: setDirty,
      api: {
        load: () => guarded(() => CourseStore.getNotebook(course.id)),
        save: (content, text, revision, lessonId) => guarded(() => CourseStore.saveNotebook(course.id, content, text, revision, lessonId)),
      },
    });
    bridge.setTheme(document.documentElement.getAttribute("data-theme"));
    bridge.setMode(mode);
  };
  React.useEffect(() => {
    const ready = event => {
      if (event.origin === window.location.origin && event.source === frame.current?.contentWindow && event.data?.type === "bh-notes-ready") connect();
    };
    window.addEventListener("message", ready);
    const theme = new MutationObserver(() => frame.current?.contentWindow?.CampusCourseNotes?.setTheme(document.documentElement.getAttribute("data-theme")));
    theme.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "style", "data-density"] });
    return () => {
      theme.disconnect();
      window.removeEventListener("message", ready);
      frame.current?.contentWindow?.CampusCourseNotes?.flush?.();
    };
  }, [course.id, userId]);
  React.useEffect(() => {
    if (!dirty) return;
    const warn = event => { event.preventDefault(); event.returnValue = ""; };
    window.addEventListener("beforeunload", warn);
    return () => window.removeEventListener("beforeunload", warn);
  }, [dirty]);
  React.useEffect(() => {
    if (!open) frame.current?.contentWindow?.CampusCourseNotes?.flush?.();
  }, [open]);
  React.useEffect(() => { frame.current?.contentWindow?.CampusCourseNotes?.setMode(mode); }, [mode]);
  return <aside id="course-notes-panel" className="rd-notes" hidden={!open} aria-label="Mis apuntes del curso">
    <header className="rd-notes-head">
      <div><span className="cb-kicker">Cuaderno del curso</span><h2>Mis apuntes</h2></div>
      <div className="rd-notes-actions">
        <div className="rd-notes-sizes" role="group" aria-label="Tamaño de los apuntes">
          {[{ value: "default", label: "Tamaño normal", title: "Vista normal · índice, lección y apuntes" },
            { value: "wide", label: "Ampliar apuntes", title: "Ampliar apuntes y ocultar el índice del curso" },
            { value: "full", label: "Página completa", title: "Apuntes a página completa" }].map(size => (
            <button key={size.value} aria-label={size.label} title={size.title} aria-pressed={mode === size.value} onClick={() => onModeChange(size.value)}>
              <NotesSizeIcon mode={size.value} />
            </button>
          ))}
        </div>
        <button className="rd-drawer-close" onClick={onClose} aria-label="Cerrar mis apuntes"><Icons.X size={17} /></button>
      </div>
    </header>
    <p className="rd-notes-course">{course.title}</p>
    <p className="rd-notes-hint"><Icons.Lock size={12} /> Solo tú puedes ver estos apuntes.</p>
    <iframe ref={frame} src="/campus-notes/index.html" title="Editor de mis apuntes del curso" onLoad={connect} />
  </aside>;
};

/* ---------- Glossary drawer: searchable, each term links to where it is explained ---------- */
const normalize = s => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
const GlossaryDrawer = ({ course, focus, currentId, open, onClose }) => {
  const [query, setQuery] = React.useState("");
  const [closing, setClosing] = React.useState(false);
  const input = React.useRef(null);
  const list = React.useRef(null);
  const close = () => { setClosing(true); setTimeout(onClose, 220); };
  React.useEffect(() => {
    if (focus) {
      const el = list.current?.querySelector(`[data-term="${CSS.escape(focus)}"]`);
      el?.scrollIntoView({ block: "center" });
    } else input.current?.focus();
    const onKey = e => { if (e.key === "Escape") close(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, []);
  const entries = [...course.glossary].sort((a, b) => a.term.localeCompare(b.term, "es"));
  const q = normalize(query.trim());
  const shown = q ? entries.filter(g => normalize(`${g.term} ${(g.aliases || []).join(" ")} ${g.def}`).includes(q)) : entries;
  let lastLetter = "";
  return (
    <div className="rd-drawer" data-closing={closing} role="dialog" aria-modal="true" aria-label="Glosario del curso">
      <div className="rd-drawer-scrim" onClick={close} />
      <aside className="rd-drawer-panel">
        <header className="rd-drawer-head">
          <div>
            <div className="cb-kicker">Glosario del curso</div>
            <h2>{course.glossary.length} conceptos clave</h2>
          </div>
          <button className="rd-drawer-close" onClick={close} aria-label="Cerrar glosario"><Icons.X size={18} /></button>
        </header>
        <label className="rd-drawer-search">
          <Icons.Search size={16} />
          <input ref={input} value={query} onChange={e => setQuery(e.target.value)} placeholder="Buscar un concepto…" />
          {query && <button onClick={() => setQuery("")} aria-label="Borrar búsqueda"><Icons.X size={14} /></button>}
        </label>
        <div className="rd-drawer-list" ref={list}>
          {shown.length === 0 && <p className="rd-drawer-empty">No hay conceptos que coincidan con «{query}».</p>}
          {shown.map(g => {
            const letter = normalize(g.term[0]).toUpperCase();
            const header = !q && letter !== lastLetter ? (lastLetter = letter) : null;
            return (
              <React.Fragment key={g.term}>
                {header && <div className="rd-drawer-letter">{header}</div>}
                <article className="rd-drawer-entry" data-term={g.term} data-focus={g.term === focus}>
                  <h3>{g.term}</h3>
                  <p>{g.def}</p>
                  {g.page && (g.page === currentId
                    ? <span className="rd-drawer-here">Se explica en esta página</span>
                    : !open.has(g.page)
                    ? <span className="rd-drawer-here rd-drawer-locked"><Icons.Lock size={12} /> Página {g.page.replace("-", ".")} · aún bloqueada</span>
                    : <a href={`#/curso/${course.id}/leccion/${g.page}`} onClick={onClose}>Página {g.page.replace("-", ".")} <Icons.ArrowRight size={13} /></a>)}
                </article>
              </React.Fragment>
            );
          })}
        </div>
      </aside>
    </div>
  );
};

/* Thin bar under the app bar: how far the reader is through the current page. */
const ReadingProgress = () => {
  const [p, setP] = React.useState(0);
  React.useEffect(() => {
    let raf = 0;
    const update = () => {
      raf = 0;
      const pane = document.querySelector('.rd[data-notes-open="true"] .rd-main');
      if (pane && getComputedStyle(pane).overflowY === "auto") {
        const max = pane.scrollHeight - pane.clientHeight;
        setP(max > 0 ? Math.min(1, pane.scrollTop / max) : 1);
        return;
      }
      const max = document.documentElement.scrollHeight - window.innerHeight;
      setP(max > 0 ? Math.min(1, window.scrollY / max) : 1);
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); };
    update();
    window.addEventListener("scroll", onScroll, { passive: true, capture: true });
    window.addEventListener("resize", onScroll);
    return () => { window.removeEventListener("scroll", onScroll, true); window.removeEventListener("resize", onScroll); cancelAnimationFrame(raf); };
  }, []);
  return <span className="rd-reading" style={{ transform: `scaleX(${p})` }} aria-hidden="true" />;
};

const PagerLink = ({ course, item, dir, locked = false }) => {
  const body = <>
    <span className="rd-pager-arrow">{locked ? <Icons.Lock size={16} /> : dir === "prev" ? <Icons.ArrowRight size={18} style={{ transform: "scaleX(-1)" }} /> : <Icons.ArrowRight size={18} />}</span>
    <span className="rd-pager-copy">
      <span className="rd-pager-dir">{dir === "prev" ? "Anterior" : "Siguiente"} · {itemKind(item)}{locked ? " · bloqueado" : ""}</span>
      <span className="rd-pager-title">{item.title}</span>
    </span>
  </>;
  return locked
    ? <span className={`rd-pager-link rd-pager-link--${dir}`} data-locked="true" aria-disabled="true" title="Se desbloquea al completar esta página">{body}</span>
    : <a className={`rd-pager-link rd-pager-link--${dir}`} href={itemHref(course, item)}>{body}</a>;
};

/* Shown instead of the content when the student opens an item that is still locked. */
const LockedItem = ({ course, item }) => {
  const blocker = blockingItem(course, item.id);
  return (
    <section className="rd-locked" aria-labelledby="rd-locked-title">
      <div className="rd-locked-icon"><Icons.Lock size={22} /></div>
      <div className="rd-kicker">{item.unit ? `Unidad ${item.unit.n} · ` : ""}{itemKind(item)}</div>
      <h1 className="rd-title" id="rd-locked-title">{item.title}</h1>
      <p className="rd-locked-text">
        Este contenido se desbloquea al completar lo anterior del curso.
        {blocker && <> Te falta {blocker.kind === "page" ? "completar" : "aprobar"} <strong>{itemLabel(blocker)}</strong>.</>}
      </p>
      {blocker && (
        <Button size="lg" iconRight={<Icons.ArrowRight size={16} />} onClick={() => navigate(itemHref(course, blocker))}>
          {blocker.kind === "page" ? "Ir a la página pendiente" : "Ir al test pendiente"}
        </Button>
      )}
    </section>
  );
};

/* End-of-page block: completes the page and moves on, with a short confirmation beat. */
const PageEnd = ({ course, item, next, done }) => {
  const [saving, setSaving] = React.useState(false);
  const complete = async () => {
    if (saving) return;
    setSaving(true);
    const saved = await CourseStore.markDone(course.id, item.id);
    setSaving(false);
    if (saved && next) navigate(itemHref(course, next));
  };
  return (
    <section className="rd-end" data-done={done || saving}>
      <div className="rd-end-icon">{done || saving ? <Icons.Check size={20} stroke={2.5} /> : <Icons.Book size={20} />}</div>
      <div className="rd-end-copy">
        <div className="rd-end-title">{saving ? "Guardando…" : done ? "Página completada" : "Has llegado al final de la página"}</div>
        <div className="rd-end-text">
          {done
            ? (next ? `Continúa con ${itemKind(next).toLowerCase()}: «${next.title}».` : "Has terminado esta sección.")
            : "Márcala como completada para avanzar en tu progreso del curso."}
          {item.unit && <UnitRemaining course={course} item={item} />}
        </div>
      </div>
      {done ? (
        <div className="rd-end-actions">
          <button className="rd-end-undo" onClick={() => CourseStore.markDone(course.id, item.id, false)}>Desmarcar</button>
          {next && <Button iconRight={<Icons.ArrowRight size={16} />} onClick={() => navigate(itemHref(course, next))}>Continuar</Button>}
        </div>
      ) : (
        <Button size="lg" className="rd-end-btn" icon={<Icons.Check size={16} stroke={2.5} />} onClick={complete} disabled={saving}>
          {next ? "Completar y continuar" : "Completar página"}
        </Button>
      )}
    </section>
  );
};

// "Te quedan ~32 min para terminar la unidad 1 (incluido el test)."
const UnitRemaining = ({ course, item }) => {
  const s = CourseStore.get(course.id);
  const unitItems = courseItems(course).filter(i => i.unit?.id === item.unit.id);
  const rest = unitItems.filter(i => i.id !== item.id && !s?.done?.[i.id]);
  if (!rest.length) return <> <strong>Es lo último que te queda de la unidad {item.unit.n}.</strong></>;
  const mins = rest.reduce((a, i) => a + i.minutes, 0);
  return <> Te quedan unos <strong>{mins} min</strong> para terminar la unidad {item.unit.n}{rest.some(i => i.kind === "quiz") ? ", test incluido" : ""}.</>;
};

/* ---------- Outline ---------- */
const ReaderOutline = ({ course, items, state, open, activeId }) => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const el = ref.current?.querySelector('[data-active="true"]');
    el?.scrollIntoView({ block: "nearest" });
  }, [activeId]);

  const groups = [];
  items.forEach(it => {
    const key = it.unit ? it.unit.id : it.kind === "exam" ? "final" : "intro";
    let g = groups.find(x => x.key === key);
    if (!g) {
      g = {
        key,
        label: it.unit ? `Unidad ${it.unit.n}` : it.kind === "exam" ? "Cierre" : "Presentación",
        title: it.unit ? it.unit.title : it.kind === "exam" ? "Evaluación final" : "Antes de empezar",
        items: [],
      };
      groups.push(g);
    }
    g.items.push(it);
  });
  return (
    <div className="rd-outline" ref={ref}>
      {groups.map(g => {
        const doneCount = g.items.filter(i => state?.done?.[i.id]).length;
        const isCurrent = g.items.some(i => i.id === activeId);
        return (
          <section key={g.key} className="rd-outline-group" data-current={isCurrent}>
            <div className="rd-outline-head">
              <span className="rd-outline-label">{g.label}</span>
              <span className="rd-outline-ring" style={{ "--p": doneCount / g.items.length }} title={`${doneCount} de ${g.items.length} completados`}>
                <svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8" /><circle cx="10" cy="10" r="8" /></svg>
                <span>{doneCount}/{g.items.length}</span>
              </span>
            </div>
            <div className="rd-outline-title">{g.title}</div>
            <ol>
              {g.items.map(it => {
                const isDone = !!state?.done?.[it.id];
                const isQuiz = it.kind !== "page";
                const isLocked = !open.has(it.id);
                return (
                  <li key={it.id}>
                    <a href={isLocked ? undefined : itemHref(course, it)} className="rd-outline-item" data-active={it.id === activeId} data-done={isDone} data-quiz={isQuiz}
                       data-locked={isLocked} aria-disabled={isLocked || undefined} title={isLocked ? "Se desbloquea al completar lo anterior" : undefined}>
                      <span className="rd-outline-dot">
                        {isDone ? <Icons.Check size={11} stroke={3} /> : isLocked ? <Icons.Lock size={10} /> : isQuiz ? <Icons.Quiz size={12} /> : null}
                      </span>
                      <span className="rd-outline-text">
                        {it.num && <span className="rd-outline-num">{it.num}</span>}
                        {it.title}
                      </span>
                      <span className="rd-outline-min">
                        {state?.marks?.[it.id] && <Icons.BookmarkFilled size={12} className="rd-outline-mark" aria-label="Guardada" />}
                        {it.minutes}′
                      </span>
                    </a>
                  </li>
                );
              })}
            </ol>
          </section>
        );
      })}
    </div>
  );
};

/* ---------- Page ---------- */
const UnitRings = () => (
  <svg className="rd-unit-rings" viewBox="0 0 400 400" aria-hidden="true">
    {[60, 100, 140, 180, 220].map((r, i) => (
      <circle key={r} cx="360" cy="360" r={r} className={`rd-unit-ring rd-unit-ring--${i % 2}`} strokeDasharray={i % 2 ? "2 8" : "40 14"} />
    ))}
  </svg>
);

const ReaderPage = ({ course, item }) => {
  const unit = item.unit;
  const isFirstOfUnit = unit && unit.pages[0].id === item.id;
  const pageIndex = unit ? unit.pages.findIndex(p => p.id === item.id) : -1;
  return (
    <>
      <header className="rd-head">
        <div className="rd-kicker">
          {unit ? <>Unidad {unit.n}<span className="rd-kicker-sep" />Página {pageIndex + 1} de {unit.pages.length}</> : "Antes de empezar"}
        </div>
        {unit && (
          <div className="rd-page-dots" aria-hidden="true">
            {unit.pages.map((p, i) => <span key={p.id} data-state={i < pageIndex ? "past" : i === pageIndex ? "active" : "next"} />)}
            <span className="rd-page-dots-test" title="Test de la unidad"><Icons.Quiz size={11} /></span>
          </div>
        )}
        <h1 className="rd-title">{item.title}</h1>
        <div className="rd-head-row">
          <div className="rd-meta">
            <span><Icons.Clock size={14} /> {item.minutes} min de lectura</span>
            {unit && <span><Icons.Layers size={14} /> {unit.title}</span>}
          </div>
          <BookmarkButton course={course} item={item} />
        </div>
      </header>

      {isFirstOfUnit && (
        <section className="rd-unit-card">
          <UnitRings />
          <div className="rd-unit-card-num">{String(unit.n).padStart(2, "0")}</div>
          <div className="rd-unit-card-body">
            <div className="rd-unit-card-label">Pregunta vertebradora</div>
            <p className="rd-unit-card-q">{unit.question}</p>
            <div className="rd-unit-card-grid">
              <div><span>Al terminar podrás</span>{unit.outcomes}</div>
              <div><span>Dedicación</span>{unit.time} · {unit.pages.length} páginas y test</div>
            </div>
          </div>
        </section>
      )}

      <ContentBlocks blocks={item.blocks} />
      <PageSources blocks={item.blocks} />
    </>
  );
};

const BookmarkButton = ({ course, item }) => {
  const marked = CourseStore.isMarked(course.id, item.id);
  const [saving, setSaving] = React.useState(false);
  const toggle = async () => {
    setSaving(true);
    try { await CourseStore.toggleMark(course.id, item.id); }
    finally { setSaving(false); }
  };
  return (
    <button className="rd-mark" data-on={marked} onClick={toggle} disabled={saving}
            aria-pressed={marked} title={marked ? "Quitar de guardadas" : "Guardar para repasar"}>
      {marked ? <Icons.BookmarkFilled size={15} /> : <Icons.Bookmark size={15} />}
      <span>{marked ? "Guardada" : "Guardar para repasar"}</span>
    </button>
  );
};

/* ---------- Quiz / exam ----------
   Every screen is one "plane" sized to the viewport:
   intro (instructions + review) · run (one question) · result · review (one corrected question).
   The server owns the clock, the attempts and the grading (see course-store.jsx):
   the countdown here is only a display of the deadline the server fixed. */
const LETTERS = ["A", "B", "C", "D"];
const fmtClock = s => {
  const t = Math.max(0, Math.ceil(s));
  return `${Math.floor(t / 60)}:${String(t % 60).padStart(2, "0")}`;
};
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;

const QuizHead = ({ item, isExam, children }) => (
  <header className="rd-qz-head">
    <div>
      <div className="rd-kicker">{isExam ? "Evaluación final" : <>Unidad {item.unit.n}<span className="rd-kicker-sep" />Evaluación</>}</div>
      <h1 className="rd-qz-title">{item.title}</h1>
    </div>
    {children}
  </header>
);

const QuizStats = ({ items }) => (
  <dl className="rd-qz-stats">
    {items.map((s, i) => (
      <div key={s.label} className="rd-qz-stat" data-tone={s.tone} style={{ "--i": i }}>
        <dt>{s.icon}{s.label}</dt>
        <dd>{s.value}</dd>
      </div>
    ))}
  </dl>
);

/* Before a test: what the unit covered, with links back and progress on each page. */
const QuizPrep = ({ course, item }) => {
  const s = CourseStore.get(course.id);
  const rows = item.kind === "exam"
    ? course.units.map(u => {
        const r = s?.quiz?.[u.quiz.id];
        return { id: u.quiz.id, label: `U${u.n}`, title: u.title, done: !!s?.done?.[u.quiz.id], meta: r ? `${r.score}/${r.total}` : "Sin hacer" };
      })
    : item.unit.pages.map((p, i) => ({ id: p.id, label: `${item.unit.n}.${i + 1}`, title: p.title, done: !!s?.done?.[p.id], meta: `${p.minutes} min` }));
  const pending = rows.filter(r => !r.done).length;
  const summary = item.kind === "quiz" && item.unit.pages[item.unit.pages.length - 1].blocks.find(b => b.t === "summary");
  return (
    <section className="rd-qz-card rd-prep">
      <div className="rd-qz-card-head">
        <h2 className="rd-qz-card-title">{item.kind === "exam" ? "Tu recorrido" : "Antes de empezar, repasa"}</h2>
        <span className="rd-prep-status" data-ok={!pending}>
          {pending ? plural(pending, item.kind === "exam" ? "test pendiente" : "página pendiente", item.kind === "exam" ? "tests pendientes" : "páginas pendientes") : <><Icons.Check size={12} stroke={3} /> Todo completado</>}
        </span>
      </div>
      <ol className="rd-prep-list">
        {rows.map(r => (
          <li key={r.id}>
            <a href={`#/curso/${course.id}/leccion/${r.id}`} data-done={r.done}>
              <span className="rd-prep-dot">{r.done ? <Icons.Check size={10} stroke={3} /> : null}</span>
              <span className="rd-prep-label">{r.label}</span>
              <span className="rd-prep-title">{r.title}</span>
              <span className="rd-prep-meta">{r.meta}</span>
            </a>
          </li>
        ))}
      </ol>
      {summary && (
        <details className="rd-prep-summary">
          <summary><Icons.Sparkles size={14} /> Lo esencial de la unidad <Icons.ChevronRight size={14} className="rd-prep-chev" /></summary>
          <ul>{summary.items.map((t, i) => <li key={i}><Rich text={t} /></li>)}</ul>
        </details>
      )}
    </section>
  );
};

/* Result ring: the score arc draws in, a tick marks the pass mark and a verdict
   badge lands once the arc is complete. */
const ScoreRing = ({ score, total, passMark, passed }) => {
  const [on, setOn] = React.useState(false);
  React.useEffect(() => { const t = requestAnimationFrame(() => setOn(true)); return () => cancelAnimationFrame(t); }, []);
  const r = 52, c = 2 * Math.PI * r;
  return (
    <div className="rd-ring" data-pass={passed} data-on={on}>
      <span className="rd-ring-halo" aria-hidden="true" />
      <svg viewBox="0 0 120 120" aria-hidden="true">
        <circle cx="60" cy="60" r={r} className="rd-ring-track" />
        {score > 0 && <circle cx="60" cy="60" r={r} className="rd-ring-fill" transform="rotate(-90 60 60)"
                              strokeDasharray={c} strokeDashoffset={on ? c * (1 - score / total) : c} />}
        <line x1="60" y1="1" x2="60" y2="15" className="rd-ring-tick" transform={`rotate(${(passMark / total) * 360} 60 60)`} />
      </svg>
      <div className="rd-ring-label">
        <span className="rd-ring-score"><strong><CountUp value={String(score)} run={on} /></strong><span>/{total}</span></span>
        <span className="rd-ring-caption">aciertos</span>
      </div>
      <span className="rd-ring-badge">{passed ? <Icons.Check size={15} stroke={3} /> : <Icons.X size={15} stroke={3} />}</span>
    </div>
  );
};

// Seconds left until `deadline` (ms, server time), corrected by the server/client clock offset.
function useCountdown(deadline, offset) {
  const calc = () => (deadline ? (deadline - (Date.now() + offset)) / 1000 : null);
  const [left, setLeft] = React.useState(calc);
  React.useEffect(() => {
    setLeft(calc());
    if (!deadline) return;
    const t = setInterval(() => setLeft(calc()), 250);
    return () => clearInterval(t);
  }, [deadline, offset]);
  return left;
}

const QuizRunner = ({ course, item, next, onActive }) => {
  const total = item.questions.length;
  const isExam = item.kind === "exam";
  const noun = isExam ? "examen" : "test";
  const [qs, setQs] = React.useState(null);          // server quiz state
  const [loadError, setLoadError] = React.useState("");
  const [view, setView] = React.useState("intro");   // intro | run | result | review
  const [screen, setScreen] = React.useState(1);     // 1…N while running or reviewing
  const [answers, setAnswers] = React.useState({});
  const [saveState, setSaveState] = React.useState({}); // question id → saving | saved | error
  const [offset, setOffset] = React.useState(0);
  const [busy, setBusy] = React.useState(false);
  const [confirming, setConfirming] = React.useState(false);
  const submitted = React.useRef(false);
  const answersRef = React.useRef({});
  const saveQueue = React.useRef(Promise.resolve());
  const saves = React.useRef({});

  const adopt = s => { setQs(s); setOffset(new Date(s.server_now).getTime() - Date.now()); return s; };
  const load = () => {
    setLoadError("");
    return CourseStore.quizState(course.id, item.id)
      .then(s => {
        adopt(s);
        if (s.open_attempt) {
          answersRef.current = s.open_attempt.answers || {};
          setAnswers(answersRef.current);
        } else if (s.attempts.length) setView("result");
      })
      .catch(e => setLoadError(CourseStore.errorText(e)));
  };
  React.useEffect(() => { load(); }, [item.id]);

  const open = qs?.open_attempt || null;
  const deadline = open ? new Date(open.deadline_at).getTime() : null;
  const left = useCountdown(deadline, offset);
  const running = view === "run" && !!open;

  React.useEffect(() => { onActive?.(running || view === "review"); return () => onActive?.(false); }, [running, view]);

  // Leaving mid-attempt: warn (the clock keeps running on the server either way).
  React.useEffect(() => {
    if (!running) return;
    const warn = e => { e.preventDefault(); e.returnValue = ""; };
    window.addEventListener("beforeunload", warn);
    return () => window.removeEventListener("beforeunload", warn);
  }, [running]);

  const toTop = () => { window.scrollTo({ top: 0 }); document.querySelector(".rd-main")?.scrollTo({ top: 0 }); };
  const show = (v, s) => { setView(v); if (s) setScreen(s); toTop(); };

  const finish = async () => {
    if (!open || submitted.current) return;
    submitted.current = true;
    setBusy(true); setConfirming(false);
    try {
      adopt(await CourseStore.submitAttempt(course.id, open.id, answersRef.current));
      show("result");
    } catch (e) {
      window.dispatchEvent(new CustomEvent("bh-toast", { detail: { msg: CourseStore.errorText(e), tone: "error" } }));
      submitted.current = false;
      await load();
    } finally { setBusy(false); }
  };

  // Time is up → hand in automatically with what has been saved.
  React.useEffect(() => {
    if (!open || left == null || left > 0 || busy) return;
    const retry = setTimeout(finish, 1000);
    return () => clearTimeout(retry);
  }, [open?.id, left != null && left <= 0, busy, qs]);

  const start = async () => {
    setBusy(true);
    try {
      const s = open ? qs : adopt(await CourseStore.startAttempt(course.id, item.id));
      answersRef.current = s.open_attempt?.answers || {};
      setAnswers(answersRef.current);
      setSaveState({});
      submitted.current = false;
      show("run", 1);
    } catch (e) {
      window.dispatchEvent(new CustomEvent("bh-toast", { detail: { msg: CourseStore.errorText(e), tone: "error" } }));
      await load();
    } finally { setBusy(false); }
  };

  const choose = (qq, oi) => {
    if (!open || busy || submitted.current || left <= 0) return;
    answersRef.current = { ...answersRef.current, [qq.id]: oi };
    setAnswers(answersRef.current);
    const revision = (saves.current[qq.id] || 0) + 1;
    saves.current[qq.id] = revision;
    setSaveState(s => ({ ...s, [qq.id]: "saving" }));
    // Keep writes ordered, including rapid changes to the same question.
    saveQueue.current = saveQueue.current.then(() => CourseStore.saveAnswer(open.id, qq.id, oi))
      .then(() => {
        if (saves.current[qq.id] === revision) setSaveState(s => ({ ...s, [qq.id]: "saved" }));
      })
      .catch(e => {
        if (CourseStore.errorCode(e) === "time_expired") { finish(); return; }
        if (saves.current[qq.id] === revision) setSaveState(s => ({ ...s, [qq.id]: "error" }));
      });
  };

  // Keyboard while answering: ← → between questions, A–D or 1–4 to answer. While reviewing: ← → and Esc.
  React.useEffect(() => {
    if (!running && view !== "review") return;
    const onKey = e => {
      if (e.metaKey || e.ctrlKey || e.altKey || /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(e.target.tagName) || e.target.isContentEditable) return;
      if (view === "review") {
        if (e.key === "ArrowRight" && screen < total) setScreen(screen + 1);
        else if (e.key === "ArrowLeft" && screen > 1) setScreen(screen - 1);
        else if (e.key === "Escape") show("result");
        else return;
        e.preventDefault();
        return;
      }
      if (busy || confirming || left <= 0) return;
      const qq = item.questions[screen - 1];
      const k = e.key.toLowerCase();
      if (e.key === "ArrowRight" && screen < total) setScreen(screen + 1);
      else if (e.key === "ArrowLeft" && screen > 1) setScreen(screen - 1);
      else if ("abcd".includes(k) && k.length === 1) choose(qq, "abcd".indexOf(k));
      else if ("1234".includes(k) && k.length === 1) choose(qq, +k - 1);
      else return;
      e.preventDefault();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [running, view, screen, open?.id, busy, confirming, left <= 0]);

  // Hand-in dialog: Esc goes back to the questions.
  React.useEffect(() => {
    if (!confirming) return;
    const onKey = e => { if (e.key === "Escape" && !busy) setConfirming(false); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [confirming, busy]);

  if (loadError) {
    return (
      <div className="rd-quiz-state">
        <p>{loadError}</p>
        <Button variant="outline" onClick={load}>Reintentar</Button>
      </div>
    );
  }
  if (!qs) return <div className="rd-quiz-state"><span className="rd-spinner" /> Cargando el {noun}…</div>;

  const finished = qs.attempts.filter(a => a.status !== "in_progress");
  const last = [...finished].reverse()[0] || null;

  /* ---- Run: one question per screen ---- */
  if (view === "run" && open) {
    const qq = item.questions[screen - 1];
    const answered = Object.keys(answers).length;
    const low = left != null && left <= 120, critical = left != null && left <= 30;
    const pct = Math.max(0, Math.min(100, (left / qs.time_limit_seconds) * 100));
    const save = saveState[qq.id] || (answers[qq.id] != null ? "saved" : "idle");
    return (
      <div className="rd-qz rd-qz--run">
        <div className="rd-runbar" data-low={low} data-critical={critical}>
          <div className="rd-runbar-count">Pregunta <strong>{screen}</strong> de {total}</div>
          <div className="rd-runbar-pips" role="group" aria-label="Preguntas">
            {item.questions.map((x, i) => (
              <button key={x.id} type="button" aria-label={`Pregunta ${i + 1}${answers[x.id] != null ? ", respondida" : ""}`}
                      data-on={answers[x.id] != null} data-current={i + 1 === screen} aria-current={i + 1 === screen ? "step" : undefined} disabled={busy} onClick={() => setScreen(i + 1)} />
            ))}
          </div>
          <div className="rd-timer" role="timer" aria-live={critical ? "assertive" : "off"} title="Tiempo restante del intento">
            <Icons.Clock size={15} /> <span>{fmtClock(left)}</span>
          </div>
          <span className="rd-runbar-time" style={{ transform: `scaleX(${pct / 100})` }} aria-hidden="true" />
        </div>

        <section className="rd-qscreen" key={qq.id}>
          <div className="rd-qscreen-kicker">
            <span>Elige una respuesta</span>
            <span className="rd-save" data-state={save} aria-live="polite">
              {save === "saving" ? <><span className="rd-save-dot" /> Guardando…</>
                : save === "error" ? "No se pudo guardar · vuelve a elegir"
                : save === "saved" ? <><Icons.Check size={12} stroke={3} /> Respuesta guardada</>
                : "Sin responder"}
            </span>
          </div>
          <h2 className="rd-qscreen-text">{qq.q}</h2>
          <div className="rd-options" role="radiogroup" aria-label={`Pregunta ${screen}`}>
            {qq.options.map((opt, oi) => (
              <button key={oi} type="button" role="radio" aria-checked={answers[qq.id] === oi}
                      className="rd-option" data-state={answers[qq.id] === oi ? "selected" : "idle"} style={{ "--i": oi }}
                      onClick={() => choose(qq, oi)} disabled={busy || left <= 0}>
                <span className="rd-option-letter">{LETTERS[oi]}</span>
                <span className="rd-option-text">{opt}</span>
                <span className="rd-option-mark">{answers[qq.id] === oi && <Icons.Check size={16} stroke={2.5} />}</span>
              </button>
            ))}
          </div>
        </section>

        <div className="rd-qnav">
          <Button variant="outline" icon={<Icons.ChevronLeft size={16} />} onClick={() => setScreen(screen - 1)} disabled={screen === 1}>Anterior</Button>
          <span className="rd-qnav-hint"><kbd>A</kbd>–<kbd>D</kbd> responder · <kbd>←</kbd><kbd>→</kbd> moverse</span>
          {screen < total
            ? <Button iconRight={<Icons.ChevronRight size={16} />} onClick={() => setScreen(screen + 1)}>Siguiente</Button>
            : <Button icon={<Icons.Check size={16} stroke={2.5} />} onClick={() => setConfirming(true)}>Entregar {noun}</Button>}
        </div>

        {confirming && (
          <div className="rd-modal" onClick={e => { if (e.target === e.currentTarget && !busy) setConfirming(false); }}>
            <div className="rd-confirm" role="alertdialog" aria-modal="true" aria-labelledby="rd-confirm-title" aria-describedby="rd-confirm-text">
              <div className="rd-confirm-icon"><Icons.Flag size={20} /></div>
              <h2 className="rd-confirm-title" id="rd-confirm-title">¿Entregar el {noun}?</h2>
              <div className="rd-confirm-meter" aria-hidden="true">
                <span style={{ width: `${(answered / total) * 100}%` }} />
              </div>
              <p id="rd-confirm-text">
                Has respondido <strong>{answered} de {total}</strong>.
                {answered < total ? " Las preguntas sin responder cuentan como incorrectas." : " Ya no podrás cambiar las respuestas."}
                {qs.attempts_used < qs.max_attempts ? ` Es tu intento ${open.number} de ${qs.max_attempts}.` : ""}
              </p>
              <div className="rd-confirm-actions">
                <Button variant="outline" onClick={() => setConfirming(false)} disabled={busy}>Seguir revisando</Button>
                <Button autoFocus onClick={finish} disabled={busy}>{busy ? "Corrigiendo…" : "Entregar ahora"}</Button>
              </div>
            </div>
          </div>
        )}
      </div>
    );
  }

  const review = qs.review; // only present when the test is closed for this student
  const stateOf = (a, qq) => (a.answers?.[qq.id] == null ? "blank" : a.results?.[qq.id] ? "right" : "wrong");

  /* ---- Review: one corrected question per screen ---- */
  if (view === "review" && last) {
    const qq = item.questions[screen - 1];
    const chosen = last.answers?.[qq.id];
    const st = stateOf(last, qq);
    const key = review?.[qq.id];
    return (
      <div className="rd-qz rd-qz--run rd-qz--review">
        <div className="rd-runbar">
          <div className="rd-runbar-count">{review ? "Corrección" : "Tus respuestas"} · <strong>{screen}</strong> de {total}</div>
          <div className="rd-runbar-pips" role="group" aria-label="Preguntas corregidas">
            {item.questions.map((x, i) => (
              <button key={x.id} type="button" data-result={stateOf(last, x)} data-current={i + 1 === screen}
                      aria-current={i + 1 === screen ? "step" : undefined} aria-label={`Pregunta ${i + 1}`} onClick={() => setScreen(i + 1)} />
            ))}
          </div>
          <button type="button" className="rd-runbar-close" onClick={() => show("result")}><Icons.X size={15} /> Cerrar</button>
        </div>

        <section className="rd-rscreen" key={qq.id}>
          <div className="rd-qscreen-kicker">
            <span>Pregunta {screen}</span>
            <span className="rd-rstate" data-state={st}>
              {st === "right" ? <><Icons.Check size={12} stroke={3} /> Correcta</> : st === "wrong" ? <><Icons.X size={12} stroke={3} /> Incorrecta</> : "Sin responder"}
            </span>
          </div>
          <h2 className="rd-qscreen-text">{qq.q}</h2>
          <div className="rd-options">
            {qq.options.map((opt, oi) => {
              const os = key
                ? (oi === key.correct ? "correct" : oi === chosen ? "incorrect" : "idle")
                : (oi === chosen ? (st === "right" ? "correct" : "incorrect") : "idle");
              return (
                <div key={oi} className="rd-option" data-state={os} aria-disabled="true" style={{ "--i": oi }}>
                  <span className="rd-option-letter">{LETTERS[oi]}</span>
                  <span className="rd-option-text">{opt}{oi === chosen && <span className="rd-option-yours">Tu respuesta</span>}</span>
                  <span className="rd-option-mark">
                    {os === "correct" && <Icons.Check size={16} stroke={2.5} />}
                    {os === "incorrect" && <Icons.X size={16} stroke={2.5} />}
                  </span>
                </div>
              );
            })}
          </div>
          {key?.explanation ? (
            <div className="rd-why" data-state={st}>
              <span className="rd-why-label">{st === "right" ? "Por qué es correcta" : `Respuesta correcta: ${LETTERS[key.correct]}`}</span>
              <Rich text={key.explanation} />
            </div>
          ) : !review && (
            <p className="rd-why rd-why--muted">La respuesta correcta se mostrará cuando apruebes el {noun} o agotes los intentos.</p>
          )}
        </section>

        <div className="rd-qnav">
          <Button variant="outline" icon={<Icons.ChevronLeft size={16} />} onClick={() => setScreen(screen - 1)} disabled={screen === 1}>Anterior</Button>
          <span className="rd-qnav-hint"><kbd>←</kbd><kbd>→</kbd> moverse · <kbd>Esc</kbd> volver al resultado</span>
          {screen < total
            ? <Button iconRight={<Icons.ChevronRight size={16} />} onClick={() => setScreen(screen + 1)}>Siguiente</Button>
            : <Button onClick={() => show("result")}>Volver al resultado</Button>}
        </div>
      </div>
    );
  }

  /* ---- Result ---- */
  if (view === "result" && last) {
    const passed = !!last.passed;
    const score = last.score || 0;
    const blank = item.questions.filter(qq => last.answers?.[qq.id] == null).length;
    const wrong = total - score - blank;
    const allDone = courseProgress(course).pct === 100;
    const attemptsLeft = qs.max_attempts - qs.attempts_used;
    const title = passed ? (isExam ? "Has aprobado el examen final" : "Unidad superada") : qs.can_start ? "Aún no alcanzas el aprobado" : "Has agotado los intentos";
    return (
      <section className="rd-qz rd-qz--result" data-pass={passed}>
        <QuizHead item={item} isExam={isExam} />

        <div className="rd-verdict">
          <ScoreRing score={score} total={last.total} passMark={qs.pass_mark} passed={passed} />
          <div className="rd-verdict-copy">
            <span className="rd-verdict-badge">
              {passed ? <Icons.Award size={13} /> : <Icons.Flag size={13} />}
              {passed ? "Aprobado" : "No aprobado"}<span className="rd-verdict-sep" />Intento {last.number} de {qs.max_attempts}
            </span>
            <div className="rd-result-title">{title}</div>
            <p>
              {last.status === "expired" && "El tiempo se agotó y el intento se corrigió con las respuestas guardadas. "}
              {passed
                ? (isExam && allDone ? "Has completado todo el nivel 1. Tu resultado queda registrado en tu expediente del curso."
                  : next ? `Tu resultado queda registrado y se ha desbloqueado ${next.kind === "exam" ? "el examen final" : next.unit ? `la unidad ${next.unit.n}` : "lo siguiente"}.`
                  : "Tu resultado queda registrado en tu expediente del curso.")
                : qs.can_start
                  ? `Necesitas ${qs.pass_mark} aciertos. Te queda ${attemptsLeft === 1 ? "un intento" : `${attemptsLeft} intentos`}: repasa la unidad antes de usarlo.`
                  : "Tu resultado queda registrado. Si necesitas un intento adicional, contacta con el equipo académico."}
            </p>
          </div>
        </div>

        <QuizStats items={[
          { label: "Aciertos", value: score, tone: "right", icon: <Icons.Check size={12} stroke={3} /> },
          { label: "Fallos", value: wrong, tone: wrong ? "wrong" : undefined, icon: <Icons.X size={12} stroke={3} /> },
          { label: "En blanco", value: blank },
          { label: "Nota", value: <>{Math.round((score / last.total) * 100)}<small> %</small></> },
        ]} />

        <section className="rd-qz-card rd-answers">
          <div className="rd-qz-card-head">
            <h2 className="rd-qz-card-title">{review ? "Corrección" : "Tus respuestas"}</h2>
            <span className="rd-answers-legend">
              <span data-result="right">Correcta</span><span data-result="wrong">Incorrecta</span>{blank > 0 && <span data-result="blank">Sin responder</span>}
            </span>
          </div>
          <ol className="rd-answers-grid">
            {item.questions.map((qq, i) => {
              const st = stateOf(last, qq);
              return (
                <li key={qq.id} style={{ "--i": i }}>
                  <button type="button" data-result={st} onClick={() => show("review", i + 1)}
                          aria-label={`Pregunta ${i + 1}: ${st === "right" ? "correcta" : st === "wrong" ? "incorrecta" : "sin responder"}`}>
                    <span>{i + 1}</span>
                    {st === "right" ? <Icons.Check size={12} stroke={3} /> : st === "wrong" ? <Icons.X size={12} stroke={3} /> : <i />}
                  </button>
                </li>
              );
            })}
          </ol>
          {!review && <p className="rd-answers-note"><Icons.Lock size={12} /> Las respuestas correctas se mostrarán cuando apruebes el {noun} o agotes los intentos.</p>}
        </section>

        <footer className="rd-qz-foot">
          <Button variant="outline" onClick={() => show("review", 1)}>{review ? "Revisar la corrección" : "Revisar mis respuestas"}</Button>
          <div className="rd-qz-foot-actions">
            {qs.can_start && <Button icon={<Icons.ArrowRight size={14} />} onClick={() => show("intro")}>Preparar el intento {qs.attempts_used + 1}</Button>}
            {passed && next && <Button iconRight={<Icons.ArrowRight size={16} />} onClick={() => navigate(itemHref(course, next))}>
              Continuar · {next.kind === "exam" ? "Examen final" : next.unit ? `Unidad ${next.unit.n}` : next.title}
            </Button>}
          </div>
        </footer>
      </section>
    );
  }

  /* ---- Intro: instructions and review ---- */
  const limitMin = Math.round(qs.time_limit_seconds / 60);
  return (
    <section className="rd-qz rd-qz--intro">
      <QuizHead item={item} isExam={isExam} />
      <QuizStats items={[
        { label: "Preguntas", value: qs.total, icon: <Icons.Quiz size={13} /> },
        { label: "Tiempo máximo", value: <>{limitMin}<small> min</small></>, icon: <Icons.Clock size={13} /> },
        { label: "Para aprobar", value: <>{qs.pass_mark}<small>/{qs.total}</small></>, icon: <Icons.Award size={13} /> },
        { label: "Intentos usados", value: <>{qs.attempts_used}<small>/{qs.max_attempts}</small></>, icon: <Icons.Layers size={13} /> },
      ]} />

      <div className="rd-qz-grid">
        <section className="rd-qz-card rd-rules">
          <div className="rd-qz-card-head"><h2 className="rd-qz-card-title">Cómo funciona el {noun}</h2></div>
          <ul>
            <li><span className="rd-rules-icon"><Icons.Clock size={15} /></span><div><strong>{limitMin} minutos en total</strong><span>{qs.seconds_per_question / 60} min por pregunta. El reloj empieza al pulsar «Comenzar» y sigue aunque salgas de la página.</span></div></li>
            <li><span className="rd-rules-icon"><Icons.Layers size={15} /></span><div><strong>Una pregunta por pantalla</strong><span>Avanza, vuelve y cambia respuestas mientras quede tiempo. Cada respuesta se guarda al momento.</span></div></li>
            <li><span className="rd-rules-icon"><Icons.Flag size={15} /></span><div><strong>Entrega automática</strong><span>Si el tiempo se agota se entrega lo guardado; lo no respondido cuenta como incorrecto.</span></div></li>
            <li><span className="rd-rules-icon"><Icons.Award size={15} /></span><div><strong>{qs.max_attempts} intentos · aprobado con {qs.pass_mark} de {qs.total}</strong><span>Solo repites si no apruebas. La corrección completa aparece al aprobar o agotar los intentos.</span></div></li>
          </ul>
        </section>
        <QuizPrep course={course} item={item} />
      </div>

      <footer className="rd-qz-foot">
        <div className="rd-qz-foot-copy">
          <span>{open ? <><strong>Tienes un intento en curso.</strong> Quedan {fmtClock(left)}.</>
            : qs.can_start ? (qs.attempts_used === 0 ? <>Cuando estés listo, tendrás <strong>{limitMin} minutos</strong>.</> : <><strong>Último intento.</strong> Tendrás {limitMin} minutos.</>)
            : qs.passed ? `Ya has aprobado este ${noun}.` : `Has agotado los intentos de este ${noun}.`}</span>
          {finished.length > 0 && (
            <span className="rd-qz-history">
              {finished.map(a => (
                <span key={a.id} className="rd-qz-chip" data-pass={!!a.passed}
                      title={a.submitted_at ? new Date(a.submitted_at).toLocaleString("es", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }) : undefined}>
                  Intento {a.number} · {a.score}/{a.total}{a.status === "expired" ? " · tiempo agotado" : ""}
                </span>
              ))}
            </span>
          )}
        </div>
        {open ? (
          <Button size="lg" icon={<Icons.PlayCircle size={18} />} onClick={start} disabled={busy}>Continuar el intento {open.number}</Button>
        ) : qs.can_start ? (
          <Button size="lg" icon={<Icons.PlayCircle size={18} />} onClick={start} disabled={busy}>
            {busy ? "Preparando…" : `Comenzar el intento ${qs.attempts_used + 1}`}
          </Button>
        ) : (
          <Button size="lg" variant="outline" onClick={() => show("result")}>Ver la corrección</Button>
        )}
      </footer>
    </section>
  );
};

/* ---------- Not enrolled ---------- */
const ReaderEnrollGate = ({ course }) => (
  <div className="rd-gate">
    <div className="rd-gate-card">
      <BrandLogo size={22} />
      <div className="rd-kicker mt-6">{course.program}</div>
      <h1 className="rd-title">{course.title}</h1>
      <p className="rd-gate-lead">Inscríbete para acceder a las páginas del curso, guardar tu progreso y realizar las evaluaciones.</p>
      <div className="flex gap-2 wrap mt-6">
        <Button size="lg" icon={<Icons.Plus size={16} stroke={3} />} onClick={async () => {
          if (await CourseStore.enroll(course.id)) navigate(itemHref(course, nextItem(course)));
        }}>Inscribirme gratis</Button>
        <Button size="lg" variant="outline" onClick={() => navigate(`#/curso/${course.id}`)}>Ver el programa</Button>
      </div>
    </div>
  </div>
);

window.CourseReaderPage = CourseReaderPage;
