// CTox EAD — curso em layout de 3 colunas (rail · player · progresso).
// Mantém: travamento progressivo, conclusão ao assistir até o fim, player sem
// adiantar (sem seek/velocidade/PiP), localStorage e teste/certificado.

// Paleta clara (casa com o teste/certificado)
const LBG = '#F1EEE6', LCARD = '#fff', LMUTED = '#7c8a84', LINK = '#22302b',
      LBORD = '#e6e1d6', LBORD2 = '#ece7dc', ANNO = '#ff5a3c';

// ── Supabase (cadastro + validação) ─────────────────────────────────────────
const SUPABASE_URL  = 'https://xzkkwsxfwjlobxgrvjzr.supabase.co';
const SUPABASE_ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh6a2t3c3hmd2psb2J4Z3J2anpyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3OTA4Nzk0NDAsImV4cCI6MjEwNjQ1NTQ0MH0.O-8wtKo4xZbvJXk9liZuywP2OGqRq0ArNON_UURTS3s';
const REG_KEY = 'ctox-registro';
const readReg = () => { try { return JSON.parse(localStorage.getItem(REG_KEY) || 'null'); } catch { return null; } };
const saveReg = (r) => { try { localStorage.setItem(REG_KEY, JSON.stringify(r)); } catch {} };
async function supaRpc(fn, body) {
  const r = await fetch(SUPABASE_URL + '/rest/v1/rpc/' + fn, {
    method: 'POST',
    headers: { apikey: SUPABASE_ANON, Authorization: 'Bearer ' + SUPABASE_ANON, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (!r.ok) throw new Error('Supabase ' + r.status + ': ' + (await r.text()));
  const t = await r.text();
  return t ? JSON.parse(t) : null;
}
const soDigitos = (s) => (s || '').replace(/\D/g, '');
const maskCPF = (s) => { s = soDigitos(s).slice(0, 11);
  return s.replace(/(\d{3})(\d)/, '$1.$2').replace(/(\d{3})(\d)/, '$1.$2').replace(/(\d{3})(\d{1,2})$/, '$1-$2'); };
const maskCNPJ = (s) => { s = soDigitos(s).slice(0, 14);
  return s.replace(/(\d{2})(\d)/, '$1.$2').replace(/(\d{3})(\d)/, '$1.$2').replace(/(\d{3})(\d)/, '$1/$2').replace(/(\d{4})(\d{1,2})$/, '$1-$2'); };

// Lista de módulos — fonte única da estrutura do curso (15 módulos, 4 etapas).
const MODS = [
  { n: '01', title: 'Tela Inicial e Avisos', sub: 'Primeiro acesso, login, pop-up de avisos e sininho de mensagens.' },
  { n: '02', title: 'Aba de Coletas — Status', sub: 'O painel do posto, os status de cada amostra e o menu de ações.' },
  { n: '03', title: 'Coletores, Testemunhas e Usuários', sub: 'Quem pode coletar e quem pode testemunhar uma coleta.' },
  { n: '04', title: 'Dashboard de Acompanhamento', sub: 'Visão gráfica do volume de coletas do posto.' },
  { n: '05', title: 'Cadastro de Nova Coleta', sub: 'O fluxo completo da nova coleta, passo a passo.' },
  { n: '06', title: 'Receituário Médico', sub: 'Como documentar o uso de medicamentos declarado pelo doador.' },
  { n: '07', title: 'Formas de Pagamento', sub: 'Cartão, PIX, boleto, dinheiro e faturamento.' },
  { n: '08', title: 'Voucher (CNH e CLT)', sub: 'O vale-exame: tipos, validação e quando perguntar.' },
  { n: '09', title: 'Finalidades: CNH, CLT, Outros e Menor', sub: 'Regras, prazos e documentos de cada finalidade.' },
  { n: '10', title: 'Concurso Público', sub: 'O nível mais alto de responsabilidade do posto.' },
  { n: '11', title: 'Logística — Criação de Lote', sub: 'Criar o lote de envio para a retirada das amostras.' },
  { n: '12', title: 'Recoleta', sub: 'Quando acontece, como resolver e como evitar.' },
  { n: '13', title: 'Contraprova', sub: 'O direito do doador de contestar um resultado.' },
  { n: '14', title: 'Pontos Importantes e LGPD', sub: 'Onde sai o resultado, proteção de dados e boas práticas.' },
  { n: '15', title: 'Módulo Financeiro (ADM)', sub: 'Repasses, fechamento mensal e inadimplência — perfil administrativo.' },
];

const TRILHAS = [
  { id: 'A', name: 'Fundamentos', sub: 'Primeiro acesso e leitura do sistema', mods: ['01', '02', '03', '04'] },
  { id: 'B', name: 'Cadastro de Coleta', sub: 'O fluxo completo da nova coleta', mods: ['05', '06', '07', '08', '09', '10'] },
  { id: 'C', name: 'Operação e Encerramento', sub: 'Logística, recoleta, contraprova e boas práticas', mods: ['11', '12', '13', '14'] },
  { id: 'D', name: 'Financeiro', sub: 'Repasses e fechamento (perfil administrativo)', mods: ['15'] },
];
const ETAPA_NUM = { A: 1, B: 2, C: 3, D: 4 };

const ALL = MODS;
const byNum = (n) => ALL.find((m) => m.n === n);

const ORDER = ALL.map((m) => m.n);
const STORE_KEY = 'ctox-course-done';
const readDone = () => { try { return JSON.parse(localStorage.getItem(STORE_KEY) || '[]'); } catch { return []; } };
const isUnlocked = (n, done) => { const i = ORDER.indexOf(n); return i <= 0 ? true : done.includes(ORDER[i - 1]); };
const nextOf = (n) => { const i = ORDER.indexOf(n); return (i >= 0 && i < ORDER.length - 1) ? ORDER[i + 1] : null; };
const TRILHA_POR_ULTIMO = {};
TRILHAS.forEach((t) => { TRILHA_POR_ULTIMO[t.mods[t.mods.length - 1]] = t; });
const trilhaDe = (n) => TRILHAS.find((t) => t.mods.includes(n));
const etapaLabel = (n) => { const t = trilhaDe(n); return t ? 'Etapa ' + ETAPA_NUM[t.id] : ''; };
const firstToWatch = (done) => {
  for (const n of ORDER) if (!done.includes(n) && isUnlocked(n, done)) return n;
  return ORDER[ORDER.length - 1];
};

// Módulo 15 (Financeiro) é só para o perfil administrativo: liberado por senha.
// O certificado/teste libera ao concluir os módulos comuns (01–14), sem exigir o 15.
const ADM_MOD = '15';
const COMMON = ORDER.filter((n) => n !== ADM_MOD);
const commonDone = (done) => COMMON.every((n) => done.includes(n));
const ADM_OK_KEY = 'ctox-adm-ok';
const readAdmOk = () => { try { return localStorage.getItem(ADM_OK_KEY) === '1'; } catch { return false; } };
// Hash SHA-256 da senha dos ADMs (a senha em texto não fica no repositório).
// Para trocar a senha, me mande a nova que eu regenero este hash.
const SENHA_ADM_SHA256 = 'eecace72d45b6b0b8429c16a452a9b63c060aa022acc6da8b4829e301a62d935';
async function sha256hex(s) {
  const b = new TextEncoder().encode(s);
  const h = await crypto.subtle.digest('SHA-256', b);
  return Array.from(new Uint8Array(h)).map((x) => x.toString(16).padStart(2, '0')).join('');
}
// Acesso a um módulo: sequência normal; o 15 exige também a senha de ADM liberada.
const canAccess = (n, done, admOk) => isUnlocked(n, done) && (n !== ADM_MOD || admOk);

// ── Descrições no estilo da apostila (Etapa 1 semeada; demais caem no sub) ────
// paras: parágrafos (aceita **negrito**). prints: telas refeitas em verde c/ marcação.
const DESCRICOES = {
  '01': { paras: [
    'O acesso ao CTox é feito com **CPF e senha do próprio profissional** (o sistema também aceita CNPJ ou e-mail). **Prefira o CPF** — é ele que identifica com clareza quem realizou cada ação. Cada pessoa usa o seu login, nunca um código genérico do posto.',
    'Ao entrar, o sistema exibe um **pop-up de avisos e comunicados**. Leia **antes de iniciar qualquer ação**: é ali que a Synvia publica mudanças de regra, orientações e avisos importantes para a operação do posto.',
    'Fique atento ao **sininho de mensagens** no topo da tela. Por ele chegam as notificações do posto — como **avisos de recoleta** e recados direcionados. Confira todos os dias.',
    'Desde a primeira tela você já lida com **dados sensíveis de doadores**. Sigilo é regra desde o começo — a **LGPD** é detalhada no módulo final.',
  ], prints: [{
    src: 'prints/mod01-avisos.png',
    ring: { x: '22%', y: '20%', w: '61%', h: '68%' },
    badge: { x: '19.5%', y: '16.5%', n: '1' },
    cap: '**① Pop-up de avisos:** aparece ao entrar no CTox. Leia antes de qualquer ação. As mensagens individuais do posto (recoleta, recados) chegam pelo **sininho** no topo.',
  }] },
  '02': { paras: [
    'A **aba de Coletas** lista todas as coletas do posto com o respectivo **status**. Entender cada status evita erro de interpretação de resultado.',
    'Atenção ao **"Aguardando Confirmatório"**: significa que a triagem detectou algo que precisa de análise confirmatória — **não é um resultado positivo**. Só o laudo final confirma.',
    'Os **dados de contato** cadastrados na coleta devem ser sempre **do doador**, nunca do posto — é por eles que o titular recebe o laudo.',
    'No **ícone de três pontos** (Ações) de cada coleta abrem-se funções conforme o **perfil do seu login**: **Ver mais** (dados do cadastro da amostra), **Ver FCC** (conferir e imprimir a ficha de cadeia de custódia), **Nota fiscal** (visualizar e baixar a NF) e **Deletar** (excluir uma amostra cadastrada por engano).',
  ], prints: [{
    src: 'prints/mod02-coletas.png',
    ring: { x: '19%', y: '37%', w: '15%', h: '46%' },
    badge: { x: '16%', y: '33.5%', n: '1' },
    cap: '**① Situação da Análise:** a coluna onde você acompanha o status de cada amostra. Lembre: *Aguardando* / *Em Confirmatório* **não** é resultado positivo.',
  }] },
  '03': { paras: [
    'Um **coletor** só fica habilitado reunindo **seis condições**: ter **vínculo empregatício** com o posto; ser **maior de idade**; assistir ao **vídeo oficial** de treinamento pelo próprio login; ser **aprovado na prova** de aptidão; solicitar o **certificado** de coleta emitido pela Synvia; e manter a habilitação **válida — vale 1 ano** e precisa ser renovada.',
    'A **testemunha** também precisa ser **maior de idade** e ter vínculo empregatício com o posto. **Nunca** pode ser familiar, amigo ou conhecido do doador, e deve permanecer na sala durante todo o procedimento.',
    'Na aba de **Usuários** ficam todas as pessoas vinculadas ao CNPJ do posto — onde se edita dados cadastrais e se redefinem senhas.',
  ], prints: [{
    src: 'prints/mod03-coletor-testemunha.png',
    ring: { x: '50%', y: '36%', w: '44%', h: '44%' },
    badge: { x: '47%', y: '32%', n: '1' },
    cap: '**① Testemunha:** cadastrada a cada nova coleta. Maior de idade, com vínculo empregatício, e nunca familiar, amigo ou conhecido do doador.',
  }] },
  '04': { paras: [
    'O **Dashboard de Acompanhamento** dá a visão geral do posto: volumes, status e evolução das coletas ao longo do tempo.',
    'Use-o para acompanhar pendências e conferir se as coletas estão fluindo até o envio das amostras.',
  ], prints: [{
    src: 'prints/mod04-dashboard.png',
    ring: { x: '9%', y: '27%', w: '82%', h: '16%' },
    badge: { x: '6.5%', y: '23%', n: '1' },
    cap: '**① Resumo do período:** total, máximo, mínimo e média de coletas — a leitura rápida do desempenho do posto. Alterne entre Ano, Mês, Semana e Dia.',
  }] },
  '05': { paras: [
    'O cadastro de uma nova coleta começa em **Coletas → Nova Coleta**. O sistema abre um assistente em etapas: Laboratório, Coletor, Finalidade, Doador, Pagamento, Testemunha e Amostra. Preencha na ordem, conferindo cada campo antes de avançar.',
    'Na etapa **Finalidade**, informe o responsável pelo atendimento e selecione a finalidade correta conforme o objetivo do doador (CNH, CLT, CLT + CNH, Outros ou Concurso).',
  ], prints: [{
    src: 'prints/mod05-finalidade.png',
    arrow: { x: '4%', y: '40%', w: '22%', h: '26%',
             d: 'M92 6 Q40 20 16 78', head: 'M16 78 l14 -6 M16 78 l4 -14' },
    badge: { x: '9%', y: '58%', n: '1' },
    cap: '**① Ponto destacado:** a finalidade **CNH** aparece com o anel de destaque e o balão de instrução — um dos frames que serão animados no vídeo.',
  }] },
  '06': { paras: [
    'Quando o doador declara uso de **qualquer medicamento** — contínuo ou recente — é obrigatório **documentar com a receita**. Não citamos marcas nem tipos de remédio: o que vale é registrar o que foi declarado e anexar o documento.',
    'A receita precisa ter **data anterior à data da coleta** e trazer a **assinatura e o carimbo do médico**. Sem esses elementos, o documento não é válido para o médico revisor da Synvia.',
    'Anexe a receita **digitalmente no sistema**, no momento do cadastro, e preencha substância, período e frequência de uso. Receita apenas física, enviada junto com a amostra mas **sem o anexo digital**, não permite o vínculo e pode comprometer o laudo.',
  ], prints: [{
    src: 'prints/mod06-receituario.png',
    ring: { x: '69%', y: '47%', w: '29%', h: '10%' },
    badge: { x: '66.5%', y: '44%', n: '1' },
    cap: '**① + Adicionar Receita:** é por aqui que a receita é anexada digitalmente, no próprio cadastro. Antes de anexar, confira data anterior à coleta, assinatura e carimbo do médico.',
  }] },
  '07': { paras: [
    'Depois de preencher os dados do cadastro, escolha a **forma de pagamento**. Cada modalidade tem regras próprias que precisam ser seguidas à risca.',
    '**Cartão:** link por e-mail/SMS, pago no momento da coleta. **PIX:** chave gerada na hora, com confirmação automática — aguarde confirmar antes de liberar. **Boleto:** usado quando o **próprio doador paga o valor integral**; vence em **3 dias úteis** e, após vencer, **não** pode ser reemitido pelo posto (acione a Synvia). O boleto pode aparecer no **DDA** do banco e em plataformas como a **Serasa**, sempre **no nome do doador**. **Dinheiro:** o posto recebe o valor e repassa à Synvia semanalmente (ver Módulo 15). **Faturamento Laboratório:** quando há empresa parceira, o PCL emite a nota fiscal, recebe da empresa e repassa à Synvia o valor do exame (fechamento mensal).',
  ], prints: [{
    src: 'prints/mod07-pagamento.png',
    ring: { x: '6%', y: '54%', w: '40%', h: '22%' },
    badge: { x: '3.5%', y: '50%', n: '1' },
    cap: '**① Forma de Pagamento:** cartão, boleto, dinheiro ou PIX. O **boleto** sai no nome do doador (3 dias úteis) e o **PIX** exige aguardar a confirmação. O **Faturamento Laboratório** (empresa parceira) é tratado à parte.',
  }] },
  '08': { paras: [
    'O **voucher** é um vale-exame que cobre o custo da coleta. Há dois tipos. O **voucher de CNH** é **comprado pelo doador no site da Synvia** e usado na coleta. O **voucher de CLT** chega pelo **sistema de empresas** — a empresa o disponibiliza ao funcionário.',
    'Pergunte **antes** de selecionar a forma de pagamento se o doador tem voucher: a preferência do cadastro é pelo **código do voucher**, e **não dá para incluí-lo depois** de finalizar o cadastro. O exame pago com voucher **não gera cobrança ao doador**.',
  ], prints: [{
    src: 'prints/mod08-voucher.png',
    ring: { x: '66%', y: '44%', w: '32%', h: '14%' },
    badge: { x: '63.5%', y: '41%', n: '1' },
    cap: '**① Vale-exame:** marque **Sim** e informe o **Nº do Vale Exame** ainda na etapa de Finalidade — antes de escolher a forma de pagamento.',
  }] },
  '09': { paras: [
    'A **finalidade** define as regras do exame. **CNH:** motoristas (categorias C, D, E e primeira habilitação A/B); a inclusão no Renach é automática. **CLT:** o pagamento é **sempre do empregador**, nunca do empregado — cadastre nome e CNPJ da empresa.',
    '**CLT + CNH:** finalidade dupla, com inclusão no Renach e laudo no nome da empresa. **Outros:** casos sem órgão regulador (particular, judicial) — exige ficha de anamnese assinada.',
    '**Menor de idade:** o **doador é o próprio menor** — cadastre os **dados do menor**. Envie, junto com a amostra, os **documentos do menor e do responsável legal** e, quando houver, o **termo de guarda**. **Só o responsável legal assina** (pai/mãe ou guarda formalizada); o menor **não assina**. Avós, tios e padrastos **não podem** sem guarda formal.',
  ], prints: [{
    src: 'prints/mod09-finalidade.png',
    ring: { x: '10.5%', y: '60%', w: '46%', h: '20%' },
    badge: { x: '8%', y: '56%', n: '1' },
    cap: '**① Finalidade:** cada opção tem regras próprias de pagamento, órgão e documentação. Selecione conforme o objetivo do doador.',
  }] },
  '10': { paras: [
    'Concurso público é o **nível mais alto de responsabilidade** do posto. Um erro de procedimento pode gerar **ação judicial por "perda de chance"**, movida pelo candidato prejudicado, com responsabilização do posto e da Synvia e **indenizações milionárias**.',
    '**Antes de aceitar:** receba o edital com a data de apresentação à banca e **envie à Synvia** para análise (resposta em até 24h). Só **agende após a aprovação formal** — nunca colete um exame de concurso sem essa aprovação.',
    '**No ato da coleta:** busque o nome exato do edital aprovado, **imprima e colha a assinatura do Termo de Ciência** (obrigatório) e confira material, FCC, numeração e envelopes. Fique atento ao **escopo** exigido (padrão ou 50+) e ao laudo lacrado, se pedido.',
    '**Regra de ouro:** o laudo de concurso **não admite recoleta corretiva** dentro do prazo da banca — um erro aqui costuma ser irreversível.',
  ], prints: [{
    src: 'prints/mod10-concurso.png',
    ring: { x: '43%', y: '58%', w: '13%', h: '22%' },
    badge: { x: '40.5%', y: '54%', n: '1' },
    cap: '**① Concurso:** selecione esta finalidade somente depois da aprovação formal da Synvia sobre o edital.',
  }] },
  '11': { paras: [
    'Ao final de cada dia de coletas é **obrigatório criar um lote** no sistema para a logística agendar a retirada. **Sem o lote, a transportadora não passa.**',
    '**Passo a passo:** aba **Enviar Amostras → Novo Pacote (BAG) → Incluir Pendente**, selecione as amostras do dia, **Incluir**, informe o número da BAG e **Concluir**. Sem BAG física, informe "sem BAG" e o sistema gera um número (anote também no envelope).',
    'O lote **não é só operacional** — é exigência normativa de **rastreabilidade**: saber quais amostras estão em trânsito, com qual transportadora e destino. Coletas até as **17h** são programadas para o próximo dia útil.',
    'Qualquer problema na retirada, fale com o **atendimento Synvia** — nunca diretamente com a transportadora.',
  ], prints: [{
    src: 'prints/mod11-lote.png',
    ring: { x: '82.5%', y: '2%', w: '16%', h: '6.5%' },
    badge: { x: '80%', y: '0.5%', n: '1' },
    cap: '**① Novo Pacote (BAG):** ao fim do dia, crie o lote aqui — sem ele a transportadora não retira as amostras.',
  }] },
  '12': { paras: [
    '**Recoleta** é quando uma amostra enviada tem um erro que, por norma, impede a análise — é sempre o **pior cenário** para todos.',
    '**Causas comuns:** erro na FCC; **divergência** entre o número do sistema e o dos envelopes/FCC; material insuficiente; dados do doador errados. **A falta de receita médica não gera recoleta.** A Synvia **avisa por e-mail** qual amostra, qual o erro e o que corrigir — confira o e-mail diariamente.',
    '**Como fazer:** na aba **Recoleta**, informe o número da amostra com erro e confirme. O sistema gera um **novo kit, com nova numeração**, **vinculado ao pagamento da primeira coleta**. A re-coleta **não tem custo adicional** para o doador nem para o posto.',
    'A re-coleta exige **coletar novo material do doador**, o que nem sempre é simples de repetir. Uma coleta correta na origem garante o laudo no prazo. Na dúvida, fale com o atendimento Synvia **antes de finalizar** o cadastro.',
  ], prints: [{
    src: 'prints/mod12-recoleta.png',
    ring: { x: '48.5%', y: '9%', w: '16%', h: '6%' },
    badge: { x: '46%', y: '6.5%', n: '1' },
    cap: '**① Aba Recoletas:** informe o número da amostra com erro e gere um novo kit — nova numeração, vinculado ao pagamento da 1ª coleta, sem custo extra.',
  }] },
  '13': { paras: [
    'A **contraprova** é um **direito do doador**: se ele quiser contestar um resultado, pode solicitar uma **nova análise do material já coletado**.',
    'O doador **preenche um requerimento** e o posto **envia ao Atendimento da Synvia**. A análise é feita em **até 5 dias úteis a partir do envio** do requerimento.',
    'O **laudo da contraprova vai por e-mail** ao doador e **não aparece no sistema** do posto. Oriente o doador sobre o prazo e o canal.',
  ] },
  '14': { paras: [
    '**Onde sai o resultado:** o doador acompanha e acessa o laudo pelo site **resultado.ctox.io** (https://resultado.ctox.io/). Oriente sempre o doador a acompanhar por lá — o acompanhamento ativo evita atrasos. Este módulo é liberado a **todos os participantes**.',
    '**LGPD — o laudo só vai para o doador.** O exame contém dados sensíveis. É **proibido** enviar o laudo por WhatsApp ou entregá-lo a pais, filhos, cônjuge, empresa ou terceiros. **Permitido** apenas ao **doador** (e-mail do titular ou pelo sistema).',
    '**CLT:** o empregador acessa só o **laudo médico** (positivo/negativo) — nunca as substâncias, quantidades ou o laudo toxicológico completo.',
    '**Erro comum na CNH:** muitos fazem o toxicológico **antes** de iniciar o processo da CNH, e aí o exame não aparece no BCA/SENATRAN nem entra no Renach. **Sempre pergunte** se o doador já iniciou o processo. Ordem correta: início do processo → taxas → exames iniciais → toxicológico.',
    '**Boas práticas:** explique o processo com clareza, nunca diga "é automático", revise o cadastro antes de finalizar e trate cada coleta como crítica. Normativos: CONTRAN 923/2022, NIT DICLA 069 e Portaria MTE 612/2024.',
  ], prints: [{
    src: 'prints/mod14-lgpd.png',
    ring: { x: '6%', y: '25%', w: '43%', h: '26%' },
    badge: { x: '3.5%', y: '21.5%', n: '1' },
    cap: '**① Proibido:** o laudo nunca vai por WhatsApp nem a terceiros — somente ao próprio doador.',
  }] },
  '15': { paras: [
    '**Módulo administrativo.** O financeiro controla o repasse entre o posto e a Synvia e normalmente é operado pelo **perfil administrativo** do posto.',
    'A tela do financeiro tem **três partes**. **1) Boletos semanais** (modalidade dinheiro): trazem o valor, o período e o **relatório** das amostras pagas em dinheiro, com **download**; o boleto vence toda terça. **2) Fechamento mensal:** mostra o **valor a receber**, o **valor a pagar**, o **valor da nota fiscal** e o **botão de upload** da NF. **3) Relatórios por método de pagamento:** para conferência (cartão, boleto, PIX, faturamento).',
    'No fechamento mensal, o relatório fica disponível até o **2º dia útil**. Confira **como o cálculo é feito** e **como se chega ao valor final da NF**, **anexe a nota fiscal** pelo botão de upload e envie. A Synvia repassa em até **5 dias úteis** após receber a NF.',
    '**Inadimplência:** **dois boletos em aberto** bloqueiam a modalidade dinheiro — e eles **não precisam ser consecutivos**. A partir daí, só o **PIX** fica disponível para novos cadastros.',
  ], prints: [{
    src: 'prints/mod15-financeiro.png',
    ring: { x: '57%', y: '14%', w: '41%', h: '13%' },
    badge: { x: '54.5%', y: '11%', n: '1' },
    cap: '**① Nota Fiscal:** no fechamento mensal, anexe a NF em PDF pelo botão **Adicionar Nota Fiscal** (ou veja a já enviada). É assim que o repasse é liberado. Inadimplência: 2 boletos em aberto — não precisam ser consecutivos.',
  }] },
};

// ── helpers de render ────────────────────────────────────────────────────────
function RT({ t }) {
  const parts = String(t).split('**');
  return parts.map((p, i) => i % 2 ? <b key={i}>{p}</b> : <React.Fragment key={i}>{p}</React.Fragment>);
}
function PrintAnotado({ src, ring, arrow, badge, cap }) {
  return (
    <div style={{ margin: '10px 0 4px' }}>
      <div style={{ position: 'relative', borderRadius: 14, overflow: 'hidden', border: '1px solid #dfe6df' }}>
        <img src={src} alt="" style={{ width: '100%', display: 'block' }} />
        {ring && <div style={{ position: 'absolute', border: `3px solid ${ANNO}`, borderRadius: 12,
          boxShadow: '0 0 0 4px rgba(255,90,60,.18)', left: ring.x, top: ring.y, width: ring.w, height: ring.h }} />}
        {arrow && <svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: 'absolute',
          left: arrow.x, top: arrow.y, width: arrow.w, height: arrow.h, overflow: 'visible' }}>
          <path d={arrow.d} fill="none" stroke={ANNO} strokeWidth="4" strokeLinecap="round" />
          <path d={arrow.head} fill="none" stroke={ANNO} strokeWidth="4" strokeLinecap="round" />
        </svg>}
        {badge && <div style={{ position: 'absolute', left: badge.x, top: badge.y, width: 24, height: 24,
          borderRadius: '50%', background: ANNO, color: '#fff', fontWeight: 800, fontSize: 13,
          display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 2px 8px rgba(0,0,0,.25)' }}>{badge.n}</div>}
      </div>
      {cap && <p style={{ fontSize: 13.5, color: LMUTED, margin: '8px 0 0', lineHeight: 1.5 }}><RT t={cap} /></p>}
    </div>
  );
}

// ── Player de vídeo sem adiantar (sem seek/velocidade/PiP) ───────────────────
function VideoModulo({ src, poster, allowSeek, onEnded, onError }) {
  const ref = React.useRef(null);
  const maxRef = React.useRef(0);
  const [playing, setPlaying] = React.useState(false);
  const [muted, setMuted] = React.useState(false);
  const [cur, setCur] = React.useState(0);
  const [dur, setDur] = React.useState(0);
  const fmt = (t) => { t = Math.max(0, t | 0); return `${(t / 60) | 0}:${String(t % 60).padStart(2, '0')}`; };
  // Pausa o vídeo quando a aba perde o foco / o app vai pro fundo (celular).
  React.useEffect(() => {
    const onHide = () => { const v = ref.current; if (v && document.hidden && !v.paused) v.pause(); };
    document.addEventListener('visibilitychange', onHide);
    window.addEventListener('pagehide', onHide);
    window.addEventListener('blur', onHide);
    return () => { document.removeEventListener('visibilitychange', onHide);
      window.removeEventListener('pagehide', onHide); window.removeEventListener('blur', onHide); };
  }, []);
  const toggle = () => { const v = ref.current; if (!v) return; v.paused ? v.play() : v.pause(); };
  const onTime = () => { const v = ref.current; if (!v) return;
    if (v.currentTime > maxRef.current) maxRef.current = v.currentTime; setCur(v.currentTime); };
  // Sem adiantar durante o curso; após concluir (allowSeek) libera navegar no tempo.
  const blockSeek = () => { const v = ref.current; if (!v || allowSeek) return;
    if (v.currentTime > maxRef.current + 0.4) v.currentTime = maxRef.current; };
  const seek = (e) => { if (!allowSeek) return; const v = ref.current; if (!v || !dur) return;
    const r = e.currentTarget.getBoundingClientRect();
    v.currentTime = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)) * dur; };
  return (
    <div style={{ position: 'absolute', inset: 0, background: '#000' }}>
      <video ref={ref} src={src} poster={poster} autoPlay playsInline
        disablePictureInPicture controlsList="nodownload noplaybackrate nofullscreen noremoteplayback"
        onClick={toggle}
        onPlay={() => setPlaying(true)} onPause={() => setPlaying(false)}
        onTimeUpdate={onTime} onSeeking={blockSeek}
        onLoadedMetadata={(e) => setDur(e.target.duration)}
        onRateChange={(e) => { if (e.target.playbackRate !== 1) e.target.playbackRate = 1; }}
        onEnded={onEnded} onError={onError}
        style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain', background: '#000' }} />
      {!playing && (
        <div onClick={toggle} style={{ position: 'absolute', inset: 0, display: 'flex',
          alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
          <div style={{ width: 84, height: 84, borderRadius: '50%', background: 'rgba(178,232,71,0.94)',
            display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 6px 24px rgba(0,0,0,0.4)' }}>
            <svg width="32" height="36" viewBox="0 0 15 17"><path d="M2 1.5l11 7-11 7z" fill="#1C4137" /></svg>
          </div>
        </div>
      )}
      <div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center',
        gap: 14, padding: '16px 20px 14px', background: 'linear-gradient(transparent, rgba(0,0,0,0.55))' }}>
        <button onClick={toggle} style={{ all: 'unset', cursor: 'pointer', color: '#fff', display: 'flex' }}>
          {playing
            ? <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" /><rect x="14" y="5" width="4" height="14" /></svg>
            : <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>}
        </button>
        <span data-tnum style={{ color: '#fff', fontSize: 13, minWidth: 92 }}>{fmt(cur)} / {fmt(dur)}</span>
        <div onClick={seek} title={allowSeek ? 'Arraste/clique para navegar' : ''}
          style={{ flex: 1, height: allowSeek ? 8 : 5, background: 'rgba(255,255,255,0.25)', borderRadius: 4,
            overflow: 'hidden', cursor: allowSeek ? 'pointer' : 'default' }}>
          <div style={{ height: '100%', width: `${dur ? cur / dur * 100 : 0}%`, background: CTX_GREEN_LIME }} />
        </div>
        <button onClick={() => { const v = ref.current; v.muted = !v.muted; setMuted(v.muted); }}
          style={{ all: 'unset', cursor: 'pointer', color: '#fff', display: 'flex' }}>
          {muted
            ? <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 9v6h4l5 4V5L8 9H4z" /><line x1="17" y1="9" x2="22" y2="15" /><line x1="22" y1="9" x2="17" y2="15" /></svg>
            : <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 9v6h4l5 4V5L8 9H4z" /><path d="M16 8a5 5 0 0 1 0 8" /></svg>}
        </button>
      </div>
    </div>
  );
}

// ── Barra lateral esquerda ───────────────────────────────────────────────────
function NavItem({ icon, label, active, onClick, locked, title }) {
  return (
    <button onClick={onClick} disabled={locked} title={title || ''}
      style={{ all: 'unset', display: 'flex', alignItems: 'center', gap: 13, padding: '13px 15px',
        borderRadius: 13, fontWeight: 600, fontSize: 15.5, cursor: locked ? 'not-allowed' : 'pointer',
        color: active ? '#fff' : (locked ? '#b3bcb7' : '#4a5852'),
        background: active ? CTX_BLUE : 'transparent' }}>
      <span style={{ display: 'flex', color: active ? CTX_GREEN_LIME : '#6c7a74' }}>{icon}</span>{label}
    </button>
  );
}
function LeftRail({ tudoConcluido, onOverview }) {
  return (
    <aside style={{ padding: '26px 20px', borderRight: `1px solid ${LBORD}`, display: 'flex',
      flexDirection: 'column', gap: 6, overflow: 'auto' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 11, fontWeight: 800, fontSize: 20,
        letterSpacing: '-0.02em', color: CTX_BLUE, margin: '2px 4px 24px' }}>
        <SynviaArrow size={24} /> CTox · Synvia
      </div>
      <NavItem onClick={onOverview} label="Conteúdo do curso" active
        icon={<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 6h16M4 12h16M4 18h10" strokeLinecap="round" /></svg>} />
      <a href={tudoConcluido ? 'teste.html' : undefined}
        title={tudoConcluido ? '' : 'Conclua todas as aulas para liberar'}
        style={{ all: 'unset', display: 'flex', alignItems: 'center', gap: 13, padding: '13px 15px',
          borderRadius: 13, fontWeight: 600, fontSize: 15.5, textDecoration: 'none',
          cursor: tudoConcluido ? 'pointer' : 'not-allowed', color: tudoConcluido ? '#4a5852' : '#b3bcb7' }}>
        <span style={{ display: 'flex', color: tudoConcluido ? CTX_BLUE : '#b3bcb7' }}>
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 11l3 3L22 4" /><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" /></svg>
        </span>Teste &amp; Certificação
      </a>
      <div style={{ marginTop: 'auto', background: '#fff', border: `1px solid ${LBORD2}`,
        borderRadius: 16, padding: 18, textAlign: 'center' }}>
        <div style={{ fontWeight: 800, color: CTX_BLUE, fontSize: 15, marginBottom: 4 }}>Certificado</div>
        <div style={{ fontSize: 12.5, color: LMUTED, lineHeight: 1.4, marginBottom: 12 }}>
          Conclua os módulos 1 a 14 e faça o teste para emitir.</div>
        <a href={tudoConcluido ? 'teste.html' : undefined}
          style={{ all: 'unset', display: 'inline-block', background: CTX_GREEN_LIME, color: CTX_BLUE,
            fontWeight: 800, fontSize: 13, padding: '9px 16px', borderRadius: 999,
            cursor: tudoConcluido ? 'pointer' : 'not-allowed', opacity: tudoConcluido ? 1 : 0.55 }}>
          {tudoConcluido ? 'Fazer o teste →' : 'Ver requisitos →'}
        </a>
      </div>
    </aside>
  );
}

// ── Barra lateral direita (progresso por etapa + animação de check) ──────────
function CheckIcon() {
  return <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke={CTX_BLUE} strokeWidth="3.4"
    strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>;
}
function ProgItem({ m, state, justDone, onClick }) {
  const locked = state === 'locked';
  const dotBase = { width: 26, height: 26, borderRadius: '50%', flexShrink: 0, display: 'flex',
    alignItems: 'center', justifyContent: 'center' };
  let dot;
  if (state === 'done') dot = <div style={{ ...dotBase, background: CTX_GREEN_LIME,
    animation: justDone ? 'checkPop .55s ease .05s both' : 'none' }}><CheckIcon /></div>;
  else if (state === 'current') dot = <div style={{ ...dotBase, background: '#fff',
    border: `2px solid ${CTX_GREEN_LIME}`, boxShadow: '0 0 0 4px rgba(178,232,71,0.3)' }}>
    <svg viewBox="0 0 24 24" width="11" height="11" fill={CTX_BLUE}><path d="M8 5v14l11-7z" /></svg></div>;
  else dot = <div style={{ ...dotBase, background: '#eef0ed', border: '1px solid #e0e4df' }}>
    <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="#9aa8a2" strokeWidth="2.4"
      strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="11" width="14" height="10" rx="2" /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></svg></div>;
  return (
    <button onClick={locked ? undefined : onClick} disabled={locked}
      title={locked ? 'Conclua o módulo anterior para liberar' : ''}
      style={{ all: 'unset', display: 'flex', alignItems: 'center', gap: 13, padding: '9px 4px',
        cursor: locked ? 'not-allowed' : 'pointer', width: '100%', boxSizing: 'border-box' }}>
      <div style={{ position: 'relative', zIndex: 1 }}>
        {dot}
        {justDone && state === 'done' && <span style={{ position: 'absolute', inset: 0, borderRadius: '50%',
          border: `2px solid ${CTX_GREEN_LIME}`, animation: 'ringOut .7s ease .05s both' }} />}
      </div>
      <div style={{ fontSize: 14.5, lineHeight: 1.3,
        fontWeight: state === 'current' ? 800 : (locked ? 500 : 600),
        color: state === 'current' ? CTX_BLUE : (locked ? '#9aa8a2' : '#3c4a44') }}>
        {m.title}
        {state === 'current' && <span style={{ marginLeft: 8, fontSize: 10.5, fontWeight: 800,
          color: CTX_BLUE, background: CTX_GREEN_LIME, padding: '2px 7px', borderRadius: 6 }}>assistindo</span>}
      </div>
    </button>
  );
}
function Progresso({ done, currentN, justDone, onSelect, admOk, narrow }) {
  const feito = done.filter((n) => COMMON.includes(n)).length;
  return (
    <aside style={{ padding: narrow ? '20px 16px 40px' : '30px 24px',
      borderLeft: narrow ? 'none' : `1px solid ${LBORD}`,
      borderTop: narrow ? `1px solid ${LBORD}` : 'none', overflow: 'auto' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 18 }}>
        <h3 style={{ fontSize: 19, fontWeight: 800, color: CTX_BLUE, margin: 0 }}>Seu progresso</h3>
        <span data-tnum style={{ fontSize: 14, fontWeight: 700, color: LMUTED }}>{feito} / {COMMON.length}</span>
      </div>
      <div style={{ background: LCARD, border: `1px solid ${LBORD2}`, borderRadius: 18, padding: '20px 18px' }}>
        {TRILHAS.map((tr) => {
          const trLocked = !isUnlocked(tr.mods[0], done);
          return (
            <div key={tr.id} style={{ marginBottom: 6 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 4px 10px',
                opacity: trLocked ? 0.55 : 1 }}>
                <span style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.10em', color: trLocked ? '#8a968f' : CTX_BLUE,
                  background: trLocked ? '#edeeec' : '#eef6dc', padding: '3px 8px', borderRadius: 6 }}>
                  ETAPA {ETAPA_NUM[tr.id]}</span>
                <span style={{ fontSize: 13, fontWeight: 700, color: '#55635d' }}>{tr.name}</span>
                {trLocked && <span style={{ marginLeft: 'auto', display: 'flex' }}>
                  <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#9aa8a2" strokeWidth="2.4"
                    strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="11" width="14" height="10" rx="2" /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></svg></span>}
              </div>
              <div style={{ position: 'relative', paddingLeft: 6, marginBottom: 14 }}>
                <div style={{ position: 'absolute', left: 18, top: 16, bottom: 16, width: 2,
                  background: '#e7ead9', borderRadius: 2 }} />
                {tr.mods.map((n) => {
                  const m = byNum(n); if (!m) return null;
                  let visual;
                  if (done.includes(n)) visual = 'done';
                  else if (n === currentN) visual = 'current';
                  else if (n === ADM_MOD) visual = !isUnlocked(n, done) ? 'locked' : (admOk ? 'avail' : 'adm');
                  else visual = isUnlocked(n, done) ? 'avail' : 'locked';
                  return <ProgItemWrap key={n} m={m} visual={visual} justDone={justDone === n}
                    onClick={() => onSelect(n)} />;
                })}
              </div>
            </div>
          );
        })}
      </div>
    </aside>
  );
}
// wrapper que traduz 'avail' (desbloqueado, não atual) num item clicável discreto
function ProgItemWrap({ m, visual, justDone, onClick }) {
  if (visual === 'adm') {
    // Módulo 15 (ADM): clicável, pede senha ao abrir.
    return (
      <button onClick={onClick} title="Módulo administrativo — requer senha"
        style={{ all: 'unset', display: 'flex', alignItems: 'center', gap: 13,
        padding: '9px 4px', cursor: 'pointer', width: '100%', boxSizing: 'border-box' }}>
        <div style={{ position: 'relative', zIndex: 1 }}>
          <div style={{ width: 26, height: 26, borderRadius: '50%', background: '#fff',
            border: `2px solid #cdd6d1`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="#7c8a84" strokeWidth="2.4"
              strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="11" width="14" height="10" rx="2" /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></svg></div>
        </div>
        <div style={{ fontSize: 14.5, lineHeight: 1.3, fontWeight: 600, color: '#3c4a44' }}>
          {m.title}
          <span style={{ marginLeft: 8, fontSize: 10, fontWeight: 800, color: '#7c8a84',
            background: '#eef0ed', padding: '2px 7px', borderRadius: 6, letterSpacing: '0.06em' }}>ADM</span>
        </div>
      </button>
    );
  }
  if (visual === 'avail') {
    return (
      <button onClick={onClick} style={{ all: 'unset', display: 'flex', alignItems: 'center', gap: 13,
        padding: '9px 4px', cursor: 'pointer', width: '100%', boxSizing: 'border-box' }}>
        <div style={{ position: 'relative', zIndex: 1 }}>
          <div style={{ width: 26, height: 26, borderRadius: '50%', background: '#fff',
            border: `2px solid ${CTX_GREEN_LIME}`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <svg viewBox="0 0 24 24" width="11" height="11" fill={CTX_BLUE}><path d="M8 5v14l11-7z" /></svg></div>
        </div>
        <div style={{ fontSize: 14.5, lineHeight: 1.3, fontWeight: 600, color: '#3c4a44' }}>{m.title}</div>
      </button>
    );
  }
  return <ProgItem m={m} state={visual} justDone={justDone} onClick={onClick} />;
}

// ── Centro: player + descrição ───────────────────────────────────────────────
function CenterModule({ mod, done, ended, tudoConcluido, onEnded, onError, onNext, onBack, narrow }) {
  const tr = trilhaDe(mod.n);
  const desc = DESCRICOES[mod.n];
  const nx = nextOf(mod.n);
  const fechaTrilha = TRILHA_POR_ULTIMO[mod.n];
  const proxTrilha = nx ? trilhaDe(nx) : null;
  const isCommonLast = mod.n === '14';     // fim do curso comum (libera o teste)
  const isAdmMod = mod.n === ADM_MOD;      // módulo financeiro (ADM)
  const courseDone = ended && isCommonLast;
  const admDone = ended && isAdmMod;
  return (
    <main style={{ padding: narrow ? '16px 14px 36px' : '30px 34px 60px', overflow: 'auto' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, color: LMUTED, fontSize: 15,
        fontWeight: 600, marginBottom: 20, flexWrap: 'wrap' }}>
        Curso CTox <span style={{ opacity: 0.5 }}>›</span>
        <b style={{ color: CTX_BLUE }}>{etapaLabel(mod.n)} · {tr ? tr.name : ''}</b>
        <span style={{ opacity: 0.5 }}>›</span> <b style={{ color: CTX_BLUE }}>Módulo {mod.n}</b>
      </div>

      <div style={{ position: 'relative', borderRadius: 20, overflow: 'hidden',
        aspectRatio: '16 / 9', boxShadow: '0 10px 34px rgba(28,65,55,.16)', background: '#000' }}>
        <VideoModulo key={mod.n} src={'video/mod' + mod.n + '.mp4'} allowSeek={tudoConcluido}
          onEnded={onEnded} onError={onError} />
        {ended && (
          <div style={{ position: 'absolute', inset: 0, zIndex: 5, display: 'flex', alignItems: 'center',
            justifyContent: 'center', textAlign: 'center', padding: 30,
            background: 'rgba(20,40,34,0.86)', animation: 'kitOverlayIn 400ms ease-out both' }}>
            <div style={{ maxWidth: 460 }}>
              <div style={{ width: 68, height: 68, borderRadius: '50%', background: CTX_GREEN_LIME, margin: '0 auto 16px',
                display: 'flex', alignItems: 'center', justifyContent: 'center', animation: 'checkPop .55s ease both' }}>
                <svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke={CTX_BLUE} strokeWidth="3"
                  strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
              </div>
              <div style={{ fontSize: 13, fontWeight: 800, letterSpacing: '0.2em', color: CTX_GREEN_LIME }}>
                {courseDone ? 'CURSO CONCLUÍDO' : admDone ? 'MÓDULO ADMINISTRATIVO CONCLUÍDO'
                  : (fechaTrilha ? 'ETAPA ' + ETAPA_NUM[fechaTrilha.id] + ' CONCLUÍDA' : 'MÓDULO ' + mod.n + ' CONCLUÍDO')}
              </div>
              <div style={{ fontSize: 26, fontWeight: 800, color: '#fff', margin: '8px 0 20px', letterSpacing: '-0.02em' }}>
                {courseDone ? 'Você concluiu o curso! 🎉' : admDone ? 'Módulo financeiro concluído ✓'
                  : (fechaTrilha ? `${fechaTrilha.name} ✓` : 'Boa! Vamos ao próximo.')}
              </div>
              <div style={{ display: 'flex', gap: 12, justifyContent: 'center', flexWrap: 'wrap' }}>
                {(courseDone || admDone) ? (
                  <a href="teste.html" style={{ all: 'unset', cursor: 'pointer', background: CTX_GREEN_LIME,
                    color: CTX_BLUE, borderRadius: 999, padding: '14px 34px', fontSize: 16, fontWeight: 800 }}>
                    Fazer o teste de certificação →</a>
                ) : (
                  <button onClick={onNext} style={{ all: 'unset', cursor: 'pointer', background: CTX_GREEN_LIME,
                    color: CTX_BLUE, borderRadius: 999, padding: '14px 34px', fontSize: 16, fontWeight: 800 }}>
                    Ir para o Módulo {nx} →</button>
                )}
              </div>
            </div>
          </div>
        )}
      </div>

      <div style={{ margin: '26px 0 6px' }}>
        <div style={{ fontSize: 13, fontWeight: 800, letterSpacing: '0.14em', color: LMUTED }}>MÓDULO {mod.n}</div>
        <h1 style={{ fontSize: 32, fontWeight: 800, letterSpacing: '-0.02em', color: CTX_BLUE, margin: '5px 0 0' }}>{mod.title}</h1>
      </div>
      <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', margin: '16px 0 26px' }}>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, background: '#fff',
          border: `1px solid ${LBORD2}`, borderRadius: 999, padding: '8px 15px', fontSize: 13.5, fontWeight: 600, color: '#4a5852' }}>
          <b style={{ color: CTX_BLUE }}>{etapaLabel(mod.n)}</b> · {tr ? tr.name : ''}</span>
      </div>
      <div style={{ background: LCARD, border: `1px solid ${LBORD2}`, borderRadius: 18, padding: '26px 28px' }}>
        <h2 style={{ fontSize: 15, fontWeight: 800, color: CTX_BLUE, margin: '0 0 14px' }}>DESCRIÇÃO DO MÓDULO</h2>
        {desc && desc.paras
          ? desc.paras.map((p, i) => <p key={i} style={{ fontSize: 15.5, lineHeight: 1.6, color: '#3c4a44', margin: '0 0 14px' }}><RT t={p} /></p>)
          : <p style={{ fontSize: 15.5, lineHeight: 1.6, color: '#3c4a44', margin: 0 }}>{mod.sub}</p>}
        {desc && desc.prints && desc.prints.map((pr, i) => <PrintAnotado key={i} {...pr} />)}
        {desc && desc.prints && (
          <div style={{ background: '#f4fae9', border: '1px solid #dcedae', borderRadius: 10,
            padding: '10px 13px', fontSize: 13, color: '#5c6b64', marginTop: 12 }}>
            📌 As marcações (grifo/seta) de cada tela ficam indicadas aqui e servem de roteiro para as animações do vídeo.
          </div>
        )}
      </div>
    </main>
  );
}

// ── Modal de acesso ADM (Módulo 15 – Financeiro) ─────────────────────────────
function AdmModal({ onClose, onSuccess }) {
  const [pw, setPw] = React.useState('');
  const [err, setErr] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const inputRef = React.useRef(null);
  React.useEffect(() => { if (inputRef.current) inputRef.current.focus(); }, []);
  const submit = async () => {
    if (!pw || busy) return;
    setBusy(true); setErr(false);
    try { const h = await sha256hex(pw); if (h === SENHA_ADM_SHA256) { onSuccess(); return; } } catch {}
    setErr(true); setBusy(false);
  };
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 50, background: 'rgba(20,40,34,0.55)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20, animation: 'kitOverlayIn 300ms ease-out both' }}>
      <div onClick={(e) => e.stopPropagation()} style={{ width: 360, maxWidth: '100%', background: '#fff',
        borderRadius: 18, border: `1px solid ${LBORD2}`, padding: '26px 24px', boxShadow: '0 20px 60px rgba(28,65,55,.3)' }}>
        <div style={{ width: 48, height: 48, borderRadius: '50%', background: '#eef6dc', display: 'flex',
          alignItems: 'center', justifyContent: 'center', margin: '0 auto 14px' }}>
          <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke={CTX_BLUE} strokeWidth="2.2"
            strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="11" width="14" height="10" rx="2" /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></svg>
        </div>
        <h3 style={{ margin: '0 0 4px', fontSize: 19, fontWeight: 800, color: CTX_BLUE, textAlign: 'center' }}>Módulo administrativo</h3>
        <p style={{ margin: '0 0 16px', fontSize: 13.5, color: LMUTED, textAlign: 'center', lineHeight: 1.5 }}>
          O Módulo 15 (Financeiro) é restrito ao perfil administrativo. Informe a senha de acesso.</p>
        <input ref={inputRef} type="password" value={pw}
          onChange={(e) => { setPw(e.target.value); setErr(false); }}
          onKeyDown={(e) => { if (e.key === 'Enter') submit(); }}
          placeholder="Senha de ADM"
          style={{ width: '100%', boxSizing: 'border-box', padding: '12px 14px', fontSize: 15, borderRadius: 11,
            border: `1.5px solid ${err ? '#e0574a' : LBORD}`, outline: 'none', marginBottom: err ? 6 : 16 }} />
        {err && <div style={{ fontSize: 12.5, color: '#e0574a', marginBottom: 14 }}>Senha incorreta. Tente novamente.</div>}
        <div style={{ display: 'flex', gap: 10 }}>
          <button onClick={onClose} style={{ all: 'unset', flex: 1, textAlign: 'center', cursor: 'pointer',
            padding: '12px 0', borderRadius: 999, fontWeight: 700, fontSize: 14, color: '#4a5852', border: `1px solid ${LBORD2}` }}>Cancelar</button>
          <button onClick={submit} disabled={busy} style={{ all: 'unset', flex: 1, textAlign: 'center', cursor: 'pointer',
            padding: '12px 0', borderRadius: 999, fontWeight: 800, fontSize: 14, color: CTX_BLUE, background: CTX_GREEN_LIME, opacity: busy ? 0.6 : 1 }}>
            {busy ? 'Verificando…' : 'Acessar'}</button>
        </div>
      </div>
    </div>
  );
}

// ── App ──────────────────────────────────────────────────────────────────────
// ── Home de entrada: hero + painel à direita (cadastro / validar / ADM) ──────
// Autenticação por ora: o cadastro grava no Supabase (iniciar_registro). A home
// também valida certificados por código (público) e dá acesso ao painel da equipe.
function HomeField({ label, value, onChange, placeholder, opcional, inputMode, type }) {
  return (
    <label style={{ display: 'block', marginBottom: 14 }}>
      <span style={{ display: 'block', fontSize: 12.5, fontWeight: 700, color: LMUTED, marginBottom: 6, letterSpacing: '0.02em' }}>
        {label}{opcional && <span style={{ fontWeight: 500, color: '#aeb6b1' }}> (opcional)</span>}
      </span>
      <input value={value} onChange={onChange} placeholder={placeholder} inputMode={inputMode} type={type || 'text'}
        style={{ width: '100%', boxSizing: 'border-box', padding: '12px 14px', fontSize: 15.5, borderRadius: 11,
          border: `1.5px solid ${LBORD}`, background: '#fff', color: LINK, outline: 'none', fontFamily: 'Manrope, sans-serif' }} />
    </label>
  );
}

function CadastroForm({ onDone }) {
  const [f, setF] = React.useState({ nome: '', cpf: '', rg: '', ponto: '', cnpj: '' });
  const [consent, setConsent] = React.useState(false);
  const [erro, setErro] = React.useState('');
  const [enviando, setEnviando] = React.useState(false);
  const set = (k) => (e) => {
    let v = e.target.value;
    if (k === 'cpf') v = maskCPF(v);
    if (k === 'cnpj') v = maskCNPJ(v);
    setF((p) => ({ ...p, [k]: v }));
  };
  const valido = f.nome.trim().length > 4 && soDigitos(f.cpf).length === 11 &&
                 f.ponto.trim().length > 1 && consent;
  const comecar = async () => {
    setErro('');
    if (!valido) { setErro('Preencha nome, CPF (11 dígitos), ponto de coleta e aceite o registro dos dados.'); return; }
    setEnviando(true);
    try {
      const id = await supaRpc('iniciar_registro', {
        p_nome: f.nome.trim(), p_cpf: soDigitos(f.cpf), p_rg: f.rg.trim(),
        p_ponto: f.ponto.trim(), p_cnpj: soDigitos(f.cnpj),
      });
      const reg = { id, nome: f.nome.trim(), cpf: soDigitos(f.cpf), ponto: f.ponto.trim(), inicio: new Date().toISOString() };
      saveReg(reg);
      onDone(reg);
    } catch (e) {
      setEnviando(false);
      setErro('Não foi possível registrar agora. Verifique a conexão e tente de novo.');
    }
  };
  return (
    <div>
      <p style={{ margin: '0 0 18px', fontSize: 14.5, color: LMUTED, lineHeight: 1.5 }}>
        Preencha seus dados para começar — é esse registro que valida a sua conclusão do treinamento.</p>
      <HomeField label="Nome completo" value={f.nome} onChange={set('nome')} placeholder="Seu nome completo" />
      <HomeField label="CPF" value={f.cpf} onChange={set('cpf')} placeholder="000.000.000-00" inputMode="numeric" />
      <HomeField label="RG" value={f.rg} onChange={set('rg')} placeholder="Número do RG" opcional />
      <HomeField label="Ponto de coleta" value={f.ponto} onChange={set('ponto')} placeholder="Nome do posto / laboratório" />
      <HomeField label="CNPJ do posto" value={f.cnpj} onChange={set('cnpj')} placeholder="00.000.000/0000-00" inputMode="numeric" opcional />
      <label style={{ display: 'flex', gap: 10, alignItems: 'flex-start', margin: '2px 0 16px', cursor: 'pointer' }}>
        <input type="checkbox" checked={consent} onChange={(e) => setConsent(e.target.checked)}
          style={{ marginTop: 3, width: 17, height: 17, accentColor: CTX_BLUE, flexShrink: 0 }} />
        <span style={{ fontSize: 12.8, color: LMUTED, lineHeight: 1.5 }}>
          Autorizo o registro destes dados pela Synvia para controle e validação da conclusão deste treinamento.</span>
      </label>
      {erro && <div style={{ background: '#fde8e6', border: '1px solid #f3b4ad', color: '#a3271c',
        borderRadius: 10, padding: '10px 13px', fontSize: 13.5, marginBottom: 14 }}>{erro}</div>}
      <button onClick={comecar} disabled={enviando}
        style={{ all: 'unset', cursor: enviando ? 'wait' : 'pointer', display: 'block', textAlign: 'center',
          boxSizing: 'border-box', width: '100%', background: valido ? CTX_GREEN_LIME : '#e7e9e4',
          color: valido ? CTX_BLUE : '#9aa8a2', borderRadius: 999, padding: '15px 0', fontSize: 16.5,
          fontWeight: 800, transition: 'all 160ms' }}>
        {enviando ? 'Registrando…' : 'Começar o curso →'}
      </button>
    </div>
  );
}

function ValidarCert() {
  const [cod, setCod] = React.useState('');
  const [estado, setEstado] = React.useState('idle'); // idle | busca | ok | vazio | erro
  const [res, setRes] = React.useState(null);
  const fmt = (s) => { if (!s) return '—'; try { return new Date(s).toLocaleDateString('pt-BR',
    { day: '2-digit', month: 'long', year: 'numeric' }); } catch { return '—'; } };
  const validar = async () => {
    const c = cod.trim().toUpperCase(); if (!c) return;
    setEstado('busca'); setRes(null);
    try {
      const rows = await supaRpc('validar_certificado', { p_codigo: c });
      if (rows && rows.length) { setRes(rows[0]); setEstado('ok'); } else setEstado('vazio');
    } catch (e) { setEstado('erro'); }
  };
  const Mini = ({ label, children }) => (
    <div><div style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.12em', color: LMUTED }}>{label}</div>
      <div style={{ fontSize: 15, fontWeight: 700, color: CTX_BLUE, marginTop: 2 }}>{children}</div></div>
  );
  return (
    <div>
      <p style={{ margin: '0 0 16px', fontSize: 14.5, color: LMUTED, lineHeight: 1.5 }}>
        Digite o código do certificado (ex.: <b>CTOX-AB12CD</b>) para conferir a autenticidade e baixar o documento de novo.</p>
      <div style={{ display: 'flex', gap: 10, marginBottom: 16 }}>
        <input value={cod} onChange={(e) => { setCod(e.target.value.toUpperCase()); setEstado('idle'); }}
          onKeyDown={(e) => { if (e.key === 'Enter') validar(); }} placeholder="CTOX-XXXXXX"
          style={{ flex: 1, minWidth: 0, boxSizing: 'border-box', padding: '12px 14px', fontSize: 15.5,
            letterSpacing: '0.04em', borderRadius: 11, border: `1.5px solid ${LBORD}`, background: '#fff',
            color: LINK, outline: 'none', fontWeight: 700 }} />
        <button onClick={validar} disabled={estado === 'busca' || !cod.trim()}
          style={{ all: 'unset', cursor: 'pointer', background: CTX_BLUE, color: '#fff', borderRadius: 11,
            padding: '0 20px', display: 'flex', alignItems: 'center', fontWeight: 800, fontSize: 15,
            opacity: (estado === 'busca' || !cod.trim()) ? 0.5 : 1 }}>
          {estado === 'busca' ? '…' : 'Validar'}</button>
      </div>
      {estado === 'vazio' && <div style={{ background: '#fdf1e7', border: '1px solid #f0cda6', color: '#8a5a1c',
        borderRadius: 12, padding: '14px 16px', fontSize: 14, lineHeight: 1.5 }}>
        Nenhum certificado encontrado com esse código. Confira se digitou exatamente como está no documento.</div>}
      {estado === 'erro' && <div style={{ background: '#fde8e6', border: '1px solid #f3b4ad', color: '#a3271c',
        borderRadius: 12, padding: '14px 16px', fontSize: 14 }}>
        Não foi possível validar agora. Tente de novo em instantes.</div>}
      {estado === 'ok' && res && (
        <div style={{ border: `1px solid ${LBORD}`, borderRadius: 16, overflow: 'hidden', background: '#fff' }}>
          <div style={{ background: '#e4f5d0', padding: '12px 16px', display: 'flex', alignItems: 'center',
            gap: 8, fontWeight: 800, color: '#3a6b16', fontSize: 14 }}>
            <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2.6"
              strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
            Certificado autêntico</div>
          <div style={{ padding: '16px 18px' }}>
            <div style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.12em', color: LMUTED }}>TITULAR</div>
            <div style={{ fontSize: 19, fontWeight: 800, color: CTX_BLUE, margin: '2px 0' }}>{res.nome}</div>
            <div style={{ fontSize: 13.5, color: LMUTED }}>CPF {res.cpf_mascara}</div>
            <div style={{ display: 'flex', gap: 22, flexWrap: 'wrap', margin: '14px 0 2px' }}>
              <Mini label="CONCLUÍDO EM">{fmt(res.fim_em)}</Mini>
              <Mini label="CÓDIGO">{res.codigo}</Mini>
            </div>
            <div style={{ marginTop: 12, fontSize: 13, fontWeight: 700, color: res.validado ? '#3a6b16' : '#8a5a1c' }}>
              {res.validado ? '✓ Validado pela equipe Synvia' : '⏳ Aguardando validação da equipe Synvia'}</div>
            <a href={'certificado.html?c=' + encodeURIComponent(res.codigo)} target="_blank" rel="noopener"
              style={{ all: 'unset', display: 'block', textAlign: 'center', marginTop: 16, cursor: 'pointer',
                background: CTX_GREEN_LIME, color: CTX_BLUE, borderRadius: 999, padding: '13px 0',
                fontWeight: 800, fontSize: 15 }}>
              Abrir / baixar certificado (PDF) →</a>
          </div>
        </div>
      )}
    </div>
  );
}

function AdmLogin() {
  const [em, setEm] = React.useState('');
  const [pw, setPw] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const entrar = async () => {
    if (!em.trim() || !pw || busy) return;
    setBusy(true); setErr('');
    try {
      const r = await fetch(SUPABASE_URL + '/auth/v1/token?grant_type=password', {
        method: 'POST', headers: { apikey: SUPABASE_ANON, 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: em.trim(), password: pw }) });
      if (!r.ok) throw new Error('login');
      const j = await r.json();
      try { sessionStorage.setItem('ctox-adm-token', j.access_token); sessionStorage.setItem('ctox-adm-email', em.trim()); } catch {}
      location.href = 'adm.html';
    } catch (e) { setBusy(false); setErr('E-mail ou senha inválidos.'); }
  };
  return (
    <div>
      <p style={{ margin: '0 0 16px', fontSize: 14.5, color: LMUTED, lineHeight: 1.5 }}>
        Acesso restrito à equipe Synvia. Entre para conferir e validar os registros e baixar a planilha completa.</p>
      <HomeField label="E-mail" value={em} onChange={(e) => { setEm(e.target.value); setErr(''); }}
        placeholder="voce@synvia.com" inputMode="email" type="email" />
      <label style={{ display: 'block', marginBottom: 14 }}>
        <span style={{ display: 'block', fontSize: 12.5, fontWeight: 700, color: LMUTED, marginBottom: 6 }}>Senha</span>
        <input value={pw} onChange={(e) => { setPw(e.target.value); setErr(''); }} type="password" placeholder="••••••••"
          onKeyDown={(e) => { if (e.key === 'Enter') entrar(); }}
          style={{ width: '100%', boxSizing: 'border-box', padding: '12px 14px', fontSize: 15.5, borderRadius: 11,
            border: `1.5px solid ${LBORD}`, background: '#fff', color: LINK, outline: 'none' }} />
      </label>
      {err && <div style={{ background: '#fde8e6', border: '1px solid #f3b4ad', color: '#a3271c',
        borderRadius: 10, padding: '10px 13px', fontSize: 13.5, marginBottom: 14 }}>{err}</div>}
      <button onClick={entrar} disabled={busy}
        style={{ all: 'unset', cursor: busy ? 'wait' : 'pointer', display: 'block', textAlign: 'center',
          width: '100%', boxSizing: 'border-box', background: CTX_BLUE, color: '#fff', borderRadius: 999,
          padding: '15px 0', fontSize: 16, fontWeight: 800, opacity: busy ? 0.7 : 1 }}>
        {busy ? 'Entrando…' : 'Entrar no painel →'}</button>
    </div>
  );
}

function RegistroHome({ onDone }) {
  const [tab, setTab] = React.useState('curso');   // curso | validar | adm
  const [narrow, setNarrow] = React.useState(() => (typeof window !== 'undefined' ? window.innerWidth <= 920 : false));
  React.useEffect(() => {
    const mq = window.matchMedia('(max-width: 920px)');
    const on = () => setNarrow(mq.matches); on();
    mq.addEventListener ? mq.addEventListener('change', on) : mq.addListener(on);
    return () => { mq.removeEventListener ? mq.removeEventListener('change', on) : mq.removeListener(on); };
  }, []);

  const TABS = [
    { id: 'curso', label: 'Fazer o curso' },
    { id: 'validar', label: 'Validar certificado' },
    { id: 'adm', label: 'Administrativo' },
  ];
  const titulo = tab === 'curso' ? 'Comece seu cadastro' : tab === 'validar' ? 'Validar certificado' : 'Área administrativa';

  const hero = (
    <div style={{ position: 'relative', ...(narrow ? { minHeight: '40vh' } : { height: '100%' }),
      display: 'flex', flexDirection: 'column', justifyContent: 'flex-end',
      backgroundImage: 'url(assets/home-predio.jpg)', backgroundSize: 'cover', backgroundPosition: 'center' }}>
      <div style={{ position: 'absolute', inset: 0,
        background: 'linear-gradient(165deg, rgba(10,30,24,0.35) 8%, rgba(10,30,24,0.9) 92%)' }} />
      <div style={{ position: 'relative', padding: narrow ? '30px 24px 26px' : 'clamp(40px,5vw,72px)', maxWidth: 640 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 11, marginBottom: 16 }}>
          <SynviaLogo size={28} color="#fff" />
          <span style={{ fontSize: 12.5, letterSpacing: '0.26em', fontWeight: 800, color: CTX_GREEN_LIME }}>TREINAMENTO CTOX · SYNVIA</span>
        </div>
        <h1 style={{ fontSize: narrow ? 'clamp(28px,8vw,40px)' : 'clamp(34px,3.4vw,52px)', fontWeight: 800,
          letterSpacing: '-0.03em', lineHeight: 1.05, margin: 0, color: '#fff' }}>
          Bem-vindo ao curso<br />de Introdução ao CTox</h1>
        <p style={{ fontSize: narrow ? 16 : 'clamp(16px,1.3vw,19px)', color: 'rgba(255,255,255,0.82)',
          maxWidth: 520, marginTop: 15, marginBottom: 0, lineHeight: 1.55 }}>
          Para os postos de coleta parceiros (PCLs). Faça seu cadastro para começar, valide um certificado ou acesse o painel da equipe.</p>
        {!narrow && (
          <ul style={{ margin: '22px 0 0', padding: 0, listStyle: 'none', display: 'grid', gap: 10, maxWidth: 500 }}>
            {['Cadastro rápido que registra início e conclusão',
              'Certificado com código verificável por qualquer pessoa',
              'Painel da Synvia para conferir e validar os registros'].map((t, i) => (
              <li key={i} style={{ display: 'flex', gap: 10, alignItems: 'flex-start', fontSize: 14.5, color: 'rgba(255,255,255,0.8)' }}>
                <span style={{ color: CTX_GREEN_LIME, marginTop: 1, flexShrink: 0 }}>
                  <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" strokeWidth="2.6"
                    strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>{t}</li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );

  const panel = (
    <div style={{ background: '#F7F5EF', ...(narrow ? {} : { height: '100%', overflowY: 'auto' }),
      display: 'flex', flexDirection: 'column' }}>
      <div style={{ padding: narrow ? '22px 20px 44px' : 'clamp(34px,4vw,54px) clamp(28px,3.5vw,54px)',
        margin: narrow ? 0 : 'auto 0', width: '100%', boxSizing: 'border-box', maxWidth: 540 }}>
        <div style={{ display: 'flex', gap: 4, background: '#eceae2', borderRadius: 12, padding: 4, marginBottom: 24 }}>
          {TABS.map((t) => (
            <button key={t.id} onClick={() => setTab(t.id)}
              style={{ all: 'unset', flex: 1, textAlign: 'center', cursor: 'pointer', padding: '9px 6px',
                borderRadius: 9, fontSize: 12.6, fontWeight: 800, lineHeight: 1.2,
                color: tab === t.id ? CTX_BLUE : '#7c8a84', background: tab === t.id ? '#fff' : 'transparent',
                boxShadow: tab === t.id ? '0 1px 4px rgba(0,0,0,0.08)' : 'none', transition: 'all 140ms' }}>
              {t.label}</button>
          ))}
        </div>
        <h2 style={{ fontSize: 23, fontWeight: 800, color: CTX_BLUE, margin: '0 0 16px' }}>{titulo}</h2>
        {tab === 'curso' && <CadastroForm onDone={onDone} />}
        {tab === 'validar' && <ValidarCert />}
        {tab === 'adm' && <AdmLogin />}
      </div>
    </div>
  );

  if (narrow) {
    return (
      <div style={{ position: 'absolute', inset: 0, overflowY: 'auto', background: '#F7F5EF',
        fontFamily: 'Manrope, system-ui, sans-serif' }}>
        {hero}{panel}
      </div>
    );
  }
  return (
    <div style={{ position: 'absolute', inset: 0, display: 'grid', gridTemplateColumns: '1fr min(46%, 560px)',
      fontFamily: 'Manrope, system-ui, sans-serif', background: '#F7F5EF' }}>
      {hero}{panel}
    </div>
  );
}


function CourseApp() {
  const [reg, setReg] = React.useState(readReg);
  const [done, setDone] = React.useState(readDone);
  const [currentN, setCurrentN] = React.useState(() => firstToWatch(readDone()));
  const [ended, setEnded] = React.useState(false);
  const [justDone, setJustDone] = React.useState(null);
  const [admOk, setAdmOk] = React.useState(readAdmOk);
  const [showAdm, setShowAdm] = React.useState(false);
  const [narrow, setNarrow] = React.useState(() => (typeof window !== 'undefined' ? window.innerWidth <= 860 : false));

  React.useEffect(() => {
    const mq = window.matchMedia('(max-width: 860px)');
    const on = () => setNarrow(mq.matches); on();
    mq.addEventListener ? mq.addEventListener('change', on) : mq.addListener(on);
    return () => { mq.removeEventListener ? mq.removeEventListener('change', on) : mq.removeListener(on); };
  }, []);

  React.useEffect(() => {
    const refresh = () => setDone(readDone());
    window.addEventListener('pageshow', refresh);
    document.addEventListener('visibilitychange', refresh);
    return () => { window.removeEventListener('pageshow', refresh); document.removeEventListener('visibilitychange', refresh); };
  }, []);

  React.useEffect(() => {
    const m = (location.hash || '').match(/play=(\d{2})/);
    if (m) { history.replaceState(null, '', location.pathname + location.search);
      if (byNum(m[1]) && canAccess(m[1], readDone(), readAdmOk())) { setCurrentN(m[1]); setEnded(false); } }
  }, []);

  const select = (n) => {
    if (!isUnlocked(n, done)) return;
    if (n === ADM_MOD && !admOk) { setShowAdm(true); return; }   // Financeiro: pede senha
    setCurrentN(n); setEnded(false);
    const c = document.getElementById('centercol'); if (c) c.scrollTo(0, 0);
  };
  const liberarAdm = () => { try { localStorage.setItem(ADM_OK_KEY, '1'); } catch {}
    setAdmOk(true); setShowAdm(false);
    if (isUnlocked(ADM_MOD, done)) { setCurrentN(ADM_MOD); setEnded(false);
      const c = document.getElementById('centercol'); if (c) c.scrollTo(0, 0); } };

  const onVideoEnded = () => {
    setEnded(true);
    setDone((prev) => {
      if (prev.includes(currentN)) return prev;
      const nd = [...prev, currentN];
      try { localStorage.setItem(STORE_KEY, JSON.stringify(nd)); } catch {}
      return nd;
    });
    setJustDone(currentN);
    setTimeout(() => setJustDone(null), 1400);
  };
  const goNext = () => { const nn = nextOf(currentN); if (nn) select(nn); };

  if (!reg) return <RegistroHome onDone={setReg} />;

  const mod = byNum(currentN) || byNum(ORDER[0]);
  const tudoConcluido = commonDone(done);

  if (narrow) {
    return (
      <div style={{ minHeight: '100%', background: LBG, color: LINK, display: 'flex', flexDirection: 'column',
        fontFamily: 'Manrope, system-ui, sans-serif' }}>
        <div style={{ position: 'sticky', top: 0, zIndex: 20, display: 'flex', alignItems: 'center', gap: 10,
          padding: '12px 16px', background: CTX_BLUE, color: '#fff' }}>
          <SynviaLogo size={22} color="#fff" />
          <b style={{ fontSize: 15 }}>Curso CTox</b>
          <span style={{ flex: 1 }} />
          <a href={tudoConcluido ? 'teste.html' : undefined}
            style={{ textDecoration: 'none', fontSize: 13, fontWeight: 800,
              background: tudoConcluido ? CTX_GREEN_LIME : 'rgba(255,255,255,0.12)',
              color: tudoConcluido ? CTX_BLUE : 'rgba(255,255,255,0.5)',
              borderRadius: 999, padding: '8px 16px' }}>
            {tudoConcluido ? 'Fazer o teste' : 'Teste (bloqueado)'}
          </a>
        </div>
        <div id="centercol" style={{ width: '100%' }}>
          <CenterModule mod={mod} done={done} ended={ended} tudoConcluido={tudoConcluido}
            onEnded={onVideoEnded} onError={() => {}} onNext={goNext} onBack={() => setEnded(false)} narrow />
        </div>
        <Progresso done={done} currentN={currentN} justDone={justDone} onSelect={select} admOk={admOk} narrow />
        {showAdm && <AdmModal onClose={() => setShowAdm(false)} onSuccess={liberarAdm} />}
      </div>
    );
  }

  return (
    <div style={{ position: 'absolute', inset: 0, background: LBG, color: LINK,
      display: 'grid', gridTemplateColumns: '248px 1fr 372px', fontFamily: 'Manrope, system-ui, sans-serif' }}>
      <LeftRail tudoConcluido={tudoConcluido} onOverview={() => select(firstToWatch(done))} />
      <div id="centercol" style={{ overflow: 'auto' }}>
        <CenterModule mod={mod} done={done} ended={ended} tudoConcluido={tudoConcluido}
          onEnded={onVideoEnded} onError={() => {}} onNext={goNext} onBack={() => setEnded(false)} />
      </div>
      <Progresso done={done} currentN={currentN} justDone={justDone} onSelect={select} admOk={admOk} />
      {showAdm && <AdmModal onClose={() => setShowAdm(false)} onSuccess={liberarAdm} />}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<CourseApp />);
