/* global React, window */
const { useState, useEffect } = React;

// CATALOG is loaded from catalog.js (global)

// ================================================================
// STEP CURADORIA INTRO, capa hero full-page com o Lex Curadoria
// Aparece antes do Step6 (Volume) abrindo a seção de literatura.
// ================================================================
function StepCuradoriaIntro() {
  return (
    <div style={{
      margin: "-40px -56px",
      position: "relative",
      minHeight: "calc(100vh - 120px)",
      overflow: "hidden",
      background: `
        radial-gradient(ellipse at 30% 35%, rgba(158,229,184,0.18) 0%, transparent 55%),
        radial-gradient(ellipse at 80% 70%, rgba(106,196,212,0.15) 0%, transparent 55%),
        linear-gradient(135deg, #1a2620 0%, #24342a 45%, #1a2620 100%)
      `,
      display: "flex", flexDirection: "column",
    }}>
      {/* ===== TOPO: imagem esquerda (full-bleed), texto direita ===== */}
      <div style={{
        display: "grid",
        gridTemplateColumns: "minmax(480px, 58%) 1fr",
        gap: 0,
        alignItems: "stretch",
        flex: 1,
        minHeight: 0,
      }}>
        {/* Lex, ocupa TODA a coluna esquerda, full-bleed até as bordas */}
        <div style={{
          position: "relative",
          overflow: "hidden",
          display: "flex", alignItems: "flex-end", justifyContent: "center",
        }}>
          <img src="assets/lex-curadoria-v2.png" alt="Lex escolhendo livros" style={{
            width: "100%", height: "100%",
            objectFit: "cover", objectPosition: "center 30%",
            filter: "drop-shadow(0 30px 50px rgba(0,0,0,0.55))",
          }}/>
          {/* fade na borda direita pra conectar com o texto */}
          <div aria-hidden style={{
            position: "absolute", inset: 0,
            background: "linear-gradient(90deg, transparent 70%, rgba(26,38,32,0.55) 100%)",
            pointerEvents: "none",
          }}/>
          {/* glow verde ambiente */}
          <div aria-hidden style={{
            position: "absolute", bottom: "8%", left: "50%",
            transform: "translateX(-50%)",
            width: "80%", height: 60,
            background: "radial-gradient(ellipse, rgba(158,229,184,0.28) 0%, transparent 70%)",
            filter: "blur(30px)",
            pointerEvents: "none",
          }}/>
        </div>

        {/* Texto à direita */}
        <div style={{ color: "#fff", maxWidth: 560, padding: "56px 64px 40px 40px", alignSelf: "center" }}>
          <div style={{
            display: "inline-flex", alignItems: "center", gap: 10,
            padding: "7px 16px", background: "rgba(255,255,255,0.1)",
            backdropFilter: "blur(10px)", WebkitBackdropFilter: "blur(10px)",
            border: "1px solid rgba(255,255,255,0.18)",
            borderRadius: 24, fontSize: 11, fontWeight: 700,
            color: "#9ee5b8", textTransform: "uppercase", letterSpacing: 1.4,
            marginBottom: 22,
          }}>
            <span style={{ width: 6, height: 6, borderRadius: "50%", background: "#9ee5b8", boxShadow: "0 0 8px #9ee5b8" }}/>
            Curadoria Literária
          </div>

          <div style={{
            fontSize: "clamp(44px, 5.2vw, 68px)", fontWeight: 800, lineHeight: 1.02,
            letterSpacing: -2.2, marginBottom: 22, textWrap: "balance",
            textShadow: "0 2px 20px rgba(0,0,0,0.4)",
          }}>
            Hora de escolher os <span style={{
              background: "linear-gradient(135deg, #9ee5b8 0%, #6ac4d4 100%)",
              WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent",
              backgroundClip: "text",
            }}>livros</span> do ano.
          </div>

          <div style={{
            fontSize: 18, lineHeight: 1.55,
            color: "rgba(255,255,255,0.82)",
            marginBottom: 24, textWrap: "pretty", fontWeight: 400,
          }}>
            Comigo você decide o volume por série, ajusta disponibilidade, define a faixa de preço
            e recebe uma curadoria personalizada baseada nos temas e no nível <strong style={{ color: "#fff" }}><Scolex/></strong> dos seus alunos, com ajuste fino livro a livro.
          </div>

          <div style={{
            display: "inline-flex", alignItems: "center", gap: 12,
            padding: "12px 18px", background: "rgba(255,255,255,0.08)",
            border: "1px solid rgba(255,255,255,0.14)", borderRadius: 14,
            fontSize: 13.5, color: "rgba(255,255,255,0.92)",
          }}>
            <span>
              <strong style={{ color: "#9ee5b8" }}>Dica do Lex:</strong> você pode voltar atrás a qualquer momento.
            </span>
          </div>
        </div>
      </div>

      {/* ===== FAIXA INFERIOR: 4 etapas em horizontal ===== */}
      <div style={{
        padding: "24px 64px 36px",
        background: "linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.22) 100%)",
        borderTop: "1px solid rgba(255,255,255,0.06)",
      }}>
        <div style={{
          fontSize: 11, fontWeight: 700, letterSpacing: 1.6,
          color: "rgba(158,229,184,0.8)", textTransform: "uppercase",
          marginBottom: 14, textAlign: "center",
        }}>
          Os 5 passos da Curadoria
        </div>
        <div style={{
          display: "grid", gridTemplateColumns: "repeat(5, 1fr)",
          gap: 14, maxWidth: 1100, margin: "0 auto",
        }}>
          {[
            { n: "01", t: "Volume",           d: "Livros por série, por idioma" },
            { n: "02", t: "Disponibilidade",  d: "Filtros de estoque e prazo" },
            { n: "03", t: "Preços",           d: "Faixa alvo por aluno / ano" },
            { n: "04", t: "Recomendação",     d: "Curadoria série a série" },
            { n: "05", t: "Detalhes",         d: "Ajuste fino livro a livro" },
          ].map(s => (
            <div key={s.n} style={{
              padding: "16px 18px",
              background: "rgba(255,255,255,0.06)",
              border: "1px solid rgba(255,255,255,0.1)",
              borderRadius: 14,
              backdropFilter: "blur(6px)", WebkitBackdropFilter: "blur(6px)",
              transition: "transform 0.2s",
            }}>
              <div style={{ fontSize: 11, fontWeight: 700, color: "#9ee5b8", letterSpacing: 1.2, marginBottom: 6 }}>{s.n}</div>
              <div style={{ fontSize: 15, fontWeight: 700, color: "#fff", marginBottom: 3 }}>{s.t}</div>
              <div style={{ fontSize: 12, color: "rgba(255,255,255,0.6)", lineHeight: 1.4 }}>{s.d}</div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function Step6_Qty() {
  const { data, setData } = useApp();
  const qtyPT = data.qtyPerYearPT ?? 3;
  const qtyEN = data.qtyPerYearEN ?? 2;
  const qty = qtyPT + qtyEN;
  const setPT = v => setData(d => ({...d, qtyPerYearPT: v, qtyPerYear: v + (d.qtyPerYearEN ?? 2)}));
  const setEN = v => setData(d => ({...d, qtyPerYearEN: v, qtyPerYear: v + (d.qtyPerYearPT ?? 3)}));
  const hasEN = data.scolexEN !== "none";
  return (
    <div>
      <Heading
        eyebrow="Curadoria Literária · Passo X-OBSOLETO, Volume"
        title="Quantos livros de literatura adotar por ano letivo, por série?"
        subtitle="Separe por idioma, Português e Inglês têm ritmos e metas diferentes. Eu distribuo ao longo dos trimestres." />
      <LexBubble pose="read">
        A média das escolas parceiras do <Scolex/> é de <strong>3 em Português + 2 em Inglês</strong> por série, o suficiente para criar hábito sem sobrecarregar.
      </LexBubble>

      <div style={{ marginTop: 32, display: "grid", gridTemplateColumns: "1fr 320px", gap: 32, alignItems: "start" }}>
        <div style={{ background: "#fff", border: "1px solid var(--border)", borderRadius: 20, padding: 32, boxShadow: "var(--shadow-sm)" }}>
          <div style={{ display: "flex", alignItems: "baseline", gap: 18, marginBottom: 24 }}>
            <div>
              <div style={{ fontSize: 12, fontWeight: 600, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.8 }}>Total por série / ano</div>
              <div style={{ fontSize: 96, fontWeight: 800, color: "var(--teal)", lineHeight: 1, letterSpacing: -4 }}>{qty}</div>
              <div style={{ fontSize: 14, color: "var(--text-mute)", marginTop: 4 }}>livros</div>
            </div>
            <div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 10, paddingBottom: 6 }}>
              <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                <div style={{ width: 4, height: 28, background: "var(--teal)", borderRadius: 2 }}/>
                <div><div style={{ fontSize: 22, fontWeight: 800, color: "var(--text)", lineHeight: 1 }}>{qtyPT}</div><div style={{ fontSize: 11, color: "var(--text-mute)", fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.8 }}>Português</div></div>
              </div>
              <div style={{ display: "flex", alignItems: "center", gap: 10, opacity: hasEN ? 1 : 0.35 }}>
                <div style={{ width: 4, height: 28, background: "#2c4ffc", borderRadius: 2 }}/>
                <div><div style={{ fontSize: 22, fontWeight: 800, color: "var(--text)", lineHeight: 1 }}>{hasEN ? qtyEN : 0}</div><div style={{ fontSize: 11, color: "var(--text-mute)", fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.8 }}>Inglês</div></div>
              </div>
            </div>
          </div>

          <div style={{ marginBottom: 24 }}>
            <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
              <div style={{ fontSize: 13, fontWeight: 700, color: "var(--text)" }}>Português</div>
              <div style={{ fontSize: 13, fontWeight: 700, color: "var(--teal-dark)" }}>{qtyPT} por série</div>
            </div>
            <input type="range" min="0" max="8" value={qtyPT} onChange={e => setPT(+e.target.value)}
              style={{ width: "100%", accentColor: "var(--teal)" }} />
            <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11, color: "var(--text-mute)", marginTop: 2 }}>
              <span>0</span><span>3 · recomendado</span><span>8</span>
            </div>
          </div>

          <div style={{ opacity: hasEN ? 1 : 0.4 }}>
            <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
              <div style={{ fontSize: 13, fontWeight: 700, color: "var(--text)" }}>Inglês {!hasEN && <span style={{ fontSize: 11, color: "var(--text-mute)", fontWeight: 500 }}>, escola não faz aula em inglês</span>}</div>
              <div style={{ fontSize: 13, fontWeight: 700, color: "var(--teal-dark)" }}>{hasEN ? qtyEN : 0} por série</div>
            </div>
            <input type="range" min="0" max="6" value={qtyEN} onChange={e => setEN(+e.target.value)} disabled={!hasEN}
              style={{ width: "100%", accentColor: "#2c4ffc" }} />
            <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11, color: "var(--text-mute)", marginTop: 2 }}>
              <span>0</span><span>2 · recomendado</span><span>6</span>
            </div>
          </div>

          <div style={{
            marginTop: 24, padding: "14px 16px",
            background: "var(--bg-light)", borderRadius: 12,
            fontSize: 14, color: "var(--text)",
            display: "flex", gap: 12, alignItems: "center",
          }}>
            <div style={{fontSize: 22}}>📚</div>
            <div>
              <strong>{qty * 12}</strong> livros no total, 12 séries × {qty} livros cada
              {qtyEN > 0 && hasEN && <span style={{ color: "var(--text-mute)" }}> ({qtyPT*12} PT + {qtyEN*12} EN)</span>}
            </div>
          </div>
        </div>

        <div style={{ background: "#fff", border: "1px solid var(--border)", borderRadius: 20, padding: 24, boxShadow: "var(--shadow-sm)" }}>
          <div style={{ fontSize: 14, fontWeight: 700, color: "var(--teal-dark)", textTransform: "uppercase", letterSpacing: 1, marginBottom: 14 }}>
            Distribuição sugerida
          </div>
          {[
            ["1º bimestre", Math.ceil(qty/4)],
            ["2º bimestre", Math.ceil(qty/4)],
            ["3º bimestre", Math.floor(qty/4)],
            ["4º bimestre", Math.max(0, qty - Math.ceil(qty/4)*2 - Math.floor(qty/4))],
          ].map(([l, n], i) => (
            <div key={i} style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 10 }}>
              <div style={{ fontSize: 13, color: "var(--text-mute)", width: 100, fontWeight: 500 }}>{l}</div>
              <div style={{ flex: 1, height: 10, background: "var(--bg-light)", borderRadius: 5, overflow: "hidden" }}>
                <div style={{ width: `${Math.max(0,n) * 15}%`, height: "100%", background: "var(--teal)", borderRadius: 5 }}/>
              </div>
              <div style={{ fontSize: 14, fontWeight: 700, color: "var(--teal-dark)", width: 28, textAlign: "right" }}>{Math.max(0,n)}</div>
            </div>
          ))}
          <div style={{ marginTop: 16, paddingTop: 14, borderTop: "1px dashed var(--border)", fontSize: 12, color: "var(--text-mute)", lineHeight: 1.5 }}>
            <strong style={{ color: "var(--text)" }}>Por série:</strong><br/>
            {qtyPT > 0 && <>• {qtyPT} livro{qtyPT>1?"s":""} em Português<br/></>}
            {qtyEN > 0 && hasEN && <>• {qtyEN} livro{qtyEN>1?"s":""} em Inglês</>}
          </div>
        </div>
      </div>
    </div>
  );
}

// Step 8: Availability check (animation)
function Step7_Availability() {
  const [phase, setPhase] = useState(0);
  useEffect(() => {
    const t1 = setTimeout(() => setPhase(1), 900);
    const t2 = setTimeout(() => setPhase(2), 2100);
    const t3 = setTimeout(() => setPhase(3), 3200);
    return () => { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3); };
  }, []);
  const publishers = [
    { n:"Companhia das Letras",     logo:"assets/editoras/companhia.png",       s: 94 },
    { n:"Pallas",                   logo:"assets/editoras/pallas.png",          s: 88 },
    { n:"Penguin",                  logo:"assets/editoras/penguin.png",         s: 91 },
    { n:"Scholastic",               logo:"assets/editoras/scholastic.png",      s: 85 },
    { n:"Bloomsbury",               logo:"assets/editoras/bloomsbury.png",      s: 79 },
    { n:"Pallas Mini",              logo:"assets/editoras/pallasmini.png",      s: 82 },
    { n:"Global Editora",           logo:"assets/editoras/global.png",          s: 90 },
    { n:"Grupo Editorial Record",   logo:"assets/editoras/record.png",          s: 87 },
    { n:"Moderna Literatura",       logo:"assets/editoras/moderna-literatura.png", s: 93 },
    { n:"HarperCollins",            logo:"assets/editoras/harpercollins.png",   s: 89 },
    { n:"Piraporiando",             logo:"assets/editoras/piraporiando.png",    s: 81 },
    { n:"Savvas",                   logo:"assets/editoras/savvas.png",          s: 86 },
    { n:"Pearson",                  logo:"assets/editoras/pearson.png",         s: 92 },
    { n:"Girassol",                 logo:"assets/editoras/girassol.png",        s: 84 },
    { n:"Editora Globo",            logo:"assets/editoras/globo.png",           s: 90 },
    { n:"FTD Educação",             logo:"assets/editoras/ftd.png",             s: 88 },
  ];
  return (
    <div>
      <Heading
        eyebrow="Curadoria Literária · Passo 1, Disponibilidade"
        title="Verificando estoque e temas nas editoras…"
        subtitle="Eu cruzo seu catálogo sugerido com o inventário em tempo real das editoras parceiras da Scolados." />
      <LexBubble pose="books">
        Só um instante, estou puxando disponibilidade de estoque e relevância de tema ao vivo 🔎
      </LexBubble>

      <div style={{ marginTop: 32, background: "#fff", border: "1px solid var(--border)", borderRadius: 20, padding: 28, boxShadow: "var(--shadow-sm)" }}>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 16 }}>
          {publishers.map((p, i) => {
            const revealCount = Math.ceil((phase / 3) * publishers.length);
            const ready = phase >= 1 && i < revealCount;
            return (
              <div key={p.n} style={{
                display: "flex", flexDirection: "column", alignItems: "center",
                padding: "18px 10px 14px",
                background: ready ? "rgba(199,222,64,0.08)" : "var(--bg-soft)",
                border: `1px solid ${ready ? "var(--teal)" : "var(--border)"}`,
                borderRadius: 14, transition: "all 0.4s",
                position: "relative",
              }}>
                <div style={{
                  height: 56, display: "flex", alignItems: "center", justifyContent: "center",
                  marginBottom: 10, opacity: ready ? 1 : 0.35, transition: "opacity 0.4s",
                }}>
                  <img src={p.logo} alt={p.n} style={{ maxHeight: 52, maxWidth: "100%", objectFit: "contain" }} />
                </div>
                <div style={{ fontSize: 11, fontWeight: 700, color: ready ? "var(--teal-dark)" : "var(--text-mute)", letterSpacing: 0.3, textTransform: "uppercase" }}>
                  {ready ? `${p.s}% em estoque` : "verificando…"}
                </div>
                <div style={{ height: 4, width: "80%", background: "rgba(0,0,0,0.06)", borderRadius: 2, marginTop: 8, overflow: "hidden" }}>
                  <div style={{
                    width: ready ? `${p.s}%` : "0%",
                    height: "100%", background: "var(--teal)",
                    transition: "width 0.8s ease", borderRadius: 2,
                  }}/>
                </div>
                {ready && (
                  <div style={{
                    position: "absolute", top: 8, right: 8, width: 18, height: 18, borderRadius: "50%",
                    background: "var(--teal)", color: "#fff", fontSize: 10, fontWeight: 800,
                    display: "flex", alignItems: "center", justifyContent: "center",
                  }}>✓</div>
                )}
              </div>
            );
          })}
        </div>

        {phase >= 3 && (
          <div style={{
            marginTop: 28, padding: "16px 20px",
            background: "linear-gradient(90deg, rgba(199,222,64,0.18), rgba(47,151,161,0.1))",
            border: "1px solid var(--teal)", borderRadius: 14,
            display: "flex", gap: 14, alignItems: "center",
          }}>
            <div style={{
              width: 36, height: 36, borderRadius: "50%",
              background: "var(--green)", color: "var(--text)",
              display: "flex", alignItems: "center", justifyContent: "center",
              fontSize: 18, fontWeight: 800, flexShrink: 0,
            }}>✓</div>
            <div>
              <div style={{ fontSize: 15, fontWeight: 700, color: "var(--text)" }}>
                288 títulos disponíveis compatíveis com seus temas
              </div>
              <div style={{ fontSize: 13, color: "var(--text-mute)", marginTop: 2 }}>
                Todos com estoque para entrega em até 5 dias úteis na sua região
              </div>
            </div>
          </div>
        )}
      </div>
      <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
    </div>
  );
}

// Step Previsão do preço da lista, depois da confirmação final, antes dos pontos de compra
// Mostra subtotais (Didáticos · Paradidáticos · Papelaria), total estimado por aluno
// ================================================================
// Hook: IPCA acumulado 12m da Banco Central API (SGS 13522)
// Fallback: cache localStorage (1 dia) e valor estático se a API falhar.
// ================================================================
function useIPCA() {
  const [state, setState] = useState({ value: null, date: null, loading: true, error: false });
  useEffect(() => {
    const KEY = "scolist_ipca_v1";
    const ONE_DAY = 24 * 60 * 60 * 1000;
    try {
      const cached = JSON.parse(localStorage.getItem(KEY) || "null");
      if (cached && Date.now() - cached.fetchedAt < ONE_DAY) {
        setState({ value: cached.value, date: cached.date, loading: false, error: false });
        return;
      }
    } catch(e) {}

    // BCB SGS série 13522 = IPCA acumulado 12 meses
    const url = "https://api.bcb.gov.br/dados/serie/bcdata.sgs.13522/dados/ultimos/1?formato=json";
    fetch(url)
      .then(r => r.json())
      .then(arr => {
        if (!arr || !arr.length) throw new Error("empty");
        const v = parseFloat(arr[0].valor);
        const d = arr[0].data; // dd/mm/yyyy
        const payload = { value: v, date: d, fetchedAt: Date.now() };
        try { localStorage.setItem(KEY, JSON.stringify(payload)); } catch(e) {}
        setState({ value: v, date: d, loading: false, error: false });
      })
      .catch(() => {
        // fallback estático conservador (último valor conhecido)
        setState({ value: 4.14, date: "01/03/2026", loading: false, error: true });
      });
  }, []);
  return state;
}

// ================================================================
// Step 9 (Passo 1): Previsão de preço, matriz por série × componente
// com variação YoY e drill-down inline.
// Mostra Literatura, Didáticos e Papelaria. "Outros" não tem preço.
// ================================================================
function Step8_Pricing() {
  const { data } = useApp();
  const ipca = useIPCA();
  const [groupByStage, setGroupByStage] = useState(true);
  const [drillKey, setDrillKey] = useState(null); // "seriesIdx-component"
  const [exportOpen, setExportOpen] = useState(false);

  const hasEN = data.scolexEN !== "none";
  const qtyPT = data.qtyPerYearPT ?? (data.qtyPerYear ?? 3);
  const qtyEN = hasEN ? (data.qtyPerYearEN ?? 2) : 0;
  const qtyLit = qtyPT + qtyEN;
  const pinned = data.pinned || {};
  const didaticos = data.didaticosAdopted || {};
  const papelaria = data.papelaria || {};
  const customMaterials = data.customMaterials || [];
  const DIDATICOS_CAT = window.DIDATICOS || [];
  const PAPELARIA_CAT = window.PAPELARIA_CATALOG || [];
  const PAPELARIA_PARTNERS_LOCAL = window.PAPELARIA_PARTNERS || {};
  const avgLit = 48.50;

  // Séries de verdade da escola (Passo 1) — não a lista fixa de 12. `s` abaixo
  // é o RÓTULO exibido; pra achar preço no catálogo estático de didáticos
  // (priceBySeries), que só conhece os nomes canônicos, usa-se o nome
  // canônico correspondente à posição (SERIES_LIST[i]) — séries extras
  // (Educação Infantil, nomes customizados sem padrão reconhecível) não têm
  // canônico, então entram com custo de didáticos zerado.
  const activeSeries = resolveSeriesList(data);

  // ===== Calcula preços 2026 por série × componente =====
  // Retorna: { lit: {value, items}, did: {value, items}, pap: {value, items} }
  const seriesData = activeSeries.map((s, i) => {
    const canonicalName = SERIES_LIST[i] || null;
    // Literatura
    const litIds = (pinned[i] || []).filter((id) => String(id || "").startsWith("rt-"));
    const litBooks = litIds.map(id => CATALOG.find(b => b.id === id)).filter(Boolean);
    let litValue = 0;
    const litItems = [];
    litBooks.forEach(b => {
      const price = b.priceCapa || b.price || avgLit;
      litValue += price;
      litItems.push({ name: b.title, sub: b.author || b.editora, price, lang: b.lang });
    });
    const remaining = Math.max(0, qtyLit - litBooks.length);
    if (remaining > 0) {
      litValue += remaining * avgLit;
      // não adiciona aos items (são livros sem nome ainda)
    }

    // Didáticos
    const didList = didaticos[i] || [];
    let didValue = 0;
    const didItems = [];
    didList.forEach(a => {
      const it = DIDATICOS_CAT.find(d => d.id === a.itemId);
      const price = (canonicalName && it?.priceBySeries?.[canonicalName]) || 0;
      didValue += price;
      if (it) didItems.push({ name: it.title, sub: it.publisher || it.editora, price, lang: it.lang });
    });

    // Papelaria
    const papList = papelaria[i] || [];
    let papValue = 0;
    const papItems = [];
    papList.forEach(c => {
      const it = PAPELARIA_CAT.find(x => x.id === c.itemId);
      const br = it?.brands?.[c.brandIdx];
      const partner = br ? PAPELARIA_PARTNERS_LOCAL[br.partnerId] : null;
      const lineTotal = br ? br.price * c.qty : 0;
      papValue += lineTotal;
      if (it && br) papItems.push({
        name: it.genericName, sub: partner?.name || br.partnerId,
        price: br.price, qty: c.qty, total: lineTotal,
      });
    });

    // Outros materiais (item "outros" do passo Confirmação final) — a Joana
    // pediu pra garantir que esses valores entram na soma da previsão de
    // preço, não só aparecem lá na revisão (reunião 26/08/2026).
    const customHere = customMaterials.filter(m => (m.seriesApplied || []).includes(i));
    let customValue = 0;
    const customItems = [];
    customHere.forEach(m => {
      const price = typeof m.price === "number" ? m.price : 0;
      customValue += price;
      customItems.push({ name: m.name, sub: m.desc, price });
    });

    return {
      label: s, idx: i,
      lit: { value: litValue, items: litItems },
      did: { value: didValue, items: didItems },
      pap: { value: papValue, items: papItems },
      custom: { value: customValue, items: customItems },
    };
  });

  // ===== Variação YoY sintética determinística =====
  // Cada combinação série×componente recebe uma variação plausível (-8% a +18%)
  // com seed estável pra demo parecer real.
  const yoyPct = (seriesIdx, component) => {
    const seedMap = { lit: 7, did: 13, pap: 21 };
    const seed = (seriesIdx + 1) * 137 + (seedMap[component] || 0) * 53;
    const r = ((seed * 9301 + 49297) % 233280) / 233280;
    // distribuição: -6% a +14%, centrada em +5%
    return Math.round((-6 + r * 20) * 10) / 10;
  };
  const isNew = (seriesIdx, component, value) => {
    // marca "novo" se valor > 0 mas seed sugere ausência ano passado
    if (value === 0) return false;
    const seedMap = { lit: 31, did: 41, pap: 59 };
    const seed = (seriesIdx + 1) * 113 + (seedMap[component] || 0) * 47;
    return ((seed * 7919 + 17) % 100) < 8; // ~8% das células viram "novo"
  };

  // ===== Totais agregados =====
  const total2026 = seriesData.reduce((s, sd) => s + sd.lit.value + sd.did.value + sd.pap.value + sd.custom.value, 0);
  const enrolled = data.enrolled || 320; // alunos totais (mock)
  const perStudent2026 = total2026 > 0 ? total2026 / activeSeries.length * (activeSeries.length / activeSeries.length) : 0;

  // total escola e por aluno
  // Por aluno: assumimos que cada série tem ~ enrolled/N alunos (N = nº de séries da escola).
  // Pra previsão por aluno, mostramos média ponderada das séries.
  const avgSeriesValue = total2026 / activeSeries.length;
  const totalEscola = total2026 * (enrolled / activeSeries.length); // total geral da escola

  // ===== Variação total =====
  // 2025 = total2026 / (1 + escolaPct/100)
  const escolaPct = (() => {
    // pondera variações de cada célula pelo valor 2026
    let weightedDelta = 0, totalW = 0;
    seriesData.forEach((sd) => {
      ["lit","did","pap"].forEach(comp => {
        const v = sd[comp].value;
        if (v > 0) {
          const d = yoyPct(sd.idx, comp);
          weightedDelta += d * v;
          totalW += v;
        }
      });
    });
    return totalW > 0 ? weightedDelta / totalW : 0;
  })();
  const total2025 = totalEscola / (1 + escolaPct / 100);
  const deltaEscolaRs = totalEscola - total2025;
  const perStudentDelta = (avgSeriesValue * (enrolled / activeSeries.length)) - (avgSeriesValue * (enrolled / activeSeries.length)) / (1 + escolaPct / 100);

  // perAluno em R$
  const perAluno2026 = totalEscola / enrolled;
  const perAluno2025 = total2025 / enrolled;
  const perAlunoDelta = perAluno2026 - perAluno2025;

  // ===== % de variação YoY e IPCA removidos da tela (reunião 26/08/2026) =====
  // A Joana pediu pra tirar por enquanto: a variação (yoyPct/escolaPct acima)
  // é sintética/demo, não uma comparação real com a lista de 2025 (que ainda
  // não existe salva em lugar nenhum), e comparar isso com o IPCA real do
  // Banco Central passava a falsa impressão de ser um dado de verdade. Fica
  // comentado pra reativar quando tivermos pelo menos um ano de listas
  // salvas pra comparar de verdade — ver useIPCA() acima e escolaPct/
  // yoyPct/isNew logo abaixo, que continuam calculando mas não são mais
  // renderizados.
  const ipcaVal = ipca.value;
  const diffPP = ipcaVal != null ? (ipcaVal - escolaPct) : null;
  const lexCopy = "Aqui está o total estimado da lista de 2026, somando literatura, didáticos, papelaria e outros materiais série por série.";

  // ===== Etapas =====
  const STAGES = [
    { label: "Pré-escola", indices: [] },
    { label: "Fundamental I", indices: [0,1,2,3,4] },
    { label: "Fundamental II", indices: [5,6,7,8] },
    { label: "Ensino Médio", indices: [9,10,11] },
  ];

  // ===== Renderiza linha de série =====
  const renderSeriesRow = (sd) => {
    const total = sd.lit.value + sd.did.value + sd.pap.value + sd.custom.value;
    const cells = ["lit", "did", "pap", "custom"];
    const componentLabels = { lit: "Literatura", did: "Didáticos", pap: "Papelaria", custom: "Outros" };
    return (
      <React.Fragment key={sd.idx}>
        <tr style={{ borderBottom: "1px solid var(--bg-soft)" }}>
          <td style={{ padding: "11px 14px 11px 28px", fontWeight: 700, fontSize: 13, color: "var(--text)" }}>{sd.label}</td>
          {cells.map(comp => {
            const v = sd[comp].value;
            const items = sd[comp].items;
            const cellKey = `${sd.idx}-${comp}`;
            const isOpen = drillKey === cellKey;
            const isClickable = items.length > 0;
            return (
              <td
                key={comp}
                onClick={isClickable ? () => setDrillKey(isOpen ? null : cellKey) : undefined}
                style={{
                  padding: "11px 14px", textAlign: "right", verticalAlign: "top",
                  cursor: isClickable ? "pointer" : "default",
                  background: isOpen ? "#f0fafa" : "transparent",
                }}
              >
                {v === 0 ? (
                  <span style={{ color: "var(--text-mute)", fontWeight: 500 }}>,</span>
                ) : (
                  <div style={{ fontWeight: 700, fontSize: 13, color: "var(--text)", fontVariantNumeric: "tabular-nums" }}>
                    R$ {v.toFixed(0)}
                  </div>
                )}
              </td>
            );
          })}
          <td style={{ padding: "11px 14px", textAlign: "right", background: "var(--bg-light)" }}>
            <div style={{ fontWeight: 800, fontSize: 14, color: "var(--teal-dark)", fontVariantNumeric: "tabular-nums" }}>
              R$ {total.toFixed(0)}
            </div>
          </td>
        </tr>
        {drillKey && drillKey.startsWith(`${sd.idx}-`) && (() => {
          const comp = drillKey.split("-")[1];
          const items = sd[comp].items;
          if (!items || items.length === 0) return null;
          return (
            <tr>
              <td colSpan={6} style={{ padding: 0, background: "#f7faf7" }}>
                <div style={{ padding: "16px 28px 18px 44px", borderLeft: "3px solid var(--teal)" }}>
                  <div style={{ fontSize: 11.5, fontWeight: 700, color: "var(--teal-dark)", textTransform: "uppercase", letterSpacing: 1, marginBottom: 12 }}>
                    {sd.label} · {componentLabels[comp]} · {items.length} {items.length === 1 ? "item" : "itens"} · R$ {sd[comp].value.toFixed(0)}
                  </div>
                  <table style={{ width: "100%", fontSize: 12.5, borderCollapse: "collapse" }}>
                    <tbody>
                      {items.map((it, j) => {
                        return (
                          <tr key={j} style={{ borderBottom: j < items.length - 1 ? "1px solid var(--bg-soft)" : "none" }}>
                            <td style={{ padding: "7px 0", fontWeight: 600, color: "var(--text)", width: "55%" }}>{it.name}</td>
                            <td style={{ padding: "7px 0", color: "var(--text-mute)" }}>{it.sub || ""}</td>
                            <td style={{ padding: "7px 8px", textAlign: "right", fontVariantNumeric: "tabular-nums", fontWeight: 700, color: "var(--text)" }}>
                              {comp === "pap" && it.qty ? `${it.qty}× R$ ${it.price.toFixed(2)}` : `R$ ${(it.total || it.price).toFixed(2)}`}
                            </td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                </div>
              </td>
            </tr>
          );
        })()}
      </React.Fragment>
    );
  };

  // ===== Render etapas (subtotal por bloco) =====
  const renderStageBlock = (stage) => {
    const stageSeries = seriesData.filter(sd => stage.indices.includes(sd.idx));
    if (stageSeries.length === 0) return null;
    const stageLit = stageSeries.reduce((s, sd) => s + sd.lit.value, 0);
    const stageDid = stageSeries.reduce((s, sd) => s + sd.did.value, 0);
    const stagePap = stageSeries.reduce((s, sd) => s + sd.pap.value, 0);
    const stageCustom = stageSeries.reduce((s, sd) => s + sd.custom.value, 0);
    const stageTotal = stageLit + stageDid + stagePap + stageCustom;
    return (
      <React.Fragment key={stage.label}>
        <tr style={{ background: "var(--bg-light)" }}>
          <td colSpan={6} style={{ padding: "10px 14px", fontWeight: 800, fontSize: 11.5, color: "var(--teal-dark)", textTransform: "uppercase", letterSpacing: 0.8 }}>
            {stage.label}
            <span style={{ marginLeft: 12, color: "var(--text-mute)", fontSize: 11, fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>
              R$ {stageTotal.toFixed(0)}
            </span>
          </td>
        </tr>
        {stageSeries.map(renderSeriesRow)}
      </React.Fragment>
    );
  };

  // ===== Exportar Excel =====
  const exportExcel = (scope) => {
    if (!window.XLSX) { alert("Biblioteca de exportação ainda carregando..."); return; }
    const XLSX = window.XLSX;
    const wb = XLSX.utils.book_new();
    const SCOPE_LABEL = { lit: "Literatura", did: "Didáticos", pap: "Papelaria", custom: "Outros", all: "Lista Completa" };
    const components = scope === "all" ? ["lit","did","pap","custom"] : [scope];
    components.forEach(comp => {
      const rows = [];
      // Coluna "Variação 2025→2026" removida (reunião 26/08/2026) — vinha de
      // yoyPct, que é sintética/demo (ver comentário perto de useIPCA acima),
      // não fazia sentido exportar uma variação fake pra escola/família.
      rows.push(["Série", "Item", "Editora/Marca", "Qtde", "Preço unitário", "Total"]);
      seriesData.forEach(sd => {
        const items = sd[comp].items || [];
        if (items.length === 0 && sd[comp].value > 0) {
          // garante uma linha por série mesmo quando o item não foi resolvido no catálogo
          rows.push([
            sd.label,
            `(${comp === "lit" ? "Literatura" : comp === "did" ? "Didáticos" : comp === "pap" ? "Papelaria" : "Outros"} estimada)`,
            "",
            1,
            sd[comp].value,
            sd[comp].value,
          ]);
        }
        items.forEach(it => {
          rows.push([
            sd.label,
            it.name,
            it.sub || "",
            comp === "pap" ? (it.qty || 1) : 1,
            it.price,
            comp === "pap" ? (it.total || it.price) : it.price,
          ]);
        });
      });
      // se a aba ficou só com header, ainda assim mantém a aba pra deixar claro
      if (rows.length === 1) {
        rows.push(["—", "(sem itens nesta categoria)", "", "", "", ""]);
      }
      const ws = XLSX.utils.aoa_to_sheet(rows);
      // formata coluna preço/total como moeda
      const range = XLSX.utils.decode_range(ws["!ref"]);
      for (let R = 1; R <= range.e.r; R++) {
        ["E","F"].forEach(col => {
          const cell = ws[col + (R+1)];
          if (cell) cell.z = '"R$" #,##0.00';
        });
      }
      ws["!cols"] = [{wch:14},{wch:42},{wch:22},{wch:8},{wch:14},{wch:14}];
      XLSX.utils.book_append_sheet(wb, ws, SCOPE_LABEL[comp]);
    });
    const fname = `Lista2026_${SCOPE_LABEL[scope].replace(/\s/g,"_")}_${new Date().toISOString().slice(0,10)}.xlsx`;
    XLSX.writeFile(wb, fname);
    setExportOpen(false);
  };

  return (
    <div>
      <Heading
        eyebrow="Fechamento · Passo 1, Previsão de preço"
        title="Quanto vai custar a lista de 2026"
        subtitle="Comparativo por série e por componente. Clica em qualquer célula com valor pra abrir os itens daquela combinação." />

      <LexBubble pose={diffPP != null && diffPP < -1 ? "think" : "cheer"}>
        {lexCopy}
      </LexBubble>

      {/* Hero: estimativa média por aluno (foco principal). Coluna do IPCA
          removida (reunião 26/08/2026, ver comentário acima) — card ocupa a
          largura toda agora. */}
      <div style={{
        marginTop: 24,
        background: "linear-gradient(135deg, var(--teal), var(--teal-dark))",
        color: "#fff", borderRadius: 24, padding: "32px 38px",
        boxShadow: "var(--shadow-md)",
      }}>
        <div style={{ fontSize: 11.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: 1.4, opacity: 0.85, marginBottom: 10 }}>
          Estimativa média por aluno · ano letivo 2026
        </div>
        <div style={{ fontSize: 64, fontWeight: 800, lineHeight: 1, letterSpacing: -2.5, fontVariantNumeric: "tabular-nums" }}>
          R$ {perAluno2026.toLocaleString("pt-BR", { maximumFractionDigits: 0 })}
        </div>
        <div style={{ fontSize: 13.5, opacity: 0.92, marginTop: 10, fontWeight: 500 }}>
          Soma de literatura, didáticos e papelaria. Cada família compra a lista da sua série, com o desconto da escola no Scolados.
        </div>
      </div>

      {/* Toolbar. Toggle "Variação em %/R$" removido junto com o resto da
          variação YoY (reunião 26/08/2026) — não fazia mais sentido sem
          nenhuma DeltaPill sendo exibida. */}
      <div style={{ display: "flex", gap: 10, alignItems: "center", margin: "24px 0 14px" }}>
        <ToolGroup>
          <ToolBtn on={!groupByStage} onClick={() => setGroupByStage(false)}>Por série</ToolBtn>
          <ToolBtn on={groupByStage} onClick={() => setGroupByStage(true)}>Agrupado por etapa</ToolBtn>
        </ToolGroup>
        <div style={{ flex: 1 }}/>
        <div style={{ position: "relative" }}>
          <button
            onClick={() => exportExcel("all")}
            style={{
              padding: "8px 14px", fontSize: 12.5, fontWeight: 700,
              background: "var(--orange)", border: "1px solid var(--orange)", color: "#fff",
              borderRadius: 8, cursor: "pointer", fontFamily: "inherit",
              display: "flex", alignItems: "center", gap: 6,
              boxShadow: "0 1px 2px rgba(0,0,0,0.08)",
            }}
            title="Exporta um Excel com 4 abas: Literatura · Didáticos · Papelaria · Outros"
          >
            ↓ Exportar Excel <span style={{ fontSize: 10, opacity: 0.85, fontWeight: 600 }}>(4 abas)</span>
          </button>
          <button
            onClick={() => setExportOpen(o => !o)}
            style={{
              padding: "8px 10px", fontSize: 12.5, fontWeight: 700,
              background: "#fff", border: "1px solid var(--border)", color: "var(--text-mute)",
              borderRadius: 8, cursor: "pointer", fontFamily: "inherit",
              marginLeft: 4,
            }}
            title="Exportar uma categoria isolada"
          >
            ▼
          </button>
          {exportOpen && (
            <div style={{
              position: "absolute", top: "calc(100% + 4px)", right: 0, zIndex: 10,
              background: "#fff", border: "1px solid var(--border)", borderRadius: 10,
              boxShadow: "var(--shadow-md)", padding: 6, minWidth: 220,
            }}>
              <div style={{ fontSize: 10, fontWeight: 700, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.7, padding: "6px 10px 4px" }}>
                Exportar só uma categoria
              </div>
              {[
                ["lit", "📚 Só Literatura"],
                ["did", "📘 Só Didáticos"],
                ["pap", "✏️ Só Papelaria"],
                ["custom", "🎒 Só Outros"],
              ].map(([id, label]) => (
                <button key={id}
                  onClick={() => exportExcel(id)}
                  style={{
                    width: "100%", textAlign: "left", padding: "9px 12px",
                    background: "transparent", border: "none", borderRadius: 6,
                    cursor: "pointer", fontSize: 12.5, fontWeight: 600, color: "var(--text)",
                    fontFamily: "inherit",
                  }}
                  onMouseEnter={e => e.currentTarget.style.background = "var(--bg-soft)"}
                  onMouseLeave={e => e.currentTarget.style.background = "transparent"}
                >
                  {label}
                </button>
              ))}
            </div>
          )}
        </div>
      </div>

      {/* Tabela matriz */}
      <div style={{ background: "#fff", border: "1px solid var(--border)", borderRadius: 16, overflow: "hidden", boxShadow: "var(--shadow-sm)" }}>
        <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
          <thead>
            <tr style={{ background: "var(--bg-light)", borderBottom: "1px solid var(--border)" }}>
              <th style={{ padding: "11px 14px 11px 28px", textAlign: "left", fontSize: 11, fontWeight: 700, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.7 }}>Série</th>
              <th style={{ padding: "11px 14px", textAlign: "right", fontSize: 11, fontWeight: 700, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.7 }}>📚 Literatura</th>
              <th style={{ padding: "11px 14px", textAlign: "right", fontSize: 11, fontWeight: 700, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.7 }}>📘 Didáticos</th>
              <th style={{ padding: "11px 14px", textAlign: "right", fontSize: 11, fontWeight: 700, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.7 }}>✏️ Papelaria</th>
              <th style={{ padding: "11px 14px", textAlign: "right", fontSize: 11, fontWeight: 700, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.7 }}>🎒 Outros</th>
              <th style={{ padding: "11px 14px", textAlign: "right", fontSize: 11, fontWeight: 700, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.7 }}>TOTAL</th>
            </tr>
          </thead>
          <tbody>
            {groupByStage
              ? STAGES.map(renderStageBlock)
              : seriesData.map(renderSeriesRow)
            }
          </tbody>
        </table>
        {/* Rodapé com legenda de cores (caiu/subiu/novo) e "clique numa
            célula..." removido (reunião 26/08/2026) — as cores eram baseadas
            na variação sintética que foi tirada da tela, e o texto de dica
            também saiu a pedido da Joana. */}
      </div>
    </div>
  );
}

// Pílula de variação: mostra % ou R$ conforme showAs
function DeltaPill({ pct, novo, showAs, baseValue, small }) {
  if (novo) {
    return <div style={{ display: "inline-block", marginTop: 3, padding: small ? "1px 5px" : "1.5px 6px", background: "#e9eafd", color: "#2c4ffc", fontSize: small ? 9.5 : 10.5, fontWeight: 700, borderRadius: 4, lineHeight: 1.2 }}>novo</div>;
  }
  if (pct == null) return null;
  const abs = Math.abs(pct);
  const flat = abs < 3;
  let bg, color, label;
  if (flat) {
    bg = "var(--bg-soft)"; color = "var(--text-mute)";
    label = showAs === "pct" ? `${pct >= 0 ? "+" : ""}${pct.toFixed(1)}%` : "flat";
  } else if (pct > 0) {
    bg = abs > 12 ? "#f8d8df" : "#f8e3e8";
    color = "#c44d6a";
    label = showAs === "pct"
      ? `↑ +${pct.toFixed(1)}%`
      : `↑ +R$ ${(baseValue * pct / 100).toFixed(0)}`;
  } else {
    bg = "#e8f3ea"; color = "#4f9d57";
    label = showAs === "pct"
      ? `↓ ${pct.toFixed(1)}%`
      : `↓ R$ ${(baseValue * pct / 100).toFixed(0)}`;
  }
  return (
    <div style={{
      display: "inline-block", marginTop: 3, padding: small ? "1px 5px" : "1.5px 6px",
      background: bg, color, fontSize: small ? 9.5 : 10.5, fontWeight: 700, borderRadius: 4,
      fontVariantNumeric: "tabular-nums", lineHeight: 1.2,
      ...(abs > 12 ? { fontWeight: 800 } : {}),
    }}>
      {label}
    </div>
  );
}

function ToolGroup({ children }) {
  return (
    <div style={{ display: "flex", background: "#fff", border: "1px solid var(--border)", borderRadius: 10, padding: 3, boxShadow: "var(--shadow-sm)" }}>
      {children}
    </div>
  );
}
function ToolBtn({ on, onClick, children }) {
  return (
    <button onClick={onClick} style={{
      padding: "7px 14px", fontSize: 12.5, fontWeight: 600,
      background: on ? "var(--teal)" : "transparent",
      color: on ? "#fff" : "var(--text-mute)",
      borderRadius: 7, cursor: "pointer", border: "none", fontFamily: "inherit",
      whiteSpace: "nowrap",
    }}>{children}</button>
  );
}
function Legend({ swatch, children }) {
  return (
    <span style={{ display: "flex", alignItems: "center", gap: 6 }}>
      <span style={{ width: 12, height: 12, borderRadius: 3, background: swatch }}/>
      {children}
    </span>
  );
}

// Step 10: Recommendations per series (swipeable)
const SERIES_LIST = ["1º ano","2º ano","3º ano","4º ano","5º ano","6º ano","7º ano","8º ano","9º ano","1º EM","2º EM","3º EM"];

// Acha a turma (grade) real vinda da API que corresponde à posição `seriesIdx`
// da lista de séries ATIVA da escola (`seriesList`, ver resolveSeriesList em
// shared.jsx — já não é mais sempre a lista fixa de 12, pode ter Educação
// Infantil na frente, séries renomeadas, extras etc). Usado tanto no
// carrossel (Step9) quanto na revisão consolidada (Step10) e na confirmação
// final (Step11) pra achar a grade certa e persistir pin/unpin na lista.
// Casa por nome exato primeiro; se a turma real tiver um nome diferente do
// cadastrado (ex: "6º Ano B" vs "6º ano"), casa pelo padrão canônico
// embutido no nome (inferCanonicalSeriesName, shared.jsx); sem nenhum dos
// dois, cai no fallback posicional (mesmo comportamento de antes).
function findGradeForSeriesIndex(grades, seriesIdx, seriesList) {
  if (!Array.isArray(grades) || grades.length === 0) return null;
  const list = seriesList || SERIES_LIST;
  const targetName = list[seriesIdx];
  if (targetName) {
    const byExactName = grades.find((grade) => grade && grade.name === targetName);
    if (byExactName) return byExactName;
    const targetCanonical = window.inferCanonicalSeriesName ? window.inferCanonicalSeriesName(targetName) : null;
    if (targetCanonical) {
      const byCanonical = grades.find((grade) => window.inferCanonicalSeriesName(grade && grade.name) === targetCanonical);
      if (byCanonical) return byCanonical;
    }
  }
  return grades[seriesIdx] || null;
}

// Depois de remover um livro de verdade no backend (unpinBookFromGrade),
// o cache local de "quais itens crus essa série tem" — data.runtimeSelection
// .recommendedByGradeId[gradeId].items — continuava com o item removido.
// Esse cache é considerado "fresco" pra sempre (hasFreshCoverCache só olha
// coverVersion, uma constante fixa, nunca expira sozinho) e é persistido
// no localStorage — então um refresh recarregava esse cache velho sem
// bater no backend de novo, e o efeito de seed (que resemeia pinned a
// partir desses itens crus) trazia o livro "deletado" de volta. O request
// de remover funcionava (o backend removia de verdade), a view atualizava
// certinho, mas o refresh sempre voltava tudo (bug reportado pelo Alex,
// 27/08/2026, depois dos fixes anteriores de remover livro). Chamar isso
// junto de toda remoção bem-sucedida mantém o cache local em sincronia
// com o que o backend realmente tem, então não sobra nada pra "ressuscitar".
function purgeFromRuntimeItemsCache(setData, gradeId, rawIdsToRemove) {
  if (!gradeId || !Array.isArray(rawIdsToRemove) || rawIdsToRemove.length === 0) return;
  const idSet = new Set(rawIdsToRemove);
  setData(d => {
    const runtimeSelection = d.runtimeSelection || {};
    const byGrade = runtimeSelection.recommendedByGradeId || {};
    const entry = byGrade[gradeId];
    if (!entry || !Array.isArray(entry.items)) return d;
    const nextItems = entry.items.filter(raw => !(raw && idSet.has(raw.id)));
    if (nextItems.length === entry.items.length) return d;
    return {
      ...d,
      runtimeSelection: {
        ...runtimeSelection,
        recommendedItems: runtimeSelection.recommendedItems === entry.items ? nextItems : runtimeSelection.recommendedItems,
        recommendedByGradeId: { ...byGrade, [gradeId]: { ...entry, items: nextItems } },
      },
    };
  });
}

// Espelho de purgeFromRuntimeItemsCache: depois de pinar um livro com
// sucesso (pinBookToGrade confirmado), o mesmo cache de itens crus da
// série (recommendedByGradeId[gradeId].items) precisa saber do livro
// novo — senão, no próximo refresh (que confia nesse cache "fresco pra
// sempre" e não busca de novo no backend), `runtimeCatalogById` não tem
// o id do livro recém-pinado, e o filtro em `pinnedForSeries`
// (`runtimeCatalogById[id]`) descarta ele silenciosamente mesmo com o id
// ainda presente em `pinned[série]` local. Bug reportado pelo Alex,
// 27/08/2026, logo depois do fix de deletar: "ao adicionar e dar refresh
// não persiste" — espelho exato do mesmo problema, na direção contrária.
function addToRuntimeItemsCache(setData, gradeId, rawItem) {
  if (!gradeId || !rawItem || !rawItem.id) return;
  setData(d => {
    const runtimeSelection = d.runtimeSelection || {};
    const byGrade = runtimeSelection.recommendedByGradeId || {};
    const entry = byGrade[gradeId];
    if (!entry || !Array.isArray(entry.items)) return d;
    if (entry.items.some(raw => raw && raw.id === rawItem.id)) return d;
    const nextItems = [...entry.items, rawItem];
    return {
      ...d,
      runtimeSelection: {
        ...runtimeSelection,
        recommendedByGradeId: { ...byGrade, [gradeId]: { ...entry, items: nextItems } },
      },
    };
  });
}

// Histórico de busca de Step9_Recommend: mora em data.searchHistory[série],
// mesmo padrão de data.pinned — passa pelo autosave de app.jsx
// (localStorage escopado por conta+escola + sync no backend), então
// sobrevive a refresh/logout e não fica só na sessão em memória (pedido do
// Alex, 03/09/2026: "o historico tem que manter no usuario e nao só na
// sessao"). Os livros de fato (pra runtimeCatalogById resolver o id depois
// de um refresh) já ficam garantidos por snapshotRuntimeCatalog/
// restoreRuntimeCatalog em app.jsx, que espelha window.CATALOG inteiro
// (todo resultado de busca é empurrado nele) — não precisa duplicar aqui.
const SEARCH_HISTORY_LIMIT = 20;

// Marca `ids` como "vistos agora" no histórico de busca da série: cada um
// vai pra frente da lista (mais recente primeiro), sem duplicar, com teto
// de SEARCH_HISTORY_LIMIT (os mais antigos caem fora primeiro). Serve tanto
// pro clique explícito em "ver detalhes" (um id) quanto pro resultado
// inteiro de uma busca (vários ids de uma vez) — ver previewSearchResult()
// e os efeitos de busca (texto e por tema) em Step9_Recommend.
function touchSearchHistory(setData, series, ids) {
  const fresh = (Array.isArray(ids) ? ids : [ids]).filter(Boolean);
  if (!fresh.length) return;
  setData(d => {
    const bySeries = d.searchHistory || {};
    const prev = bySeries[series] || [];
    const freshSet = new Set(fresh);
    const rest = prev.filter((id) => !freshSet.has(id));
    const next = [...fresh, ...rest].slice(0, SEARCH_HISTORY_LIMIT);
    return { ...d, searchHistory: { ...bySeries, [series]: next } };
  });
}

// Tira um livro do histórico de busca da série — botão de lixeira no card
// do carrossel (Step9_Recommend), pedido do Alex, 03/09/2026. Só some do
// carrossel de fato quando o livro está lá por causa do histórico
// (extraManual); um livro pinado que também esteja no histórico continua
// aparecendo (via extraPinned) — remover daqui não desfaz o pin, por
// isso o botão só é mostrado pra quem não está pinado (ver isPinnedCur na
// renderização).
function removeFromSearchHistory(setData, series, bookId) {
  setData(d => {
    const bySeries = d.searchHistory || {};
    const prev = bySeries[series] || [];
    if (!prev.includes(bookId)) return d;
    return { ...d, searchHistory: { ...bySeries, [series]: prev.filter((id) => id !== bookId) } };
  });
}

function Step9_Recommend() {
  const { data, setData } = useApp();
  const COVER_CACHE_VERSION = 3;
  // Séries de verdade da escola (Passo 1), não a lista fixa de 12 — inclui
  // Educação Infantil, nomes renomeados e séries extras cadastradas lá.
  const activeSeries = resolveSeriesList(data);
  const [series, setSeries] = useState(0); // primeira série da lista da escola
  const [idx, setIdx] = useState(0);
  const [query, setQuery] = useState("");
  const [searchFocused, setSearchFocused] = useState(false);
  const [subjectFilter, setSubjectFilter] = useState("");
  // Livros achados pela busca manual, ainda não decididos (item 2, reunião
  // 03/08/2026) — ver previewSearchResult() mais abaixo. Também funciona
  // como histórico de busca: todo resultado que aparece numa busca (não só
  // o que o usuário clica em "ver detalhes") entra aqui, pra ficar
  // navegável no carrossel mesmo depois que a busca é limpada. Vem de
  // data.searchHistory (ver touchSearchHistory acima) — já nasce escopado
  // por série (cada série lê só a própria chave), então não precisa de
  // efeito pra limpar ao trocar de aba.
  const searchHistory = (data.searchHistory && data.searchHistory[series]) || [];
  const hasEN = data.scolexEN !== "none";
  const qtyPT = data.qtyPerYearPT ?? (data.qtyPerYear ?? 3);
  const qtyEN = hasEN ? (data.qtyPerYearEN ?? 2) : 0;
  const qty = qtyPT + qtyEN;

  // Dados reais do Scolist (escola/turmas vindas do login)
  const runtimeSelection = data.runtimeSelection || {};
  const runtimeSchools = Array.isArray(runtimeSelection.availableSchools) ? runtimeSelection.availableSchools : [];
  const runtimeGrades = Array.isArray(runtimeSelection.availableGrades) ? runtimeSelection.availableGrades : [];
  const runtimeSchoolId = runtimeSelection.schoolId || "";
  const runtimePeriod = runtimeSelection.period || String(new Date().getFullYear());
  const runtimeByGrade = (runtimeSelection.recommendedByGradeId && typeof runtimeSelection.recommendedByGradeId === "object")
    ? runtimeSelection.recommendedByGradeId
    : {};
  const inflightGradesRef = React.useRef(new Set());
  const [runtimeError, setRuntimeError] = useState("");
  const selectedRuntimeSchool = runtimeSchools.find((s) => s.id === runtimeSchoolId) || null;

  const gradeForCurrentSeries = React.useMemo(
    () => findGradeForSeriesIndex(runtimeGrades, series, activeSeries),
    [runtimeGrades, series, activeSeries]
  );

  const activeRuntimeGradeId = (gradeForCurrentSeries && gradeForCurrentSeries.id) || "";
  const activeRuntimeEntry = activeRuntimeGradeId ? runtimeByGrade[activeRuntimeGradeId] : null;
  const hasFreshCoverCache = (entry) => entry && Number(entry.coverVersion || 0) >= COVER_CACHE_VERSION;
  const runtimeItems = Array.isArray(activeRuntimeEntry && activeRuntimeEntry.items)
    ? activeRuntimeEntry.items
    : [];

  // Primeiro uso: se login trouxe escola/período mas não trouxe turmas no contexto local,
  // buscamos as grades automaticamente.
  useEffect(() => {
    let canceled = false;
    if (!runtimeSchoolId || !runtimePeriod) return undefined;
    if (runtimeGrades.length > 0) return undefined;

    (async () => {
      try {
        const grades = await window.ScolistAPI.getSchoolGrades(runtimeSchoolId, runtimePeriod);
        if (canceled) return;
        setData((d) => ({
          ...d,
          runtimeSelection: {
            ...(d.runtimeSelection || {}),
            availableGrades: grades,
          },
        }));
      } catch (error) {
        if (!canceled) {
          console.error(error);
          setRuntimeError("Não foi possível carregar as turmas da escola.");
        }
      }
    })();

    return () => { canceled = true; };
  }, [runtimeSchoolId, runtimePeriod, runtimeGrades.length, setData]);

  const loadRuntimeRecommendationsForGrade = React.useCallback(async (grade) => {
    if (!grade || !grade.id || !runtimeSchoolId || !runtimePeriod) return;
    if (hasFreshCoverCache(runtimeByGrade[grade.id])) return;
    if (inflightGradesRef.current.has(grade.id)) return;
    inflightGradesRef.current.add(grade.id);

    setRuntimeError("");

    try {
      const payload = await window.ScolistAPI.getRecommendedBooks({
        schoolId: runtimeSchoolId,
        period: runtimePeriod,
        gradeId: grade.id,
      });

      const items = Array.isArray(payload && payload.items) ? payload.items : [];
      // Série sem itens salvos no Scolist ainda não trava em erro — cai no
      // estado "calibração pendente", que dispara busca por tema (efeito
      // logo abaixo) quando há temas escolhidos, ou busca manual quando não.
      const isCalibrationPending = items.length === 0;

      setData((d) => {
        const prevSelection = d.runtimeSelection || {};
        const prevByGrade = (prevSelection.recommendedByGradeId && typeof prevSelection.recommendedByGradeId === "object")
          ? prevSelection.recommendedByGradeId
          : {};

        return {
          ...d,
          schoolName: (selectedRuntimeSchool && selectedRuntimeSchool.name) || d.schoolName,
          runtimeSelection: {
            ...prevSelection,
            schoolId: runtimeSchoolId,
            schoolName: (selectedRuntimeSchool && selectedRuntimeSchool.name) || prevSelection.schoolName || "",
            period: String(runtimePeriod),
            gradeId: grade.id,
            gradeName: grade.name || grade.id,
            gradeSeriesIdx: series,
            listId: (payload && payload.listId) || null,
            source: (payload && payload.source) || null,
            recommendedItems: items,
            recommendedByGradeId: {
              ...prevByGrade,
              [grade.id]: {
                items,
                listId: (payload && payload.listId) || null,
                source: (payload && payload.source) || null,
                gradeId: grade.id,
                gradeName: grade.name || grade.id,
                coverVersion: COVER_CACHE_VERSION,
                loadedAt: new Date().toISOString(),
                isCalibrationPending,
              },
            },
            updatedAt: new Date().toISOString(),
          },
        };
      });
    } catch (error) {
      console.error(error);
      setRuntimeError("Falha ao buscar livros recomendados para essa série.");
    } finally {
      inflightGradesRef.current.delete(grade.id);
    }
  }, [runtimeSchoolId, runtimePeriod, runtimeByGrade, selectedRuntimeSchool, series, setData]);

  // Fluxo principal: trocou a aba (série), carrega automaticamente os livros daquela série.
  useEffect(() => {
    setRuntimeError("");
    if (!gradeForCurrentSeries) return;
    if (hasFreshCoverCache(runtimeByGrade[gradeForCurrentSeries.id])) return;
    loadRuntimeRecommendationsForGrade(gradeForCurrentSeries);
  }, [series, gradeForCurrentSeries, runtimeByGrade, loadRuntimeRecommendationsForGrade]);

  // Mantém o contexto sincronizado com a aba ativa (mesmo quando os dados da série já estavam em cache).
  useEffect(() => {
    if (!gradeForCurrentSeries || !gradeForCurrentSeries.id) return;
    if (runtimeSelection.gradeId === gradeForCurrentSeries.id) return;

    setData((d) => ({
      ...d,
      runtimeSelection: {
        ...(d.runtimeSelection || {}),
        gradeId: gradeForCurrentSeries.id,
        gradeName: gradeForCurrentSeries.name || gradeForCurrentSeries.id,
        gradeSeriesIdx: series,
        updatedAt: new Date().toISOString(),
      },
    }));
  }, [gradeForCurrentSeries, runtimeSelection.gradeId, series, setData]);

  // sourceTag namespaceia o fallback de id sintético (ver safeIdRaw abaixo)
  // por origem (recomendação da turma vs. busca manual vs. busca por tema).
  // Sem isso, um item de recomendação e um resultado de busca sem isbn/id
  // real caíam no mesmo `runtime-${index+1}` (index é local a cada array
  // mapeado, sempre começando em 1) — dois livros diferentes ganhavam o
  // mesmo id, e o efeito que sincroniza pro CATALOG global (`Object.assign`
  // em cima do objeto já existente, mais abaixo) sobrescrevia em memória o
  // card da recomendação com os dados do livro buscado, sem mudar o
  // tamanho da lista — parecia que a recomendação tinha "sumido"/sido
  // substituída, quando na real os dois arrays nunca deveriam colidir.
  const normalizeRuntimeBook = (item, index, sourceTag = "runtime") => {
    const mapStock = (rawStock, statusText) => {
      const text = String(rawStock || "").toLowerCase();
      const status = String(statusText || "").toUpperCase();
      if (text.includes("baix") || text.includes("indispon") || text.includes("esgotad")) return "baixo";
      if (text.includes("alto") || text.includes("high") || text.includes("dispon")) return "alto";
      if (text.includes("medio") || text.includes("médio") || text.includes("medium")) return "médio";
      if (status.includes("APPROV")) return "alto";
      return "médio";
    };

    const rawLang = String(item && item.lang ? item.lang : "pt").toLowerCase();
    const lang = rawLang.startsWith("en") ? "en" : "pt";
    const isbn = (item && item.isbn ? String(item.isbn) : "").trim();
    const rawId = (item && item.id ? String(item.id) : "").trim();
    const safeIdRaw = (isbn || rawId || `${sourceTag}-${index + 1}`).replace(/[^a-zA-Z0-9_-]/g, "");
    const safeId = safeIdRaw || `${sourceTag}-${index + 1}`;
    const status = String((item && item.status) || "").toUpperCase();
    const pages = Number.isFinite(item && item.pages) ? Number(item.pages) : 0;
    const stock = mapStock(item && item.stock, status);
    const level = (item && item.level ? String(item.level) : "").trim();
    // Sinopse real vem de item.summary (mapeada em api.js a partir da coluna
    // `description` do catálogo, ou persistida no item quando o livro é
    // pinado — ver buildItemFromBook/collectItemsFromGroup). Fica cru aqui
    // (string vazia quando não existe) — o texto de fallback "não
    // disponível" é responsabilidade de quem exibe (ver bloco de sinopse
    // mais abaixo), não do dado em si: a página da família (pages-familia.jsx)
    // lê esse mesmo campo direto de CATALOG e só quer mostrar o bloco
    // quando existe sinopse de verdade, sem fallback nenhum.
    const summary = (item && item.summary ? String(item.summary) : "").trim();
    const themesFromApi = Array.isArray(item && item.themes) ? item.themes.filter(Boolean).slice(0, 5) : [];

    const imageUrl = (() => {
      const local = isbn && window.ISBN_COVERS && window.ISBN_COVERS[isbn];
      if (local) return local;
      const direct = String(
        (item && (item.imageUrl || item.image || item.coverUrl)) || ""
      ).trim();
      return direct || null;
    })();

    const palette = lang === "en"
      ? ["#2c4ffc", "#8fa2ff", "#ffffff"]
      : ["#2f97a1", "#7dc7ce", "#ffffff"];

    return {
      id: `rt-${safeId}`,
      bookId: (item && item.bookId) || rawId || null,
      // Na lista do Scolist, o id do item na lista já É a identidade real
      // (não existe "bookId de catálogo" separado de "id na série") — semeia
      // gradeBookId a partir do id real assim que o item é carregado, não só
      // depois de um pin nesta sessão, senão itens já salvos antes não
      // conseguem ser removidos até serem repinados.
      gradeBookId: (item && item.gradeBookId) || rawId || null,
      groupName: (item && item.groupName) || "",
      sampleUrl: (item && item.sampleUrl) || null,
      isbn,
      title: (item && item.title) || "Livro sem título",
      author: (item && item.author) || "Autor não informado",
      editora: (item && item.publisher) || "Editora não informada",
      publisher: (item && item.publisher) || "",
      provider: (item && item.provider) || "",
      lang,
      langLabel: lang === "en" ? "Inglês" : "Português",
      series: activeSeries[series],
      pages,
      level: level || String(Math.max(100, 120 + (index * 7))),
      stock,
      stockLabel: (item && item.availabilityMessage) || stock,
      price: typeof item.price === "number" ? item.price : 0,
      summary,
      why: (item && item.provider)
        ? `Livro importado da lista real da turma selecionada (${item.provider}).`
        : "Livro importado da lista real da turma selecionada.",
      themes: themesFromApi.length > 0 ? themesFromApi : (Array.isArray(data.themes) ? data.themes.slice(0, 4) : []),
      palette,
      shape: index % 2 === 0 ? "square" : "circle",
      imageUrl,
    };
  };

  const runtimeBooksForSlide = React.useMemo(
    () => runtimeItems.map((item, index) => normalizeRuntimeBook(item, index, "grade")),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [runtimeItems, series, JSON.stringify(data.themes || [])]
  );

  useEffect(() => {
    if (!runtimeBooksForSlide.length) return;

    runtimeBooksForSlide.forEach((book) => {
      const existing = CATALOG.find((catalogBook) => catalogBook.id === book.id);
      if (existing) {
        const keepGradeBookId = book.gradeBookId || existing.gradeBookId;
        Object.assign(existing, book, { gradeBookId: keepGradeBookId });
      } else {
        CATALOG.push(book);
      }
    });
  }, [runtimeBooksForSlide]);

  // Todo item que já está no grupo literário salvo no backend já é, por
  // definição, um livro pinado (pinBookToGrade é a única forma de um item
  // entrar nesse grupo) — mas data.pinned é estado local e não nascia
  // sabendo disso. Sem este seed, dar logout (que zera data.pinned) e
  // logar de novo fazia a lista salva "sumir" da tela: os livros
  // continuavam no Scolist, só não apareciam marcados como pinados até
  // serem repinados manualmente.
  useEffect(() => {
    if (!runtimeBooksForSlide.length) return;
    // Deduplicado: se o grupo salvo no Scolist tem mais de um item pro
    // mesmo ISBN (dado antigo, de antes do fix de idempotência em
    // pinBookToGrade em api.js), vários itens brutos normalizam pro mesmo
    // id "rt-<isbn>" — sem o Set aqui, cada um deles entrava como uma
    // cópia extra em pinned[série], e o card duplicava na tela.
    const idsToSeed = Array.from(new Set(runtimeBooksForSlide.map((book) => book.id)));

    setData((d) => {
      const prevPinned = d.pinned || {};
      const existingRaw = prevPinned[series] || [];
      // Limpa duplicata que já esteja salva de sessões anteriores (antes
      // deste fix) — reescreve pinned[série] sem repetição, o que também
      // corrige de graça a soma de preço (Step8_Pricing) e a lista final
      // (Step11_Confirm/export), que leem pinned[série] direto e não tinham
      // esse dedup.
      const existing = Array.from(new Set(existingRaw));
      const missing = idsToSeed.filter((id) => !existing.includes(id));
      if (missing.length === 0 && existing.length === existingRaw.length) return d;
      return { ...d, pinned: { ...prevPinned, [series]: [...existing, ...missing] } };
    });
  }, [runtimeBooksForSlide, series, setData]);

  const isRuntimeBookId = (id) => String(id || "").startsWith("rt-");
  // Not memoized on purpose: CATALOG is a mutable global array (search results
  // get pushed into it outside of React state), so a memo keyed on
  // runtimeBooksForSlide alone goes stale the moment a search adds a book
  // that isn't part of the active grade's recommended items —
  // previewSearchResult() would then silently no-op because
  // runtimeCatalogById[bookId] comes back empty.
  // Recomputing every render keeps this in sync with CATALOG at ~O(catalog
  // size), which is small enough here not to matter.
  const runtimeCatalog = (() => {
    const byId = new Map();

    CATALOG
      .filter((book) => isRuntimeBookId(book && book.id))
      .forEach((book) => byId.set(book.id, book));

    // The active grade's API result is already available during this render.
    // Do not wait for the CATALOG sync effect, otherwise badges/actions lag one click behind.
    runtimeBooksForSlide.forEach((book) => byId.set(book.id, book));

    return Array.from(byId.values());
  })();
  const runtimeCatalogById = Object.fromEntries(runtimeCatalog.map((book) => [book.id, book]));

  // Livro do slide deve vir apenas da fonte real carregada para a série ativa.
  const currentSeries = activeSeries[series];
  const runtimeAppliesCurrentSeries = !!gradeForCurrentSeries && !!activeRuntimeEntry;
  // Já sabemos qual turma é (gradeForCurrentSeries existe), mas o fetch
  // dela (loadRuntimeRecommendationsForGrade) ainda não voltou —
  // activeRuntimeEntry é null nesse meio-tempo. Sem distinguir isso de
  // "carreguei e realmente não tem nada", a tela mostrava "Nenhum título
  // foi adicionado ainda" — idêntico visualmente a "o livro sumiu" — só
  // que era carregamento, não ausência de dado. Explica o "às vezes
  // aparece, às vezes não" no refresh: depende de quão rápido a pessoa
  // olha a tela em relação a esse fetch (reportado pelo Alex, 27/08/2026).
  const isLoadingRuntimeData = !!gradeForCurrentSeries && !activeRuntimeEntry && !runtimeError;
  const seriesBooks = runtimeAppliesCurrentSeries
    ? runtimeBooksForSlide
    : [];
  const neighbors = [];
  const pool = [...seriesBooks, ...neighbors];
  // Sort: PT first then EN (unless qtyPT is 0), and by within-series preference
  // Build a BALANCED suggestion list: up to N/2 PT + N/2 EN, interleaved.
  const rawSuggestions = pool.filter(b => b.lang === "pt" || hasEN);
  const ptPool = rawSuggestions.filter(b => (b.lang || "pt") === "pt");
  const enPool = rawSuggestions.filter(b => b.lang === "en");
  // Interleave PT, EN, PT, EN… up to max 5+5 by default
  const interleave = (a, b) => {
    const out = [];
    const n = Math.max(a.length, b.length);
    for (let i = 0; i < n; i++) {
      if (a[i]) out.push(a[i]);
      if (b[i]) out.push(b[i]);
    }
    return out;
  };
  const suggestions = hasEN ? interleave(ptPool, enPool) : ptPool;

  const pinned = data.pinned || {};
  const pinnedForSeriesRaw = pinned[series] || [];
  // dedup defensivo: mesmo se pinned[série] tiver id repetido (dado salvo
  // antes do fix de duplicata), a tela nunca deve renderizar o mesmo livro
  // duas vezes nem contar errado a cota.
  const pinnedForSeries = Array.from(new Set(pinnedForSeriesRaw)).filter((id) => isRuntimeBookId(id) && runtimeCatalogById[id]);
  const pinnedBooks = pinnedForSeries.map((id) => runtimeCatalogById[id]).filter(Boolean);
  const pinnedPT = pinnedBooks.filter(b => b.lang === "pt").length;
  const pinnedEN = pinnedBooks.filter(b => b.lang === "en").length;

  // Show 10 ideas at a time; "Ver mais ideias" reveals +10. Pinned books are
  // ALWAYS kept in the carousel so user can review/remove even if beyond shownCount.
  const [shownCount, setShownCount] = useState(10);
  // Preview modal state, kind is "sample" (book sample) or "guide" (teacher guide)
  const [preview, setPreview] = useState(null); // { kind, book }
  // Reset carousel + shown count quando troca de série. Também limpa a
  // busca (query/assunto/foco): sem isso, uma busca digitada na série
  // anterior ficava com o dropdown "pendurado" depois de trocar de aba, e
  // clicar num resultado dali gravava o livro no histórico da série NOVA
  // — previewSearchResult() usa o `series` de quando é clicado, não o de
  // quando a busca foi feita, então dava pra vazar resultado de uma série
  // pro histórico de outra. Pedido do Alex, 03/09/2026: "quando eu voltar
  // em cada ano, tem que exibir no carousel so os livros que foram
  // buscado para aqueles anos".
  useEffect(() => {
    setIdx(0);
    setShownCount(10);
    setQuery("");
    setSubjectFilter("");
    setSearchFocused(false);
  }, [series]);
  // Sinopse: "ver mais" não deve ficar expandida ao trocar de livro no carrossel
  const [summaryExpanded, setSummaryExpanded] = useState(false);
  useEffect(() => { setSummaryExpanded(false); }, [idx, series]);

  const manualBooks = searchHistory.map((id) => runtimeCatalogById[id]).filter(Boolean);
  const firstN = suggestions.slice(0, shownCount);
  const extraPinned = suggestions.filter(b => pinnedForSeries.includes(b.id) && !firstN.includes(b));
  const extraManual = manualBooks.filter(b => !firstN.includes(b) && !extraPinned.includes(b));
  // searchHistory vai sempre na frente — é o que o usuário acabou de buscar,
  // previewSearchResult() já chama setIdx(0) contando com essa ordem.
  const visible = [...extraManual, ...firstN, ...extraPinned];
  const safeSuggestions = visible.length ? visible : suggestions;
  const hasMore = suggestions.length > shownCount;
  const cur = safeSuggestions.length ? safeSuggestions[idx % safeSuggestions.length] : null;
  const curWithCover = React.useMemo(() => {
    if (!cur) return cur;

    // Prioridade 1: capa local pelo ISBN (isbn-covers.js)
    const isbnLocal = cur.isbn && window.ISBN_COVERS && window.ISBN_COVERS[String(cur.isbn).trim()];
    if (isbnLocal) return { ...cur, imageUrl: isbnLocal };

    if (cur.imageUrl) return cur;

    const normalizeIsbn = (value) => String(value || "").replace(/\D/g, "");
    const normalizedTitle = String(cur.title || "").toLowerCase().trim();
    const normalizedAuthor = String(cur.author || "").toLowerCase().trim();
    const normalizedIsbn = normalizeIsbn(cur.isbn);

    const byIsbn = normalizedIsbn
      ? runtimeBooksForSlide.find((book) => normalizeIsbn(book.isbn) === normalizedIsbn && book.imageUrl)
      : null;
    if (byIsbn) return { ...cur, imageUrl: byIsbn.imageUrl };

    const byTitle = normalizedTitle
      ? runtimeBooksForSlide.find((book) => {
          const bookTitle = String(book.title || "").toLowerCase().trim();
          const titleMatch = bookTitle === normalizedTitle || bookTitle.includes(normalizedTitle) || normalizedTitle.includes(bookTitle);
          const authorMatch = !normalizedAuthor || String(book.author || "").toLowerCase().trim() === normalizedAuthor;
          return titleMatch && authorMatch && book.imageUrl;
        })
      : null;

    if (byTitle) return { ...cur, imageUrl: byTitle.imageUrl };

    return cur;
  }, [cur, runtimeBooksForSlide]);

  const curLang = (cur && cur.lang) || "pt";
  const isAtQuotaForCur = curLang === "pt" ? pinnedPT >= qtyPT : pinnedEN >= qtyEN;
  const isPinnedCur = !!cur && pinnedForSeries.includes(cur.id);
  const carouselArrowTop = 250; // fixed so arrows don't jump when content height changes

  const nextBook = () => {
    if (!safeSuggestions.length) return;
    setIdx(i => (i + 1) % safeSuggestions.length);
  };
  const prevBook = () => {
    if (!safeSuggestions.length) return;
    setIdx(i => (i - 1 + safeSuggestions.length) % safeSuggestions.length);
  };

  // O backend não cria a série (Grade) sozinho quando o ano letivo é criado —
  // só existe se alguém já chamou POST .../grade pra ela. Como o wizard não
  // tem uma tela própria pra isso, criamos sob demanda ao abrir a série.
  const ensureActiveGradeId = React.useCallback(async () => {
    if (activeRuntimeGradeId) return activeRuntimeGradeId;
    if (!window.ScolistAPI || typeof window.ScolistAPI.ensureGrade !== "function" || !runtimeSchoolId) return "";
    try {
      const gradeId = await window.ScolistAPI.ensureGrade(runtimeSchoolId, runtimePeriod, activeSeries[series]);
      setData((d) => {
        const prevSelection = d.runtimeSelection || {};
        const prevGrades = Array.isArray(prevSelection.availableGrades) ? prevSelection.availableGrades : [];
        if (prevGrades.some((g) => g.id === gradeId)) return d;
        return {
          ...d,
          runtimeSelection: {
            ...prevSelection,
            availableGrades: [...prevGrades, { id: gradeId, name: activeSeries[series] }],
          },
        };
      });
      return gradeId;
    } catch (error) {
      console.error("Falha ao criar série:", error);
      setRuntimeError("Não foi possível criar essa série.");
      return "";
    }
  }, [activeRuntimeGradeId, runtimeSchoolId, runtimePeriod, series, setData]);

  // O backend só cria a Grade sob demanda (ver comentário acima), mas até
  // pouco tempo atrás isso só acontecia no primeiro pin — então uma série
  // que o coordenador nunca pinou nada não tinha Grade nenhuma no backend,
  // e a recomendação ficava vazia pra sempre, mesmo depois de preencher o
  // onboarding inteiro (reclamação real de cliente, 11/08/2026). Agora cria
  // assim que a aba da série é aberta, não só no pin.
  useEffect(() => {
    if (gradeForCurrentSeries || !runtimePeriod) return;
    ensureActiveGradeId();
  }, [series, gradeForCurrentSeries, runtimePeriod, ensureActiveGradeId]);

  // Persiste pin/unpin no Scolist em segundo plano (a lista local já
  // atualizou na hora — isso só reflete a mudança na lista real do backend,
  // que não tem upsert por item: cada pin/unpin reenvia a lista inteira via
  // PUT /list/:listId). Falha não desfaz a ação local, só avisa: o dado real
  // da curadoria mora no backend, então vale tentar de novo em vez de perder
  // a seleção do usuário.
  const syncPinToList = React.useCallback(async (book) => {
    if (!window.ScolistAPI || typeof window.ScolistAPI.pinBookToGrade !== "function") return;
    if (!book || !runtimeSchoolId || !runtimePeriod) return;
    const gradeId = await ensureActiveGradeId();
    if (!gradeId) return;
    try {
      const result = await window.ScolistAPI.pinBookToGrade(runtimeSchoolId, runtimePeriod, gradeId, book);
      if (result && result.id) {
        book.gradeBookId = result.id;
        // Mesmo formato que collectItemsFromGroup (api.js) produziria pro
        // item real — mantém o cache local em sincronia com o backend
        // (ver addToRuntimeItemsCache acima).
        addToRuntimeItemsCache(setData, gradeId, {
          id: result.id,
          title: book.title,
          author: book.author,
          isbn: book.isbn,
          publisher: book.publisher || book.editora || "",
          lang: book.lang,
          price: typeof book.price === "number" ? book.price : 0,
          imageUrl: book.imageUrl || null,
          sampleUrl: book.sampleUrl || null,
          groupName: book.groupName || "",
          status: "WAITING_APPROVAL",
          optional: true,
          observation: "",
        });
      }
    } catch (error) {
      console.error("Falha ao salvar livro na lista:", error);
      setRuntimeError("Não foi possível salvar esse livro na lista. Tente de novo.");
    }
  }, [runtimeSchoolId, runtimePeriod, ensureActiveGradeId, setData]);

  // Remove TODOS os itens crus do grupo que batem com o ISBN desse livro,
  // não só o gradeBookId que o front tinha guardado — listas antigas podem
  // ter mais de um item pro mesmo ISBN (bug de duplicata do pinBookToGrade,
  // já corrigido, mas o dado velho ainda está salvo em algumas escolas).
  // Sem isso, clicar ✕ numa dessas listas removia só um dos duplicados: o
  // card sumia da tela (a UI já dedupa, ver pinnedForSeries acima) mas o
  // grupo real no Scolist ainda ficava com cópia(s) sobrando.
  // Devolve true/false (sucesso) — o chamador só tira o livro da tela
  // depois de confirmar que o backend removeu de verdade (ver pin()/
  // removeBook() abaixo). Antes essa função era chamada sem esperar
  // (fogo-e-esquece) e engolia o próprio erro sem avisar quem chamou:
  // se o PUT falhasse ou não terminasse antes da série recarregar, o
  // efeito de seed (linha ~1234) achava o livro "faltando" localmente e
  // resemeava ele de volta a partir do backend (ainda não atualizado) —
  // por isso "deletar e dar refresh, o livro volta" (reportado pelo
  // Alex, 27/08/2026).
  const syncUnpinFromList = React.useCallback(async (book) => {
    if (!window.ScolistAPI || typeof window.ScolistAPI.unpinBookFromGrade !== "function") return true;
    if (!runtimeSchoolId || !runtimePeriod || !activeRuntimeGradeId) return true;

    const isbn = book && book.isbn;
    const byIsbn = isbn ? runtimeItems.filter((raw) => raw && raw.isbn === isbn) : [];
    const idsToRemove = byIsbn.length > 0
      ? Array.from(new Set(byIsbn.map((raw) => raw.id).filter(Boolean)))
      : [book && (book.gradeBookId || book.bookId)].filter(Boolean);
    if (idsToRemove.length === 0) return true;

    try {
      for (const itemId of idsToRemove) {
        // eslint-disable-next-line no-await-in-loop
        await window.ScolistAPI.unpinBookFromGrade(runtimeSchoolId, runtimePeriod, activeRuntimeGradeId, itemId);
      }
      if (book) book.gradeBookId = null;
      purgeFromRuntimeItemsCache(setData, activeRuntimeGradeId, idsToRemove);
      return true;
    } catch (error) {
      console.error("Falha ao remover livro da lista:", error);
      setRuntimeError("Não foi possível remover esse livro da lista. Tente de novo.");
      return false;
    }
  }, [runtimeSchoolId, runtimePeriod, activeRuntimeGradeId, runtimeItems, setData]);

  // pin/removeBook calculavam `list` a partir de `pinnedForSeries` (valor
  // capturado no closure do render atual), não do estado mais recente — um
  // clique duplo no botão pequeno (26x26px) disparava dois cliques antes do
  // primeiro re-render, e os dois cálculos partiam do mesmo array sem o
  // livro ainda, cada um adicionando cur.id por conta própria: resultado,
  // o mesmo livro entrava duas vezes em pinned[series] e aparecia repetido
  // lado a lado na tira de pinados (bug reportado pela Joana, reunião
  // 26/08/2026: "ele copiou pro cara do lado"). Trocado pra updater
  // funcional (lê o `d` mais recente) com guarda contra duplicata — mesmo
  // padrão que addToSeries já usa em steps-didaticos.jsx.
  // Remover agora é pessimista: só some da tela (e do localStorage, via o
  // setData) depois que o backend confirmar. Pinar continua otimista —
  // a reclamação era especificamente sobre remover "voltar" depois de um
  // refresh, não sobre adicionar.
  const [removingIds, setRemovingIds] = useState(() => new Set());

  const pin = async () => {
    if (!cur) return;
    if (!isPinnedCur && isAtQuotaForCur) return; // blocked
    if (!isPinnedCur) {
      setData(d => {
        const prev = (d.pinned || {})[series] || [];
        const list = prev.includes(cur.id) ? prev : [...prev, cur.id];
        return { ...d, pinned: { ...(d.pinned || {}), [series]: list } };
      });
      syncPinToList(cur);
      return;
    }
    setRemovingIds(prev => new Set(prev).add(cur.id));
    const ok = await syncUnpinFromList(cur);
    setRemovingIds(prev => { const next = new Set(prev); next.delete(cur.id); return next; });
    if (!ok) return; // erro já avisado por syncUnpinFromList; livro continua na lista
    setData(d => {
      const prev = (d.pinned || {})[series] || [];
      const list = prev.filter(x => x !== cur.id);
      return { ...d, pinned: { ...(d.pinned || {}), [series]: list } };
    });
  };

  const removeBook = async (bookId) => {
    const book = runtimeCatalogById[bookId];
    setRemovingIds(prev => new Set(prev).add(bookId));
    const ok = book ? await syncUnpinFromList(book) : true;
    setRemovingIds(prev => { const next = new Set(prev); next.delete(bookId); return next; });
    if (!ok) return;
    setData(d => {
      const prev = (d.pinned || {})[series] || [];
      const list = prev.filter(x => x !== bookId);
      return { ...d, pinned: { ...(d.pinned || {}), [series]: list } };
    });
  };

  // Clicar num resultado da busca manual NÃO pina direto — só traz o livro
  // pro carrossel, no mesmo formato de card de sugestão, pra o usuário ver
  // detalhes (nível Scolex etc.) antes de decidir. O pin de fato só
  // acontece pelo botão "Adicionar à lista" do card de detalhe (pin(),
  // já existente). searchHistory (data.searchHistory, ver touchSearchHistory
  // no topo do arquivo) declarado lá em cima (perto de idx/query) porque
  // suggestions/visible, calculados antes deste ponto, já precisam dele.
  const previewSearchResult = (bookId) => {
    const book = runtimeCatalogById[bookId];
    if (!book) return;
    if (pinnedForSeries.includes(bookId)) return; // already there
    // Sempre põe o livro clicado na frente (touchSearchHistory já
    // reprependa removendo ocorrência antiga) — senão idx=0 (logo abaixo)
    // mostra o pick anterior em vez do que acabou de ser buscado.
    touchSearchHistory(setData, series, [bookId]);
    setQuery("");
    setSearchFocused(false);
    setIdx(0); // searchHistory é sempre prependado em safeSuggestions
  };

  // Busca ao vivo na base compartilhada de livros (proxy pro Supabase via
  // GET /books/search, ver README-INTEGRACAO-SCOLIST.md §8). Debounce
  // simples de 350ms.
  const q = query.trim().toLowerCase();
  const [liveSearchResults, setLiveSearchResults] = useState([]);
  const [liveSearchLoading, setLiveSearchLoading] = useState(false);

  useEffect(() => {
    if (query.trim().length < 2 && !subjectFilter) {
      setLiveSearchResults([]);
      setLiveSearchLoading(false);
      return;
    }
    if (!window.ScolistAPI || typeof window.ScolistAPI.searchBooks !== "function") return;

    let cancelled = false;
    setLiveSearchLoading(true);

    const handle = setTimeout(async () => {
      try {
        const rawResults = await window.ScolistAPI.searchBooks(query, subjectFilter ? { tags: [subjectFilter] } : undefined);
        if (cancelled) return;
        const normalized = rawResults.map((item, i) => normalizeRuntimeBook(item, i, "search"));
        normalized.forEach((book) => {
          const existing = CATALOG.find((catalogBook) => catalogBook.id === book.id);
          if (existing) {
            const keepGradeBookId = book.gradeBookId || existing.gradeBookId;
            Object.assign(existing, book, { gradeBookId: keepGradeBookId });
          } else {
            CATALOG.push(book);
          }
        });
        setLiveSearchResults(normalized);
        // NÃO entra no histórico de busca (searchHistory) aqui — resultado
        // só aparece no dropdown (searchResults, abaixo). Bug reportado pela
        // Joana, 08/09/2026: livros com título parecido ao termo buscado
        // apareciam sozinhos no carrossel de recomendação, sem o usuário ter
        // clicado em nada. previewSearchResult() já registra no histórico o
        // livro que o usuário de fato clicar em "ver detalhes" — só ali é
        // que o livro deve entrar no carrossel.
      } catch (err) {
        if (!cancelled) setLiveSearchResults([]);
      } finally {
        if (!cancelled) setLiveSearchLoading(false);
      }
    }, 350);

    return () => { cancelled = true; clearTimeout(handle); };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [query, subjectFilter]);

  // Quando a recomendação automática ainda não existe pra série (calibração
  // pendente, ver isCalibrationPending em loadRuntimeRecommendationsForGrade)
  // e a escola já escolheu temas (Step2_Themes), busca sugestões reais por
  // assunto em vez de deixar a série sempre vazia (item 2,
  // TAREFAS-JOANA-2026-08-20.md). Entram no histórico de busca (searchHistory)
  // — mesmo tratamento de "achado por busca, ainda não decidido" já usado
  // pela busca manual.
  //
  // Também dispara só a metade de inglês quando a série JÁ tem livros
  // salvos (não é "calibração pendente") mas nenhum deles é inglês —
  // sem isso, uma série que já tinha itens salvos (mesmo que todos
  // errados/pt por um bug de dado antigo) nunca acionava essa busca,
  // porque o gate original só olhava isCalibrationPending. Print do
  // Alex/Joana, 27/08/2026: "Judy Moody Saves the World" (inglês) salvo
  // como português, e "Inglês · 0/2" sem nenhuma sugestão nova pra cobrir
  // a cota.
  useEffect(() => {
    // Temas prontos (data.themes, escola inteira) + temas próprios do
    // Step2_Themes (data.customThemes), que são por escopo: "all" (toda a
    // escola), "seg:<id>" (Fundamental I/II, Médio) ou "ser:<série>" (uma
    // série específica) — ver customThemes em steps-intro.jsx:666.
    // customThemes nunca era lido aqui: um tema próprio cadastrado só pro
    // Fundamental I ou só pro 3º ano não tinha nenhum efeito na
    // recomendação, mesmo nesse fallback onde os temas prontos já
    // funcionavam.
    const customThemes = data.customThemes || {};
    const currentSeriesLabel = activeSeries[series];
    // Step2_Themes (steps-intro.jsx) só conhece as 12 séries canônicas —
    // SEGMENTS e as chaves "seg:"/"ser:" que ele grava usam sempre o nome
    // canônico, nunca o rótulo customizado pela escola. Sem essa conversão,
    // uma série renomeada (ex: "6º Ano - Turma Nova") nunca bateria com o
    // tema próprio cadastrado pra "6º ano".
    const canonicalSeriesLabel = window.inferCanonicalSeriesName ? window.inferCanonicalSeriesName(currentSeriesLabel) : currentSeriesLabel;
    const segmentForSeries = (window.SEGMENTS || []).find((seg) => seg.series.includes(canonicalSeriesLabel));
    const scopedCustomThemes = [
      ...(customThemes.all || []),
      ...(segmentForSeries ? (customThemes["seg:" + segmentForSeries.id] || []) : []),
      ...(canonicalSeriesLabel ? (customThemes["ser:" + canonicalSeriesLabel] || []) : []),
    ];
    const themes = Array.from(new Set([
      ...(Array.isArray(data.themes) ? data.themes : []),
      ...scopedCustomThemes,
    ]));
    const isPending = !!(activeRuntimeEntry && activeRuntimeEntry.isCalibrationPending);
    const hasSomeEnInPool = runtimeBooksForSlide.some((b) => b.lang === "en");
    const needsEnTopUp = !isPending && hasEN && qtyEN > 0 && !hasSomeEnInPool;
    if (!activeRuntimeEntry || (!isPending && !needsEnTopUp)) return undefined;
    if (themes.length === 0) return undefined;
    if (!window.ScolistAPI || typeof window.ScolistAPI.searchBooks !== "function") return undefined;

    let cancelled = false;

    (async () => {
      try {
        // Busca por idioma explicitamente — sem isso, o endpoint volta uma
        // única leva "misturada" que na prática é quase só português, e a
        // cota de inglês da calibragem (qtyEN) nunca é preenchida mesmo
        // quando a escola pediu livros em inglês (bug reportado pela Joana,
        // reunião 26/08/2026: "botei 4 de inglês no calibragem e não veio
        // nenhum"). O backend já suporta o filtro (`language`, ver
        // BookSearchController.js) — só não estava sendo enviado daqui.
        //
        // Faixa de Scolex também explícita por série — sem isso, a busca
        // por tema devolvia o mesmo pool de livros pra qualquer série,
        // ignorando a dificuldade (bug reportado pela Joana, 27/08/2026,
        // vídeo mostrando praticamente o mesmo nível de livro pra séries
        // bem diferentes: "ele não está filtrando pelos colexes"). Faixa
        // vem de SCOLEX_RANGE_BY_SERIES (shared.jsx), tabela que ela mandou.
        // Casa pelo nome canônico da série ativa, não pela posição — a
        // posição na lista de séries da escola muda conforme ela cadastra
        // Educação Infantil, renomeia ou reordena séries (ver
        // resolveSeriesList/scolexRangeForSeriesName em shared.jsx).
        const scolexRange = window.scolexRangeForSeriesName ? window.scolexRangeForSeriesName(activeSeries[series]) : null;
        const scolexOpts = scolexRange
          ? { scolexLevelMin: scolexRange.min, scolexLevelMax: scolexRange.max }
          : {};
        const searches = [];
        if (isPending) searches.push(window.ScolistAPI.searchBooks(null, { tags: themes, language: "pt", ...scolexOpts }));
        if (isPending || needsEnTopUp) searches.push(window.ScolistAPI.searchBooks(null, { tags: themes, language: "en", ...scolexOpts }));
        const rawResults = (await Promise.all(searches)).flat();
        if (cancelled || !rawResults.length) return;
        const normalized = rawResults.map((item, i) => normalizeRuntimeBook(item, i, "topic"));
        normalized.forEach((book) => {
          const existing = CATALOG.find((catalogBook) => catalogBook.id === book.id);
          if (existing) Object.assign(existing, book, { gradeBookId: book.gradeBookId || existing.gradeBookId });
          else CATALOG.push(book);
        });
        const freshIds = normalized.map((b) => b.id).filter((id) => !pinnedForSeries.includes(id));
        touchSearchHistory(setData, series, freshIds);
      } catch (err) {
        console.error("Falha ao buscar sugestões por tema:", err);
      }
    })();

    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [activeRuntimeEntry, series]);

  const searchResults = (q.length >= 2 || subjectFilter)
    ? liveSearchResults.filter(b => !pinnedForSeries.includes(b.id)).slice(0, 8)
    : [];

  // Banner logic, what does the user need to do next for this série?
  const ptDone = pinnedPT >= qtyPT;
  const enDone = pinnedEN >= qtyEN;
  const seriesDone = ptDone && enDone;

  return (
    <div>
      <Heading
        eyebrow="Curadoria Literária · Passo 1, Curadoria por série"
        title={<>Recomendações ranqueadas por nível <Scolex/> + temas</>}
        subtitle={hasEN
          ? `Para cada série: ${qtyPT} livro${qtyPT!==1?"s":""} em Português + ${qtyEN} em Inglês. Adicione os títulos que fizerem sentido, ou busque um livro específico.`
          : `Para cada série: ${qtyPT} livro${qtyPT!==1?"s":""} em Português. Adicione os títulos que fizerem sentido, ou busque um livro específico.`} />

      {/* Series tabs */}
      <div style={{
        display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 20,
        padding: 8, background: "#fff", border: "1px solid var(--border)", borderRadius: 14, boxShadow: "var(--shadow-sm)",
      }}>
        {activeSeries.map((s, i) => {
          const pi = (pinned[i] || []).filter((id) => isRuntimeBookId(id) && runtimeCatalogById[id]);
          const pPT = pi.map((id) => runtimeCatalogById[id]).filter((b) => b && b.lang === "pt").length;
          const pEN = pi.map((id) => runtimeCatalogById[id]).filter((b) => b && b.lang === "en").length;
          const complete = pPT >= qtyPT && pEN >= qtyEN;
          return (
            <button key={s} onClick={() => { setSeries(i); setIdx(0); }} style={{
              padding: "8px 14px", borderRadius: 8,
              background: i === series ? "var(--teal)" : "transparent",
              color: i === series ? "#fff" : "var(--text)",
              fontSize: 13, fontWeight: 600,
              transition: "all 0.15s",
              position: "relative",
            }}>
              {s}
              {pi.length > 0 && <span style={{
                marginLeft: 6, fontSize: 11,
                background: i === series ? "rgba(255,255,255,0.3)" : complete ? "var(--green)" : "var(--teal)",
                color: i === series ? "#fff" : complete ? "var(--text)" : "#fff",
                padding: "1px 6px", borderRadius: 8,
              }}>{complete ? "✓" : `${pi.length}/${qty}`}</span>}
            </button>
          );
        })}
      </div>
      {runtimeError && (
        <div style={{
          marginTop: -8,
          marginBottom: 16,
          padding: "10px 12px",
          background: "#fff1f1",
          border: "1px solid #f3b2b2",
          borderRadius: 10,
          color: "#8a2525",
          fontSize: 12.5,
          fontWeight: 600,
        }}>
          {runtimeError}
        </div>
      )}

      {/* Language quota tracker for current series */}
      <div style={{
        display: "grid", gridTemplateColumns: hasEN ? "1fr 1fr" : "1fr",
        gap: 12, marginBottom: 20,
      }}>
        <div style={{
          background: ptDone ? "rgba(199,222,64,0.18)" : "#fff",
          border: `1px solid ${ptDone ? "var(--green)" : "var(--border)"}`,
          borderRadius: 14, padding: "14px 18px",
          display: "flex", alignItems: "center", gap: 14,
        }}>
          <div style={{
            width: 40, height: 40, borderRadius: "50%",
            background: "var(--teal)", color: "#fff",
            display: "flex", alignItems: "center", justifyContent: "center",
            fontWeight: 800, fontSize: 16, flexShrink: 0,
          }}>PT</div>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 13, color: "var(--text-mute)", fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.6 }}>Português</div>
            <div style={{ fontSize: 18, fontWeight: 800, color: "var(--text)", marginTop: 2 }}>
              {pinnedPT} de {qtyPT} {ptDone && <span style={{ color: "var(--teal-dark)", fontSize: 14 }}>✓ completo</span>}
            </div>
          </div>
          <div style={{ width: 70, height: 6, background: "var(--bg-light)", borderRadius: 3, overflow: "hidden" }}>
            <div style={{ width: `${qtyPT ? Math.min(100,(pinnedPT/qtyPT)*100) : 0}%`, height: "100%", background: ptDone ? "var(--green)" : "var(--teal)", transition: "width 0.3s" }}/>
          </div>
        </div>
        {hasEN && (
          <div style={{
            background: enDone ? "rgba(199,222,64,0.18)" : "#fff",
            border: `1px solid ${enDone ? "var(--green)" : "var(--border)"}`,
            borderRadius: 14, padding: "14px 18px",
            display: "flex", alignItems: "center", gap: 14,
          }}>
            <div style={{
              width: 40, height: 40, borderRadius: "50%",
              background: "#2c4ffc", color: "#fff",
              display: "flex", alignItems: "center", justifyContent: "center",
              fontWeight: 800, fontSize: 16, flexShrink: 0,
            }}>EN</div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13, color: "var(--text-mute)", fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.6 }}>Inglês</div>
              <div style={{ fontSize: 18, fontWeight: 800, color: "var(--text)", marginTop: 2 }}>
                {pinnedEN} de {qtyEN} {enDone && <span style={{ color: "var(--teal-dark)", fontSize: 14 }}>✓ completo</span>}
              </div>
            </div>
            <div style={{ width: 70, height: 6, background: "var(--bg-light)", borderRadius: 3, overflow: "hidden" }}>
              <div style={{ width: `${qtyEN ? Math.min(100,(pinnedEN/qtyEN)*100) : 0}%`, height: "100%", background: enDone ? "var(--green)" : "#2c4ffc", transition: "width 0.3s" }}/>
            </div>
          </div>
        )}
      </div>

      {/* Alert banner when a quota just got filled */}
      {seriesDone && (
        <div style={{
          marginBottom: 20, padding: "14px 18px",
          background: "linear-gradient(90deg, rgba(199,222,64,0.18), rgba(47,151,161,0.1))",
          border: "1px solid var(--green)", borderRadius: 14,
          display: "flex", gap: 12, alignItems: "center",
        }}>
          <div style={{ fontSize: 22 }}>🎯</div>
          <div style={{ flex: 1, fontSize: 14, color: "var(--text)" }}>
            <strong>{activeSeries[series]} completo!</strong> Você pode revisar ou seguir para a próxima série.
          </div>
        </div>
      )}
      {!seriesDone && ptDone && hasEN && !enDone && (
        <div style={{
          marginBottom: 20, padding: "14px 18px",
          background: "rgba(44,79,252,0.08)",
          border: "1px solid #2c4ffc", borderRadius: 14,
          display: "flex", gap: 12, alignItems: "center",
        }}>
          <div style={{ fontSize: 22, color: "var(--teal-dark)", fontWeight: 800 }}>EN</div>
          <div style={{ flex: 1, fontSize: 14, color: "var(--text)" }}>
            <strong>Português completo.</strong> Agora escolha {qtyEN - pinnedEN} livro{qtyEN-pinnedEN!==1?"s":""} em Inglês pra {activeSeries[series]}.
          </div>
        </div>
      )}
      {!seriesDone && enDone && !ptDone && hasEN && (
        <div style={{
          marginBottom: 20, padding: "14px 18px",
          background: "rgba(47,151,161,0.08)",
          border: "1px solid var(--teal)", borderRadius: 14,
          display: "flex", gap: 12, alignItems: "center",
        }}>
          <div style={{ fontSize: 22, color: "var(--teal-dark)", fontWeight: 800 }}>PT</div>
          <div style={{ flex: 1, fontSize: 14, color: "var(--text)" }}>
            <strong>Inglês completo.</strong> Agora escolha {qtyPT - pinnedPT} livro{qtyPT-pinnedPT!==1?"s":""} em Português pra {activeSeries[series]}.
          </div>
        </div>
      )}

      {/* Direct search, "já sei qual livro quero adotar" */}
      <div style={{
        marginBottom: 20, position: "relative",
      }}>
        <div style={{
          background: "#fff", border: `1.5px solid ${searchFocused || q ? "var(--teal)" : "var(--border)"}`,
          borderRadius: 14, padding: "14px 18px",
          display: "flex", alignItems: "center", gap: 12,
          boxShadow: searchFocused ? "0 0 0 4px rgba(47,151,161,0.15)" : "var(--shadow-sm)",
          transition: "all 0.15s",
        }}>
          <div style={{ fontSize: 20, opacity: 0.6 }}>🔍</div>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 11, fontWeight: 700, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.8, marginBottom: 2 }}>
              Já sei qual livro quero adotar
            </div>
            <input
              type="text"
              value={query}
              onChange={e => setQuery(e.target.value)}
              onFocus={() => setSearchFocused(true)}
              onBlur={() => setTimeout(() => setSearchFocused(false), 200)}
              placeholder={`Buscar título ou autor para adicionar em ${activeSeries[series]}…`}
              style={{
                width: "100%", border: "none", outline: "none",
                fontSize: 16, fontWeight: 500, color: "var(--text)",
                background: "transparent", fontFamily: "inherit",
              }}
            />
          </div>
          {query && (
            <button onClick={() => setQuery("")} style={{
              background: "var(--bg-light)", color: "var(--text-mute)",
              width: 28, height: 28, borderRadius: "50%",
              display: "flex", alignItems: "center", justifyContent: "center",
              fontSize: 14, fontWeight: 700,
            }}>×</button>
          )}
        </div>

        {/* Filtro por assunto/tema — busca independente do título/autor */}
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 8, paddingLeft: 4 }}>
          <span style={{ fontSize: 11, fontWeight: 700, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.6 }}>Assunto</span>
          <select value={subjectFilter} onChange={e => setSubjectFilter(e.target.value)} style={{
            padding: "5px 8px", border: "1px solid var(--border)", borderRadius: 6,
            fontSize: 12, fontFamily: "inherit", background: "#fff", color: "var(--text)",
            cursor: "pointer", maxWidth: 260,
          }}>
            <option value="">Todos os assuntos</option>
            {THEMES.map(t => (
              <option key={t.id} value={t.id}>{t.emoji}  {t.t}</option>
            ))}
          </select>
        </div>

        {/* Results dropdown */}
        {(searchFocused || subjectFilter) && (q.length >= 2 || subjectFilter) && (
          <div style={{
            position: "absolute", top: "calc(100% + 6px)", left: 0, right: 0, zIndex: 20,
            background: "#fff", border: "1px solid var(--border)", borderRadius: 14,
            boxShadow: "var(--shadow-lg)", overflow: "hidden",
            maxHeight: 420, overflowY: "auto",
          }}>
            {liveSearchLoading ? (
              <div style={{ padding: "18px 20px", color: "var(--text-mute)", fontSize: 14 }}>
                Buscando…
              </div>
            ) : searchResults.length === 0 ? (
              <div style={{ padding: "18px 20px", color: "var(--text-mute)", fontSize: 14 }}>
                {query.trim()
                  ? <>Nenhum livro encontrado com "{query}".</>
                  : <>Nenhum livro encontrado para esse assunto.</>}
              </div>
            ) : searchResults.map(b => {
              const bookLang = b.lang || "pt";
              const bookSeriesIdx = activeSeries.indexOf(b.series);
              const isDifferentSeries = bookSeriesIdx !== series;
              return (
                <button
                  key={b.id}
                  onMouseDown={e => e.preventDefault()}
                  onClick={() => previewSearchResult(b.id)}
                  style={{
                    width: "100%", textAlign: "left",
                    padding: "12px 16px", display: "flex", gap: 14, alignItems: "center",
                    borderBottom: "1px solid var(--border)",
                    background: "transparent",
                    cursor: "pointer",
                    transition: "background 0.1s",
                  }}
                  onMouseEnter={e => { e.currentTarget.style.background = "var(--bg-light)"; }}
                  onMouseLeave={e => { e.currentTarget.style.background = "transparent"; }}
                >
                  <div style={{ width: 44, flexShrink: 0 }}>
                    <BookCover book={b} size="sm" />
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 14, fontWeight: 700, color: "var(--text)", marginBottom: 2 }}>
                      {b.title}
                    </div>
                    <div style={{ fontSize: 12, color: "var(--text-mute)" }}>
                      {b.author} · {b.editora}
                    </div>
                    <div style={{ display: "flex", gap: 6, marginTop: 6, alignItems: "center", flexWrap: "wrap" }}>
                      <span style={{
                        fontSize: 10, fontWeight: 700, padding: "2px 7px", borderRadius: 4,
                        background: bookLang === "pt" ? "var(--teal)" : "#2c4ffc", color: "#fff",
                      }}>{bookLang === "pt" ? "PT" : "EN"}</span>
                      {isDifferentSeries && (
                        <span style={{ fontSize: 11, color: "var(--amber, #c88a00)", fontWeight: 600 }}>
                          Diferente da série atual
                        </span>
                      )}
                    </div>
                  </div>
                  <div style={{
                    padding: "8px 14px",
                    background: "var(--bg-light)",
                    color: "var(--teal-dark)",
                    border: "1px solid var(--border)",
                    borderRadius: 8, fontSize: 13, fontWeight: 700, flexShrink: 0,
                  }}>
                    Ver detalhes
                  </div>
                </button>
              );
            })}
          </div>
        )}
      </div>

      {/* Book carousel */}
      {isLoadingRuntimeData ? (
        // Carregando de verdade (fetch em voo) — distinto de "vazio", que
        // é o mesmo texto/visual de baixo. Sem essa distinção, um refresh
        // mostrava "Nenhum título foi adicionado ainda" enquanto o fetch
        // ainda não tinha voltado, dando a impressão errada de que um
        // livro recém-adicionado/removido não persistiu.
        <div style={{
          background: "#fff", border: "1px solid var(--border)", borderRadius: 20,
          padding: 48, boxShadow: "var(--shadow-md)",
          display: "flex", flexDirection: "column", alignItems: "center", gap: 12,
        }}>
          <div style={{
            width: 32, height: 32, borderRadius: "50%",
            border: "3px solid var(--bg-soft)", borderTopColor: "var(--teal)",
            animation: "spin 0.8s linear infinite",
          }}/>
          <div style={{ fontSize: 14, fontWeight: 600, color: "var(--text-mute)" }}>
            Carregando recomendações de {activeSeries[series]}…
          </div>
          <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
        </div>
      ) : safeSuggestions.length === 0 ? (
        <div style={{
          background: "#fff", border: "1px solid var(--border)", borderRadius: 20,
          padding: 32, boxShadow: "var(--shadow-md)",
          position: "relative",
        }}>
          <div style={{
            padding: "24px 18px",
            borderRadius: 14,
            background: "var(--bg-light)",
            border: "1px dashed var(--border)",
            textAlign: "center",
          }}>
            <div style={{ fontSize: 18, fontWeight: 800, color: "var(--text)" }}>
              {!runtimeSchoolId ? "Conta ainda sem escola vinculada" : !gradeForCurrentSeries ? "Turma não encontrada para esta série" : "Ainda não há livros nesta série"}
            </div>
            <div style={{ fontSize: 14, color: "var(--text-mute)", marginTop: 8, maxWidth: 440, marginLeft: "auto", marginRight: "auto" }}>
              {!runtimeSchoolId
                ? <>Sua conta ainda não está associada a uma escola, então não há recomendação automática pra mostrar. Use a busca acima pra encontrar e adicionar títulos manualmente enquanto isso é configurado.</>
                : !gradeForCurrentSeries
                ? <>Não encontramos uma turma de {activeSeries[series]} configurada pra esta escola no período atual. Verifique a configuração de turmas no login/console — enquanto isso, use a busca acima pra adicionar títulos manualmente.</>
                : activeRuntimeEntry && activeRuntimeEntry.isCalibrationPending
                ? <>A recomendação automática pra {activeSeries[series]} ainda não está configurada. Use a busca acima pra encontrar e adicionar títulos manualmente.</>
                : <>Nenhum título foi adicionado ainda pra {activeSeries[series]}. Use a busca acima pra encontrar e adicionar títulos.</>}
            </div>
          </div>
        </div>
      ) : (
      <div style={{
        background: "#fff", border: "1px solid var(--border)", borderRadius: 20,
        padding: 32, boxShadow: "var(--shadow-md)",
        position: "relative",
      }}>
        <div style={{
          display: "grid", gridTemplateColumns: "280px 1fr", gap: 32,
          alignItems: "start",
        }}>
        <button onClick={prevBook} style={{
          position: "absolute", left: -22, top: carouselArrowTop, transform: "translateY(-50%)",
          width: 48, height: 48, borderRadius: "50%",
          background: "#fff", border: "1px solid var(--border)",
          boxShadow: "var(--shadow-md)", fontSize: 22, color: "var(--teal-dark)",
          display: "flex", alignItems: "center", justifyContent: "center", zIndex: 2,
        }}>←</button>

        <div style={{display:"flex", justifyContent:"center"}}>
          <BookCover key={`${curWithCover.id}-${curWithCover.imageUrl || "no-cover"}`} book={curWithCover} size="lg" />
        </div>

        <div>
          <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 10 }}>
            <div style={{
              display: "inline-block", padding: "4px 10px",
              background: "var(--bg-light)", borderRadius: 6,
              fontSize: 12, fontWeight: 700, color: "var(--teal-dark)",
              textTransform: "uppercase", letterSpacing: 0.8,
            }}>
              Sugestão {idx + 1} de {safeSuggestions.length} · {activeSeries[series]}
            </div>
            <div style={{
              padding: "4px 10px",
              background: curLang === "pt" ? "var(--teal)" : "#2c4ffc",
              color: "#fff", borderRadius: 6,
              fontSize: 12, fontWeight: 700,
              textTransform: "uppercase", letterSpacing: 0.8,
            }}>
              {cur.langLabel || (curLang === "pt" ? "Português" : "Inglês")}
            </div>
            {searchHistory.includes(cur.id) && !isPinnedCur && (
              <button
                onClick={() => removeFromSearchHistory(setData, series, cur.id)}
                title="Remover do histórico de busca"
                style={{
                  marginLeft: "auto", width: 28, height: 28, borderRadius: "50%",
                  background: "var(--bg-light)", border: "1px solid var(--border)",
                  color: "var(--text-mute)", fontSize: 13, flexShrink: 0,
                  display: "flex", alignItems: "center", justifyContent: "center",
                  cursor: "pointer",
                }}
              >🗑️</button>
            )}
          </div>
          <h2 style={{ fontSize: 30, fontWeight: 800, color: "var(--text)", lineHeight: 1.15, letterSpacing: -0.5 }}>
            {cur.title}
          </h2>
          <div style={{ fontSize: 16, color: "var(--text-mute)", marginTop: 4, fontWeight: 500 }}>
            {cur.author} · {cur.editora}{cur.pages ? ` · ${cur.pages} páginas` : ""}
          </div>

          <div style={{ display: "flex", gap: 10, marginTop: 18, flexWrap: "wrap" }}>
            <div style={{
              padding: "6px 12px", background: "var(--teal)", color: "#fff",
              borderRadius: 20, fontSize: 13, fontWeight: 700,
            }}><Scolex/> {cur.level}</div>
            <div style={{
              padding: "6px 12px", background: cur.stock === "alto" ? "rgba(199,222,64,0.3)" : cur.stock === "médio" ? "rgba(245,185,66,0.25)" : "rgba(224,121,142,0.2)",
              borderRadius: 20, fontSize: 13, fontWeight: 600, color: "var(--text)",
            }}>Estoque {cur.stockLabel || cur.stock}</div>
            <div style={{
              padding: "6px 12px", background: "#fff", border: "1px solid var(--border)",
              borderRadius: 20, fontSize: 13, fontWeight: 600, color: "var(--text)",
            }}>R$ {cur.price.toFixed(2)}</div>
          </div>

          {/* Lex + balão com a sinopse do livro — antes eram dois blocos
              separados (parágrafo de sinopse + "Lex sugere" com um texto de
              justificativa genérico pra livros reais). Simplificado pra um
              só, a pedido da Joana (item 13, TAREFAS-JOANA-2026-08-21.md).
              Truncado em ~5 linhas com "ver mais/ver menos" (reunião
              26/08/2026) pra sinopse grande não empurrar o resto da tela
              pra baixo. */}
          {(() => {
            const hasSummary = !!cur.summary;
            const summaryText = hasSummary ? cur.summary : "Sinopse não disponível para este título.";
            return (
            <div style={{
              marginTop: 18, padding: "12px 16px",
              background: "var(--bg-light)", borderRadius: 12,
              borderLeft: "3px solid var(--teal)",
              display: "flex", gap: 10,
            }}>
              <LexAvatar size={28} />
              <div>
                <div style={{
                  fontSize: 13.5, color: hasSummary ? "var(--text)" : "var(--text-mute)", lineHeight: 1.5,
                  ...(!hasSummary || summaryExpanded ? {} : {
                    display: "-webkit-box",
                    WebkitLineClamp: 5,
                    WebkitBoxOrient: "vertical",
                    overflow: "hidden",
                  }),
                }}>
                  {summaryText}
                </div>
                {hasSummary && summaryText.length > 260 && (
                  <button onClick={() => setSummaryExpanded(v => !v)} style={{
                    marginTop: 6, padding: 0, background: "none", border: "none",
                    fontSize: 12, fontWeight: 700, color: "var(--teal-dark)",
                    cursor: "pointer", fontFamily: "inherit",
                  }}>
                    {summaryExpanded ? "ver menos" : "ver mais"}
                  </button>
                )}
              </div>
            </div>
            );
          })()}

          {/* Card "Scolex da turma": nível atual (sem projeção — reunião
              03/08/2026, item 3, a projeção fica de fora até a Joana definir
              a metodologia de cálculo). Mostra o dado real quando a turma já
              tiver avaliação Scolex; enquanto isso (hoje, nenhuma escola
              concluiu a avaliação — piloto em vendas), mostra o estado
              "ainda não calculado" em vez de esconder o card. */}
          <div style={{
            marginTop: 18, padding: "12px 16px",
            background: "var(--bg-light)", borderRadius: 12,
            border: "1px solid var(--border)",
            display: "flex", gap: 10, alignItems: "center",
          }}>
            <Scolex/>
            {gradeForCurrentSeries && gradeForCurrentSeries.scolexLevel != null ? (
              <div style={{ fontSize: 13.5, color: "var(--text)" }}>
                <strong style={{color:"var(--teal-dark)"}}>Scolex da turma:</strong> nível {gradeForCurrentSeries.scolexLevel}
              </div>
            ) : (
              <div style={{ fontSize: 13.5, color: "var(--text-mute)" }}>
                Scolex turma ainda não calculado
              </div>
            )}
          </div>

          {/* Temas trabalhados */}
          {cur.themes && cur.themes.length > 0 && (
            <div style={{ marginTop: 14, display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
              <span style={{ fontSize: 11, color: "var(--text-mute)", fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.5, marginRight: 4 }}>Temas:</span>
              {cur.themes.slice(0, 5).map(t => (
                <div key={t} style={{ padding: "4px 10px", background: "#fff", border: "1px solid var(--border)", borderRadius: 14, fontSize: 12, fontWeight: 500 }}>{t}</div>
              ))}
            </div>
          )}

          {/* Bimestre/trimestre - quando o professor usa o livro */}
          {(() => {
            const { count: periodCount, label: periodLabel } = periodInfo(data);
            const curBim = ((data.bimestres || {})[series] || {})[cur.id] || null;
            const setBim = (b) => {
              setData(d => {
                const all = { ...(d.bimestres || {}) };
                const forSeries = { ...(all[series] || {}) };
                if (b == null) delete forSeries[cur.id];
                else forSeries[cur.id] = b;
                all[series] = forSeries;
                return { ...d, bimestres: all };
              });
            };
            return (
              <div style={{ marginTop: 18, padding: "14px 16px", background: "#fff", border: "1px solid var(--border)", borderRadius: 12 }}>
                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 10, flexWrap: "wrap", gap: 8 }}>
                  <div>
                    <div style={{ fontSize: 11, color: "var(--text-mute)", fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6 }}>
                      Quando vai usar
                    </div>
                    <div style={{ fontSize: 13, color: "var(--text)", marginTop: 3 }}>
                      {`Marque o ${periodLabel} em que esse livro será trabalhado em sala`}
                    </div>
                  </div>
                  {curBim != null && (
                    <button onClick={() => setBim(null)} style={{
                      padding: "4px 10px", fontSize: 11, fontWeight: 600,
                      background: "transparent", border: "1px solid var(--border)",
                      borderRadius: 6, color: "var(--text-mute)", cursor: "pointer",
                    }}>limpar</button>
                  )}
                </div>
                <div style={{ display: "grid", gridTemplateColumns: `repeat(${periodCount}, 1fr)`, gap: 8 }}>
                  {Array.from({ length: periodCount }, (_, i) => i + 1).map(b => {
                    const active = curBim === b;
                    return (
                      <button key={b} onClick={() => setBim(active ? null : b)} style={{
                        padding: "10px 8px",
                        background: active ? "var(--teal)" : "var(--bg-light)",
                        border: `1px solid ${active ? "var(--teal)" : "var(--border)"}`,
                        borderRadius: 10,
                        cursor: "pointer", fontFamily: "inherit",
                        display: "flex", flexDirection: "column", alignItems: "center", gap: 2,
                        transition: "all 0.15s",
                      }}>
                        <span style={{ fontSize: 16, fontWeight: 800, color: active ? "#fff" : "var(--text)", lineHeight: 1 }}>{b}º</span>
                        <span style={{ fontSize: 10.5, fontWeight: 600, color: active ? "rgba(255,255,255,0.9)" : "var(--text-mute)" }}>{periodLabel}</span>
                      </button>
                    );
                  })}
                </div>
                {!isPinnedCur && (
                  <div style={{ fontSize: 11, color: "var(--text-mute)", marginTop: 8, fontStyle: "italic" }}>
                    {`Adicione o livro à lista pra que o ${periodLabel} apareça pra família.`}
                  </div>
                )}
              </div>
            );
          })()}
        </div>
        </div>

        {/* ===== Action footer, full-width below grid ===== */}
        <div style={{ marginTop: 24, paddingTop: 20, borderTop: "1px solid var(--border)", display: "flex", gap: 10, flexWrap: "wrap", alignItems: "center" }}>
          {/* Ver amostra: revertido de "esconde quando não existe" (item 6,
              reunião 03/08/2026) pra "mostra desabilitado com aviso" (reunião
              26/08/2026) — a Joana notou o botão "sumido" e preferiu deixar
              claro que a amostra não existe pro livro em vez de ele desaparecer
              sem explicação. */}
          {cur.sampleUrl ? (
            <button onClick={() => setPreview({ kind: "sample", book: cur })} style={{
              padding: "10px 16px", background: "#fff",
              border: "1px solid var(--border)", borderRadius: 10,
              fontSize: 13, fontWeight: 600, color: "var(--text)",
              cursor: "pointer", display: "flex", alignItems: "center", gap: 6,
            }}
            onMouseEnter={e => { e.currentTarget.style.borderColor = "var(--teal)"; e.currentTarget.style.color = "var(--teal-dark)"; }}
            onMouseLeave={e => { e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.color = "var(--text)"; }}>
              👁 Ver amostra do livro
            </button>
          ) : (
            <button disabled title="Este livro ainda não tem amostra cadastrada" style={{
              padding: "10px 16px", background: "var(--bg-light)",
              border: "1px solid var(--border)", borderRadius: 10,
              fontSize: 13, fontWeight: 600, color: "var(--text-mute)",
              cursor: "not-allowed", display: "flex", alignItems: "center", gap: 6,
            }}>
              👁 Amostra não disponível
            </button>
          )}
          {/* Botão "Guia docente" removido por enquanto (pedido do Alex,
              26/08/2026) — não é dado real de nenhum livro, é texto fixo
              genérico igual pra todos. Confirmado direto na base (Supabase
              prod-scolados, raw_metadata/ONIX): nenhum dos ~6.297 livros com
              metadado tem guia docente de verdade (código ONIX 20), só 37
              têm "instructional material" (código 39), que na prática são
              instruções de uso do produto físico (livro-quebra-cabeça,
              cartonado), não guia pedagógico. Ver TAREFAS-JOANA-2026-08-26.md.
              O modal (kind "guide", mais abaixo) continua no código, só sem
              botão que abra ele — reativa fácil se aparecer uma fonte real.
          <button onClick={() => setPreview({ kind: "guide", book: cur })} style={{
            padding: "10px 16px", background: "#fff",
            border: "1px solid var(--border)", borderRadius: 10,
            fontSize: 13, fontWeight: 600, color: "var(--text)",
            cursor: "pointer", display: "flex", alignItems: "center", gap: 6,
          }}
          onMouseEnter={e => { e.currentTarget.style.borderColor = "var(--teal)"; e.currentTarget.style.color = "var(--teal-dark)"; }}
          onMouseLeave={e => { e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.color = "var(--text)"; }}>
            👩‍🏫 Guia docente
          </button>
          */}

          <div style={{ flex: 1 }}/>

          {isAtQuotaForCur && !isPinnedCur ? (
            <div style={{
              padding: "12px 16px",
              background: "rgba(245,185,66,0.15)",
              border: "1px solid #f5b942",
              borderRadius: 12,
              fontSize: 13.5, color: "var(--text)",
              display: "flex", alignItems: "center", gap: 10,
            }}>
              <span style={{ fontSize: 18 }}>ℹ️</span>
              <div>
                Você já atingiu a cota de <strong>{curLang === "pt" ? "Português" : "Inglês"}</strong> para {activeSeries[series]}.
                {curLang === "pt" && !enDone && hasEN && <> Falta{qtyEN-pinnedEN>1?"m":""} {qtyEN-pinnedEN} em Inglês.</>}
                {curLang === "en" && !ptDone && <> Falta{qtyPT-pinnedPT>1?"m":""} {qtyPT-pinnedPT} em Português.</>}
              </div>
            </div>
          ) : isPinnedCur ? (
            <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
              <div style={{
                padding: "12px 16px",
                background: "rgba(199,222,64,0.25)",
                border: "1px solid var(--green)",
                borderRadius: 12,
                fontSize: 14, fontWeight: 700, color: "var(--teal-dark)",
                display: "flex", alignItems: "center", gap: 8,
              }}>
                <span>✓</span> Na lista
              </div>
              <button onClick={pin} disabled={cur && removingIds.has(cur.id)} style={{
                padding: "12px 16px",
                background: "#fff",
                border: "1px solid var(--border)",
                borderRadius: 12,
                fontSize: 13.5, fontWeight: 600, color: "#b53a3a",
                cursor: (cur && removingIds.has(cur.id)) ? "wait" : "pointer",
                opacity: (cur && removingIds.has(cur.id)) ? 0.6 : 1,
                display: "flex", alignItems: "center", gap: 6,
              }}
              onMouseEnter={e => { e.currentTarget.style.background = "#fdecec"; e.currentTarget.style.borderColor = "#e0798e"; }}
              onMouseLeave={e => { e.currentTarget.style.background = "#fff"; e.currentTarget.style.borderColor = "var(--border)"; }}>
                {cur && removingIds.has(cur.id) ? "Removendo…" : "Remover da lista"}
              </button>
            </div>
          ) : (
            <BtnTeal onClick={pin}>
              ⊕ Adicionar à lista
            </BtnTeal>
          )}
          <BtnGhost onClick={nextBook}>Ver outra opção →</BtnGhost>
        </div>

        <button onClick={nextBook} style={{
          position: "absolute", right: -22, top: carouselArrowTop, transform: "translateY(-50%)",
          width: 48, height: 48, borderRadius: "50%",
          background: "#fff", border: "1px solid var(--border)",
          boxShadow: "var(--shadow-md)", fontSize: 22, color: "var(--teal-dark)",
          display: "flex", alignItems: "center", justifyContent: "center", zIndex: 2,
        }}>→</button>
      </div>
      )}

      {/* Ver mais ideias, keeps pinned books in carousel; reveals +10 */}
      {safeSuggestions.length > 0 && hasMore && (
        <div style={{ display: "flex", justifyContent: "center", marginTop: 16 }}>
          <button onClick={() => setShownCount(c => c + 10)} style={{
            padding: "10px 18px", borderRadius: 10,
            background: "#fff", border: "1px dashed var(--teal)",
            color: "var(--teal-dark)", fontSize: 13.5, fontWeight: 600,
            cursor: "pointer", display: "flex", alignItems: "center", gap: 8,
          }}
          onMouseEnter={e => { e.currentTarget.style.background = "var(--bg-light)"; }}
          onMouseLeave={e => { e.currentTarget.style.background = "#fff"; }}>
            <span style={{ fontSize: 15 }}>✨</span>
            Ver mais ideias <span style={{ color: "var(--text-mute)", fontWeight: 500 }}>(+{Math.min(10, suggestions.length - shownCount)})</span>
          </button>
        </div>
      )}

      {/* Pinned strip for current series, split by language. Enquanto
          isLoadingRuntimeData, nem mostra — "0/N" com a grade vazia aqui
          é o mesmo visual enganoso do bloco de sugestões acima. */}
      {isLoadingRuntimeData ? (
        <div style={{ marginTop: 24, fontSize: 13, color: "var(--text-mute)", fontStyle: "italic" }}>
          Carregando livros já adicionados…
        </div>
      ) : (
      <div style={{ marginTop: 24, display: "grid", gridTemplateColumns: hasEN ? "1fr 1fr" : "1fr", gap: 20 }}>
        <div>
          <div style={{ fontSize: 13, fontWeight: 700, color: "var(--teal-dark)", textTransform: "uppercase", letterSpacing: 1, marginBottom: 12, display: "flex", alignItems: "center", gap: 8 }}>
            <span style={{ display: "inline-block", width: 18, height: 18, borderRadius: "50%", background: "var(--teal)", color: "#fff", fontSize: 10, textAlign: "center", lineHeight: "18px", fontWeight: 800 }}>PT</span>
            Português · {pinnedPT}/{qtyPT}
          </div>
          <div style={{ display: "flex", gap: 14, flexWrap: "wrap" }}>
            {pinnedBooks.filter(b => b.lang === "pt").map(b => (
              <div key={b.id} style={{ width: 110, textAlign:"center", position: "relative" }}
                   className="pinned-item">
                <BookCover book={b} size="sm" />
                <button onClick={() => removeBook(b.id)}
                  disabled={removingIds.has(b.id)}
                  title={removingIds.has(b.id) ? "Removendo…" : "Remover da lista"}
                  style={{
                    position: "absolute", top: -8, right: -8,
                    width: 26, height: 26, borderRadius: "50%",
                    background: "#fff", border: "1px solid var(--border)",
                    color: "#b53a3a", fontSize: 16, fontWeight: 600,
                    cursor: removingIds.has(b.id) ? "wait" : "pointer",
                    opacity: removingIds.has(b.id) ? 0.5 : 1,
                    boxShadow: "var(--shadow-sm)",
                    display: "flex", alignItems: "center", justifyContent: "center",
                    lineHeight: 1,
                  }}
                  onMouseEnter={e => { e.currentTarget.style.background = "#fdecec"; e.currentTarget.style.borderColor = "#e0798e"; }}
                  onMouseLeave={e => { e.currentTarget.style.background = "#fff"; e.currentTarget.style.borderColor = "var(--border)"; }}>
                  {removingIds.has(b.id) ? "…" : "×"}
                </button>
                <div style={{ fontSize: 12, fontWeight: 600, marginTop: 8, lineHeight: 1.25 }}>{b.title}</div>
              </div>
            ))}
            {Array.from({length: Math.max(0, qtyPT - pinnedPT)}).map((_, i) => (
              <div key={"pt"+i} style={{
                width: 110, height: 160, borderRadius: 6,
                border: "2px dashed var(--border)", background: "var(--bg-soft)",
                display: "flex", alignItems: "center", justifyContent: "center",
                color: "var(--text-mute)", fontSize: 24, fontWeight: 300,
              }}>+</div>
            ))}
          </div>
        </div>
        {hasEN && qtyEN > 0 && (
          <div>
            <div style={{ fontSize: 13, fontWeight: 700, color: "var(--teal-dark)", textTransform: "uppercase", letterSpacing: 1, marginBottom: 12, display: "flex", alignItems: "center", gap: 8 }}>
              <span style={{ display: "inline-block", width: 18, height: 18, borderRadius: "50%", background: "#2c4ffc", color: "#fff", fontSize: 10, textAlign: "center", lineHeight: "18px", fontWeight: 800 }}>EN</span>
              Inglês · {pinnedEN}/{qtyEN}
            </div>
            <div style={{ display: "flex", gap: 14, flexWrap: "wrap" }}>
              {pinnedBooks.filter(b => b.lang === "en").map(b => (
                <div key={b.id} style={{ width: 110, textAlign:"center", position: "relative" }}>
                  <BookCover book={b} size="sm" />
                  <button onClick={() => removeBook(b.id)}
                    disabled={removingIds.has(b.id)}
                    title={removingIds.has(b.id) ? "Removendo…" : "Remover da lista"}
                    style={{
                      position: "absolute", top: -8, right: -8,
                      width: 26, height: 26, borderRadius: "50%",
                      background: "#fff", border: "1px solid var(--border)",
                      color: "#b53a3a", fontSize: 16, fontWeight: 600,
                      cursor: removingIds.has(b.id) ? "wait" : "pointer",
                      opacity: removingIds.has(b.id) ? 0.5 : 1,
                      boxShadow: "var(--shadow-sm)",
                      display: "flex", alignItems: "center", justifyContent: "center",
                      lineHeight: 1,
                    }}
                    onMouseEnter={e => { e.currentTarget.style.background = "#fdecec"; e.currentTarget.style.borderColor = "#e0798e"; }}
                    onMouseLeave={e => { e.currentTarget.style.background = "#fff"; e.currentTarget.style.borderColor = "var(--border)"; }}>
                    {removingIds.has(b.id) ? "…" : "×"}
                  </button>
                  <div style={{ fontSize: 12, fontWeight: 600, marginTop: 8, lineHeight: 1.25 }}>{b.title}</div>
                </div>
              ))}
              {Array.from({length: Math.max(0, qtyEN - pinnedEN)}).map((_, i) => (
                <div key={"en"+i} style={{
                  width: 110, height: 160, borderRadius: 6,
                  border: "2px dashed var(--border)", background: "var(--bg-soft)",
                  display: "flex", alignItems: "center", justifyContent: "center",
                  color: "var(--text-mute)", fontSize: 24, fontWeight: 300,
                }}>+</div>
              ))}
            </div>
          </div>
        )}
      </div>
      )}

      {/* Preview modal, sample or teacher guide, non-downloadable */}
      {preview && (
        <div onClick={() => setPreview(null)} style={{
          position: "fixed", inset: 0, background: "rgba(15,30,50,0.55)",
          backdropFilter: "blur(4px)", zIndex: 1000,
          display: "flex", alignItems: "center", justifyContent: "center", padding: 40,
        }}>
          <div onClick={e => e.stopPropagation()} style={{
            background: "#fff", borderRadius: 16, maxWidth: 780, width: "100%",
            maxHeight: "90vh", overflow: "hidden", boxShadow: "0 24px 60px rgba(0,0,0,0.3)",
            display: "flex", flexDirection: "column",
          }}>
            {/* Header */}
            <div style={{
              padding: "16px 22px", borderBottom: "1px solid var(--border)",
              display: "flex", alignItems: "center", gap: 14,
            }}>
              <div style={{
                width: 36, height: 36, borderRadius: 10,
                background: preview.kind === "sample" ? "rgba(44,79,252,0.12)" : "rgba(199,222,64,0.3)",
                display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18,
              }}>{preview.kind === "sample" ? "👁" : "👩‍🏫"}</div>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 11, color: "var(--text-mute)", fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6 }}>
                  {preview.kind === "sample" ? "Amostra do livro" : "Guia docente"}
                </div>
                <div style={{ fontSize: 16, fontWeight: 700, color: "var(--text)" }}>{preview.book.title}</div>
              </div>
              <button onClick={() => setPreview(null)} style={{
                width: 32, height: 32, borderRadius: "50%",
                background: "var(--bg-light)", border: "none",
                fontSize: 18, color: "var(--text-mute)", cursor: "pointer",
              }}>✕</button>
            </div>
            {/* Watermark notice */}
            <div style={{
              padding: "8px 22px", background: "rgba(245,185,66,0.1)",
              borderBottom: "1px solid rgba(245,185,66,0.3)",
              fontSize: 12, color: "#8a6414", display: "flex", alignItems: "center", gap: 8,
            }}>
              <span>🔒</span>
              <span>Pré-visualização apenas, conteúdo protegido por direitos autorais, não pode ser baixado ou impresso.</span>
            </div>
            {/* Preview body */}
            <div style={{
              flex: 1, overflow: "auto", padding: preview.kind === "sample" ? 0 : "24px",
              background: "#f4f5f8",
              userSelect: preview.kind === "sample" ? "auto" : "none",
              WebkitUserSelect: preview.kind === "sample" ? "auto" : "none",
            }}
            onContextMenu={e => preview.kind !== "sample" && e.preventDefault()}>
              {preview.kind === "sample" ? (
                preview.book.sampleUrl ? (
                  // #toolbar=0 esconde a barra nativa do visualizador de PDF do
                  // Chrome (zoom, girar, e principalmente baixar/imprimir) —
                  // sem isso, o Chrome desenha a própria barra dentro do
                  // iframe por cima do aviso de "não pode ser baixado ou
                  // impresso" logo acima, com um botão de download bem ali.
                  // É só a UI nativa escondida (não é DRM de verdade: o
                  // arquivo em si continua acessível por quem inspecionar a
                  // aba de rede), mas resolve o pedido do Alex de não exibir
                  // esse painel do Chrome.
                  <iframe
                    src={`${preview.book.sampleUrl}${preview.book.sampleUrl.includes("#") ? "&" : "#"}toolbar=0`}
                    title={`Amostra — ${preview.book.title}`}
                    style={{ width: "100%", height: "70vh", border: "none", display: "block" }}
                  />
                ) : (
                  <div style={{ padding: 32, textAlign: "center", color: "var(--text-mute)", fontSize: 14 }}>
                    Amostra não disponível para este livro.
                  </div>
                )
              ) : (
                <div style={{
                  maxWidth: 640, margin: "0 auto", background: "#fff",
                  borderRadius: 6, padding: "36px 44px",
                  boxShadow: "0 4px 12px rgba(0,0,0,0.1)", position: "relative", overflow: "hidden",
                }}>
                  <div style={{
                    position: "absolute", inset: 0,
                    display: "flex", alignItems: "center", justifyContent: "center",
                    fontSize: 60, fontWeight: 900, color: "rgba(0,0,0,0.04)",
                    transform: "rotate(-30deg)", pointerEvents: "none", letterSpacing: 6,
                  }}>SCOLIST</div>
                  <div style={{ fontSize: 11, color: "var(--teal-dark)", fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.8 }}>Guia docente</div>
                  <h2 style={{ fontSize: 24, fontWeight: 800, marginTop: 6, marginBottom: 18, color: "var(--text)" }}>{preview.book.title}</h2>
                  <h3 style={{ fontSize: 14, fontWeight: 700, color: "var(--teal-dark)", textTransform: "uppercase", letterSpacing: 0.6, marginTop: 18, marginBottom: 8 }}>Objetivos pedagógicos</h3>
                  <ul style={{ fontSize: 14, lineHeight: 1.7, color: "var(--text)", paddingLeft: 20 }}>
                    <li>Desenvolver a compreensão leitora e a capacidade de inferência.</li>
                    <li>Trabalhar vocabulário específico da faixa etária.</li>
                    <li>Estimular discussões sobre os temas centrais da obra.</li>
                  </ul>
                  <h3 style={{ fontSize: 14, fontWeight: 700, color: "var(--teal-dark)", textTransform: "uppercase", letterSpacing: 0.6, marginTop: 18, marginBottom: 8 }}>Perguntas para debate</h3>
                  <ul style={{ fontSize: 14, lineHeight: 1.7, color: "var(--text)", paddingLeft: 20 }}>
                    <li>O que mais chamou sua atenção no comportamento dos personagens?</li>
                    <li>Que situação do livro se parece com algo que você já viveu?</li>
                    <li>Como você se sentiria no lugar do protagonista?</li>
                  </ul>
                  <h3 style={{ fontSize: 14, fontWeight: 700, color: "var(--teal-dark)", textTransform: "uppercase", letterSpacing: 0.6, marginTop: 18, marginBottom: 8 }}>Atividades sugeridas</h3>
                  <ul style={{ fontSize: 14, lineHeight: 1.7, color: "var(--text)", paddingLeft: 20 }}>
                    <li>Escrita criativa: reescrever um capítulo da perspectiva de outro personagem.</li>
                    <li>Arte: ilustrar a cena favorita, justificando a escolha.</li>
                    <li>Roda de conversa sobre os temas trabalhados.</li>
                  </ul>
                </div>
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// Step 11: Revisão consolidada, todas as séries com livros escolhidos
function Step10_Detail() {
  const { data, setData } = useApp();
  // Séries de verdade da escola (Passo 1), não a lista fixa de 12.
  const activeSeries = resolveSeriesList(data);
  const pinned = data.pinned || {};
  const isRuntimeBookId = (id) => String(id || "").startsWith("rt-");
  const runtimeCatalog = CATALOG.filter((book) => isRuntimeBookId(book && book.id));
  const runtimeCatalogById = Object.fromEntries(runtimeCatalog.map((book) => [book.id, book]));
  // dedup defensivo — mesmo motivo do Step9_Recommend (pinned[série] pode
  // ter id repetido em dado salvo antes do fix de duplicata em
  // pinBookToGrade, api.js)
  const pinnedIdsForSeries = (si) => Array.from(new Set(pinned[si] || [])).filter((id) => isRuntimeBookId(id) && runtimeCatalogById[id]);
  const hasEN = data.scolexEN !== "none";
  const qtyPT = data.qtyPerYearPT ?? (data.qtyPerYear ?? 3);
  const qtyEN = hasEN ? (data.qtyPerYearEN ?? 2) : 0;
  const bimestres = data.bimestres || {};
  const { count: periodCount, label: periodLabel, labelPlural: periodLabelPlural } = periodInfo(data);
  const [dragOver, setDragOver] = useState(null); // {si, bn}
  const [dragging, setDragging] = useState(null); // {si, bookId}
  const [removingIds, setRemovingIds] = useState(() => new Set());

  const setBim = (si, bookId, bn) => {
    setData(d => {
      const all = { ...(d.bimestres || {}) };
      const forSeries = { ...(all[si] || {}) };
      if (bn === 0 || bn == null) delete forSeries[bookId];
      else forSeries[bookId] = bn;
      all[si] = forSeries;
      return { ...d, bimestres: all };
    });
  };

  // Stats globais
  const allPinnedIds = activeSeries.flatMap((_, si) => pinnedIdsForSeries(si));
  const totalBooks = allPinnedIds.length;
  const expectedTotal = activeSeries.length * (qtyPT + qtyEN);
  const seriesDoneCount = activeSeries.filter((_, si) => {
    const ids = pinnedIdsForSeries(si);
    const pt = ids.filter((id) => { const b = runtimeCatalogById[id]; return b && (b.lang || "pt") === "pt"; }).length;
    const en = ids.filter((id) => { const b = runtimeCatalogById[id]; return b && b.lang === "en"; }).length;
    return pt >= qtyPT && (!hasEN || en >= qtyEN);
  }).length;

  const runtimeSelection = data.runtimeSelection || {};
  const runtimeGrades = Array.isArray(runtimeSelection.availableGrades) ? runtimeSelection.availableGrades : [];
  const runtimePeriod = runtimeSelection.period || String(new Date().getFullYear());
  const runtimeSchoolId = runtimeSelection.schoolId || "";

  // Pessimista: só tira da tela (e do localStorage) depois que o backend
  // confirmar a remoção — antes era fogo-e-esquece, então um refresh
  // rápido depois de clicar ✕ podia "trazer o livro de volta", porque o
  // efeito de seed (Step9_Recommend) resemeia a partir do que ainda
  // estivesse salvo no Scolist se o PUT não tivesse terminado a tempo
  // (bug reportado pelo Alex, 27/08/2026).
  const removeBook = async (seriesIdx, bookId) => {
    const book = runtimeCatalogById[bookId];
    const grade = findGradeForSeriesIndex(runtimeGrades, seriesIdx, activeSeries);
    // Mesmo motivo do fix em syncUnpinFromList (Step9_Recommend): remove
    // todo item cru com o mesmo ISBN, não só o gradeBookId que o front
    // guardou — lista antiga pode ter mais de um item pro mesmo livro.
    const rawItems = (grade && data.runtimeSelection && data.runtimeSelection.recommendedByGradeId
      && data.runtimeSelection.recommendedByGradeId[grade.id]
      && data.runtimeSelection.recommendedByGradeId[grade.id].items) || [];
    const byIsbn = book && book.isbn ? rawItems.filter((raw) => raw && raw.isbn === book.isbn) : [];
    const idsToRemove = byIsbn.length > 0
      ? Array.from(new Set(byIsbn.map((raw) => raw.id).filter(Boolean)))
      : [book && (book.gradeBookId || book.bookId)].filter(Boolean);

    let ok = true;
    if (window.ScolistAPI && typeof window.ScolistAPI.unpinBookFromGrade === "function" && idsToRemove.length > 0 && runtimeSchoolId && grade && grade.id) {
      setRemovingIds(prev => new Set(prev).add(bookId));
      try {
        for (const itemId of idsToRemove) {
          // eslint-disable-next-line no-await-in-loop
          await window.ScolistAPI.unpinBookFromGrade(runtimeSchoolId, runtimePeriod, grade.id, itemId);
        }
        if (book) book.gradeBookId = null;
        purgeFromRuntimeItemsCache(setData, grade.id, idsToRemove);
      } catch (error) {
        // Step10_Detail não tem o banner de runtimeError que o
        // Step9_Recommend tem — só loga, mesmo comportamento de antes.
        console.error("Falha ao remover livro da lista:", error);
        ok = false;
      } finally {
        setRemovingIds(prev => { const next = new Set(prev); next.delete(bookId); return next; });
      }
    }
    if (!ok) return;

    setData(d => {
      const prev = ((d.pinned || {})[seriesIdx] || []).filter((id) => isRuntimeBookId(id));
      const list = prev.filter(id => id !== bookId);
      return { ...d, pinned: { ...(d.pinned || {}), [seriesIdx]: list } };
    });
  };

  return (
    <div>
      <Heading
        eyebrow="Curadoria Literária · Revisão consolidada"
        title="Revisão Livros Literatura"
        subtitle={<>Confira tudo antes de seguir. Você pode remover ou trocar qualquer título voltando pro carrossel da série.</>} />

      {/* Stats bar */}
      <div style={{ marginTop: 20, display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 14, marginBottom: 24 }}>
        <div style={{ padding: "16px 20px", background: "#fff", border: "1px solid var(--border)", borderRadius: 14, boxShadow: "var(--shadow-sm)" }}>
          <div style={{ fontSize: 11, color: "var(--text-mute)", fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6 }}>Livros na lista</div>
          <div style={{ fontSize: 26, fontWeight: 800, color: "var(--text)", marginTop: 4 }}>{totalBooks}<span style={{ fontSize: 14, color: "var(--text-mute)", fontWeight: 500 }}> de {expectedTotal} esperados</span></div>
          <div style={{ fontSize: 11, color: "var(--text-mute)", marginTop: 4 }}>
            {activeSeries.length} séries × ({qtyPT} PT{hasEN ? ` + ${qtyEN} EN` : ""})
          </div>
        </div>
        <div style={{ padding: "16px 20px", background: "#fff", border: "1px solid var(--border)", borderRadius: 14, boxShadow: "var(--shadow-sm)" }}>
          <div style={{ fontSize: 11, color: "var(--text-mute)", fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6 }}>Séries completas</div>
          <div style={{ fontSize: 26, fontWeight: 800, color: seriesDoneCount === activeSeries.length ? "var(--teal-dark)" : "var(--text)", marginTop: 4 }}>{seriesDoneCount}<span style={{ fontSize: 14, color: "var(--text-mute)", fontWeight: 500 }}> de {activeSeries.length}</span></div>
          <div style={{ fontSize: 11, color: "var(--text-mute)", marginTop: 4 }}>
            cota mínima atingida
          </div>
        </div>
        {(() => {
          // Só conta marcações dentro da faixa do calendário atual — uma tag
          // de "4º bimestre" salva antes de trocar pra trimestre não é mais
          // válida (não aparece em nenhuma lane) e não deve contar aqui.
          const totalBim = Object.values(bimestres).reduce((s, m) => s + Object.values(m || {}).filter(v => v >= 1 && v <= periodCount).length, 0);
          const bimColor = totalBim === totalBooks && totalBooks > 0 ? "var(--teal-dark)" : "var(--text)";
          return (
            <div style={{ padding: "16px 20px", background: "#fff", border: "1px solid var(--border)", borderRadius: 14, boxShadow: "var(--shadow-sm)" }}>
              <div style={{ fontSize: 11, color: "var(--text-mute)", fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.6 }}>{`${periodLabelPlural} marcados`}</div>
              <div style={{ fontSize: 26, fontWeight: 800, color: bimColor, marginTop: 4 }}>
                {totalBim}<span style={{ fontSize: 14, color: "var(--text-mute)", fontWeight: 500 }}> de {totalBooks}</span>
              </div>
              <div style={{ fontSize: 11, color: "var(--text-mute)", marginTop: 4 }}>
                {`arraste pro ${periodLabel} certo`}
              </div>
            </div>
          );
        })()}
      </div>

      <LexBubble pose="read">
        Aqui está a lista toda. <strong>{`Arraste cada livro pro ${periodLabel}`}</strong> em que vai usar — ou clique pra ciclar. Use o <strong>✕</strong> pra remover.
      </LexBubble>

      {/* Grid de séries */}
      <div style={{ marginTop: 24, display: "flex", flexDirection: "column", gap: 18 }}>
        {activeSeries.map((sName, si) => {
          const ids = pinnedIdsForSeries(si);
          const books = ids.map((id) => runtimeCatalogById[id]).filter(Boolean);
          const pt = books.filter(b => (b.lang||"pt") === "pt").length;
          const en = books.filter(b => b.lang === "en").length;
          const complete = pt >= qtyPT && (!hasEN || en >= qtyEN);
          return (
            <div key={si} style={{
              background: "#fff", border: `1px solid ${complete ? "var(--teal)" : "var(--border)"}`,
              borderRadius: 16, padding: "18px 22px", boxShadow: "var(--shadow-sm)",
            }}>
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 14, flexWrap: "wrap", gap: 10 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
                  <div style={{
                    width: 34, height: 34, borderRadius: 10,
                    background: complete ? "var(--teal)" : "var(--bg-light)",
                    color: complete ? "#fff" : "var(--text-mute)",
                    display: "flex", alignItems: "center", justifyContent: "center",
                    fontSize: 14, fontWeight: 800,
                  }}>{si+1}</div>
                  <div>
                    <div style={{ fontSize: 16, fontWeight: 800, color: "var(--text)" }}>{sName}</div>
                    <div style={{ fontSize: 12, color: "var(--text-mute)", marginTop: 2 }}>
                      {books.length} {books.length === 1 ? "livro" : "livros"}
                      {complete && <span style={{ marginLeft: 8, color: "var(--teal-dark)", fontWeight: 700 }}>✓ completa</span>}
                    </div>
                  </div>
                </div>
              </div>
              {books.length === 0 ? (
                <div style={{ padding: "18px 14px", background: "var(--bg-light)", borderRadius: 10, textAlign: "center", fontSize: 13, color: "var(--text-mute)" }}>
                  Nenhum livro selecionado ainda. Volte pro Passo 1 pra escolher.
                </div>
              ) : (() => {
                // Agrupa livros por bimestre/trimestre. Sem-período vai pra lane especial.
                const seriesBim = bimestres[si] || {};
                const lanes = { 0: [] };
                for (let p = 1; p <= periodCount; p++) lanes[p] = [];
                books.forEach(b => {
                  const bim = seriesBim[b.id];
                  if (bim >= 1 && bim <= periodCount) lanes[bim].push(b);
                  else lanes[0].push(b);
                });
                const renderTile = (b) => {
                  const curBim = seriesBim[b.id] || null;
                  const cycleBim = () => {
                    const cur = curBim;
                    const next = cur === null ? 1 : cur === periodCount ? null : cur + 1;
                    setBim(si, b.id, next);
                  };
                  const isDragging = dragging && dragging.si === si && dragging.bookId === b.id;
                  return (
                    <div key={b.id}
                      draggable
                      onDragStart={(e) => {
                        e.dataTransfer.effectAllowed = "move";
                        e.dataTransfer.setData("text/plain", `${si}:${b.id}`);
                        setDragging({ si, bookId: b.id });
                      }}
                      onDragEnd={() => { setDragging(null); setDragOver(null); }}
                      style={{
                        width: 110, position: "relative",
                        cursor: "grab",
                        opacity: isDragging ? 0.4 : 1,
                        transition: "opacity 0.15s",
                      }}>
                      <div onClick={cycleBim} style={{ cursor: "pointer" }} title={curBim ? `${curBim}º ${periodLabel} · arraste ou clique` : `Arraste pra um ${periodLabel} ou clique pra marcar`}>
                        <BookCover book={b} size="sm" />
                      </div>
                      <button onClick={() => removeBook(si, b.id)}
                        disabled={removingIds.has(b.id)}
                        title={removingIds.has(b.id) ? "Removendo…" : "Remover"}
                        style={{
                          position: "absolute", top: -6, right: -6,
                          width: 22, height: 22, borderRadius: "50%",
                          background: "#fff", border: "1px solid var(--border)",
                          fontSize: 12, color: "#b53a3a",
                          cursor: removingIds.has(b.id) ? "wait" : "pointer",
                          opacity: removingIds.has(b.id) ? 0.5 : 1,
                          boxShadow: "0 2px 6px rgba(0,0,0,0.12)",
                        }}>{removingIds.has(b.id) ? "…" : "✕"}</button>
                      <div style={{ fontSize: 11, fontWeight: 600, color: "var(--text)", marginTop: 8, lineHeight: 1.3, textAlign: "center" }}>
                        {b.title}
                      </div>
                      <div style={{ fontSize: 10, color: "var(--text-mute)", textAlign: "center", marginTop: 2 }}>
                        {(b.lang || "pt").toUpperCase()} · Scolex {b.level}
                      </div>
                    </div>
                  );
                };
                const laneDragProps = (bn) => ({
                  onDragOver: (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (!dragOver || dragOver.si !== si || dragOver.bn !== bn) setDragOver({ si, bn }); },
                  onDragLeave: (e) => {
                    if (!e.currentTarget.contains(e.relatedTarget)) {
                      setDragOver(prev => prev && prev.si === si && prev.bn === bn ? null : prev);
                    }
                  },
                  onDrop: (e) => {
                    e.preventDefault();
                    const payload = e.dataTransfer.getData("text/plain") || "";
                    const [siStr, bookId] = payload.split(":");
                    const fromSi = parseInt(siStr, 10);
                    if (Number.isFinite(fromSi) && bookId) setBim(fromSi, bookId, bn);
                    setDragOver(null);
                    setDragging(null);
                  },
                });
                const isOver = (bn) => dragOver && dragOver.si === si && dragOver.bn === bn;
                return (
                  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 14 }}>
                    {Array.from({ length: periodCount }, (_, i) => i + 1).map(bn => (
                      <div key={bn}
                        {...laneDragProps(bn)}
                        style={{
                          padding: "12px 14px",
                          background: isOver(bn) ? "#fff2c8" : (lanes[bn].length > 0 ? "linear-gradient(180deg, #fffaf0 0%, #ffffff 100%)" : "var(--bg-light)"),
                          border: `${isOver(bn) ? "2px" : "1px"} ${isOver(bn) ? "dashed" : "solid"} ${isOver(bn) ? "#d99a1f" : (lanes[bn].length > 0 ? "#f0d28a" : "var(--border)")}`,
                          borderRadius: 12,
                          minHeight: 180,
                          transition: "background 0.15s, border 0.15s",
                        }}>
                        <div style={{
                          display: "flex", alignItems: "center", justifyContent: "space-between",
                          marginBottom: 10, paddingBottom: 8,
                          borderBottom: `1px solid ${lanes[bn].length > 0 ? "#f0d28a" : "var(--border)"}`,
                        }}>
                          <div style={{ fontSize: 11, fontWeight: 800, color: lanes[bn].length > 0 ? "#a06a14" : "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.8 }}>
                            {`${bn}º ${periodLabel}`}
                          </div>
                          <div style={{ fontSize: 11, fontWeight: 700, color: "var(--text-mute)", fontVariantNumeric: "tabular-nums" }}>
                            {lanes[bn].length}
                          </div>
                        </div>
                        {lanes[bn].length === 0 ? (
                          <div style={{ fontSize: 11, color: "var(--text-mute)", fontStyle: "italic", padding: "12px 4px", textAlign: "center" }}>
                            {isOver(bn) ? "soltar aqui" : "arraste livros pra cá"}
                          </div>
                        ) : (
                          <div style={{ display: "flex", gap: 12, flexWrap: "wrap", justifyContent: "flex-start" }}>
                            {lanes[bn].map(renderTile)}
                          </div>
                        )}
                      </div>
                    ))}
                    {lanes[0].length > 0 && (
                      <div
                        {...laneDragProps(0)}
                        style={{
                          gridColumn: "1 / -1",
                          padding: "12px 14px",
                          background: isOver(0) ? "#fff2c8" : "var(--bg-light)",
                          border: `${isOver(0) ? "2px" : "1px"} dashed ${isOver(0) ? "#d99a1f" : "var(--border)"}`,
                          borderRadius: 12,
                          transition: "background 0.15s, border 0.15s",
                        }}>
                        <div style={{
                          display: "flex", alignItems: "center", gap: 8,
                          marginBottom: 10, paddingBottom: 8,
                          borderBottom: "1px dashed var(--border)",
                        }}>
                          <div style={{ fontSize: 11, fontWeight: 800, color: "var(--text-mute)", textTransform: "uppercase", letterSpacing: 0.8 }}>
                            {`sem ${periodLabel} definido`}
                          </div>
                          <div style={{ fontSize: 10, color: "var(--text-mute)", fontStyle: "italic" }}>
                            {`arraste pro ${periodLabel} certo`}
                          </div>
                          <div style={{ flex: 1 }}/>
                          <div style={{ fontSize: 11, fontWeight: 700, color: "var(--text-mute)", fontVariantNumeric: "tabular-nums" }}>
                            {lanes[0].length}
                          </div>
                        </div>
                        <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
                          {lanes[0].map(renderTile)}
                        </div>
                      </div>
                    )}
                  </div>
                );
              })()}
            </div>
          );
        })}
      </div>
    </div>
  );
}
// Legacy placeholder, no longer used
Object.assign(window, { CATALOG, SERIES_LIST, StepCuradoriaIntro, Step6_Qty, Step7_Availability, Step8_Pricing, Step9_Recommend, Step10_Detail });
