/* ============================================================
   EDUCARE — GAN · Geração Aleatória de Números (6 a 12 anos)
   Funções executivas (memória de trabalho e inibição) via
   evocação aleatória de números ao ritmo de estímulos sonoros.
   Padrões: instância por ID, salvamento explícito, guarda de
   saída, histórico por criança, tela limpa ao finalizar.
   Obs.: normas (Z/percentis) virão numa etapa seguinte — por
   ora, apenas escores brutos.
   ============================================================ */
(function () {
  const { useState, useRef, useEffect } = React;
  const Icon = window.Icon;
  const INSTRUMENT_NAME = "GAN — Geração Aleatória de Números (6 a 12 anos)";

  // ---------------- Persistência por INSTÂNCIA (ID único) ----------------
  const GAN_KEY = "educare-gan-avaliacoes";
  function loadAvals() { try { const o = JSON.parse(localStorage.getItem(GAN_KEY) || "null"); if (o && typeof o === "object") return o; } catch (e) { /* */ } return {}; }
  function saveAvals(o) { localStorage.setItem(GAN_KEY, JSON.stringify(o)); }
  function loadAval(id) { return loadAvals()[id] || null; }
  function putAval(a) { const all = loadAvals(); all[a.id] = a; saveAvals(all); }
  function newAvalId() { return "gan_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 8); }
  function avalsDoPaciente(pid) { const all = loadAvals(); return Object.keys(all).map((k) => all[k]).filter((a) => a && a.pid === pid).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); }

  const SI = { width: 32, height: 32, borderRadius: 10, background: "var(--primary-soft)", color: "var(--primary)", display: "grid", placeItems: "center", flex: "none" };
  const fmtTempo = (s) => { s = Math.max(0, Math.floor(s || 0)); const m = Math.floor(s / 60); const ss = s % 60; return m + ":" + (ss < 10 ? "0" : "") + ss; };

  // ---------------- Config das partes ----------------
  const CFG = {
    a: { titulo: "Parte A — velocidade lenta (2 s)", treinoDur: 10, tarefaDur: 90, intervalo: 2, estimulos: 45 },
    b: { titulo: "Parte B — velocidade rápida (1 s)", treinoDur: 10, tarefaDur: 90, intervalo: 1, estimulos: 90 },
  };
  const PARTES = [
    { key: "a", n: 1, next: "b", titulo: CFG.a.titulo, desc: "Um número a cada 2 segundos." },
    { key: "b", n: 2, next: null, titulo: CFG.b.titulo, desc: "Um número a cada 1 segundo." },
  ];
  const STATUS = { nao: { l: "Não iniciada", tag: "muted" }, andamento: { l: "Em andamento", tag: "warn" }, concluida: { l: "Concluída", tag: "ok" } };

  const GERAIS = [
    "Ambiente climatizado, iluminado e silencioso.",
    "Examinador sentado de frente para a criança, evitando que o protocolo fique no campo de visão dela.",
    "Tempo de aplicação de aproximadamente 5 minutos. Não há critério de interrupção.",
    "Se a criança se confundir durante a evocação, dizer: “Não tem problema, apenas encontre o ritmo novamente.”",
  ];
  const INSTR = {
    a: "Vamos jogar um jogo com números. Você vai fingir, imaginar, que joga um dado (mostrar um dado) e me dizer qual o número que apareceu depois que jogou o dado. O dado que você vai imaginar tem números de 1 a 10. Você vai ouvir sons como este aqui (mostrar o estímulo sonoro), e deve dizer um número para cada som, ou seja, você deve dizer os números do dado que você está jogando na sua mente na mesma velocidade em que os sons vão aparecendo. Assim, cada vez que você ouvir um som, tem que dizer qual o novo número que apareceu no dado que você imaginou jogar. O que você deve tentar é dizer os números misturados, em qualquer ordem, sem formar sequências conhecidas como 1, 2, 3, 4 ou 9, 8, 7, 6. Você não pode dizer o número 2, depois o 3 e depois o 4, pois isso não seria parecido com os números que aparecem quando o dado é jogado. Você pode usar todos os números entre 1 e 10, procurando não repetir números um perto do outro. Por exemplo, você pode dizer o número 4, depois o número 1, depois o número 6, depois o número 3, e assim por diante. Primeiro vamos fazer essa tarefa em uma velocidade mais lenta (um número novo a cada dois segundos) e depois, mais rápida (um número a cada um segundo). Vamos treinar?",
    aOk: "Ok, é isso mesmo! Você deve dizer os números até o som terminar. Não pare, mesmo que você erre ou se atrapalhe com os números. Podemos começar então?",
    b: "Agora faremos a mesma tarefa, mas em uma velocidade mais rápida. Vamos treinar?",
    bOk: "Ok, é isso mesmo! Podemos começar então?",
  };

  // ---------------- Classificação (tipos + cores) ----------------
  const TIPOS = {
    acerto: { l: "Acerto", col: "#16A34A" },
    intrusao: { l: "Intrusão", col: "#DC2626" },
    omissao: { l: "Omissão", col: "#94A3B8" },
    perseveracao: { l: "Perseveração", col: "#F59E0B" },
    seq_direta: { l: "Seq. direta", col: "#7C3AED" },
    seq_indireta: { l: "Seq. indireta", col: "#2563EB" },
    par: { l: "Par", col: "#0891B2" },
    impar: { l: "Ímpar", col: "#DB2777" },
    pronta: { l: "Pronta/repetida", col: "#EA580C" },
  };
  const TIPO_KEYS = ["acerto", "intrusao", "omissao", "perseveracao", "seq_direta", "seq_indireta", "par", "impar", "pronta"];

  // Classificação automática (heurística — o examinador pode reclassificar).
  function autoClassify(cells, estimulos) {
    const n = Math.min(cells.length, estimulos);
    const a = cells.slice(0, n);
    const tipos = new Array(n).fill(null);
    const isN = (x) => typeof x === "number";
    const val = (x) => isN(x) && x >= 1 && x <= 10;
    for (let i = 0; i < n; i++) {
      const v = a[i];
      if (v === "om") tipos[i] = "omissao";
      else if (v === null || v === undefined) tipos[i] = null;
      else if (isN(v) && !val(v)) tipos[i] = "intrusao";
    }
    function runs(step, tipo, minLen) {
      let i = 0;
      while (i < n) {
        if (val(a[i])) {
          let j = i;
          while (j + 1 < n && val(a[j + 1]) && a[j + 1] - a[j] === step) j++;
          if (j - i + 1 >= minLen) { for (let k = i; k <= j; k++) if (!tipos[k]) tipos[k] = tipo; i = j + 1; continue; }
        }
        i++;
      }
    }
    runs(1, "seq_direta", 2);
    runs(-1, "seq_indireta", 2);
    function runs2(parity, tipo) {
      let i = 0;
      while (i < n) {
        if (val(a[i]) && a[i] % 2 === parity) {
          let j = i;
          while (j + 1 < n && val(a[j + 1]) && a[j + 1] % 2 === parity && a[j + 1] - a[j] === 2) j++;
          if (j - i + 1 >= 3) { for (let k = i; k <= j; k++) if (!tipos[k]) tipos[k] = tipo; i = j + 1; continue; }
        }
        i++;
      }
    }
    runs2(0, "par"); runs2(1, "impar");
    const trip = {};
    for (let i = 0; i + 2 < n; i++) { if (val(a[i]) && val(a[i + 1]) && val(a[i + 2])) { const key = a[i] + "-" + a[i + 1] + "-" + a[i + 2]; (trip[key] = trip[key] || []).push(i); } }
    Object.keys(trip).forEach((k) => { if (trip[k].length >= 2) trip[k].forEach((i) => { for (let x = i; x < i + 3; x++) if (!tipos[x]) tipos[x] = "pronta"; }); });
    for (let i = 0; i < n; i++) {
      if (tipos[i] || !val(a[i])) continue;
      for (let d = 1; d <= 4 && i - d >= 0; d++) { if (val(a[i - d]) && a[i - d] === a[i]) { tipos[i] = "perseveracao"; break; } }
    }
    for (let i = 0; i < n; i++) { if (!tipos[i] && val(a[i])) tipos[i] = "acerto"; }
    return tipos;
  }
  function contar(eff) {
    const c = { acerto: 0, intrusao: 0, omissao: 0, perseveracao: 0, par: 0, impar: 0, pronta: 0, sdErr: 0, sdSeq: 0, siErr: 0, siSeq: 0, parSeq: 0, imparSeq: 0, prontaSeq: 0 };
    // Cada posição dentro do total de estímulos recebe exatamente UMA classificação.
    // Posição em branco (null/undefined) = criança não respondeu ao estímulo = OMISSÃO.
    eff.forEach((t) => { if (t === "acerto") c.acerto++; else if (t === "intrusao") c.intrusao++; else if (t === "omissao" || t == null) c.omissao++; else if (t === "perseveracao") c.perseveracao++; else if (t === "par") c.par++; else if (t === "impar") c.impar++; else if (t === "pronta") c.pronta++; else if (t === "seq_direta") c.sdErr++; else if (t === "seq_indireta") c.siErr++; });
    const runs = (tipo) => { let i = 0, n = 0; while (i < eff.length) { if (eff[i] === tipo) { n++; while (i < eff.length && eff[i] === tipo) i++; } else i++; } return n; };
    c.sdSeq = runs("seq_direta"); c.siSeq = runs("seq_indireta");
    c.parSeq = runs("par"); c.imparSeq = runs("impar"); c.prontaSeq = runs("pronta");
    return c;
  }
  // Coerência: a soma de todas as classificações deve fechar com o total de estímulos.
  function coerenciaGAN(c, est) {
    const soma = c.acerto + c.intrusao + c.omissao + c.perseveracao + c.sdErr + c.siErr + c.par + c.impar + c.pronta;
    return { soma, est, ok: soma === est };
  }
  // Quantos quadrados foram efetivamente registrados (número, intrusão ou omissão explícita).
  function preenchidosGAN(cells, est) { let n = 0; for (let i = 0; i < est; i++) { const v = cells[i]; if (v !== null && v !== undefined && v !== "") n++; } return n; }

  // ---------------- Normas (Tabelas 2–15) — percentis por idade × escola × parte ----------------
  // Colunas: idade;escola;vel(2s=A/1s=B);medida;Média;DP;P95;P75;P50;P25;P5
  const NORMAS_RAW = `6;privada;2s;AC;26,41;10,03;42;35;27;23;-
6;privada;2s;IN;4,41;8,39;-;-;1;6;33
6;privada;2s;OM;5,82;4,53;-;3;4;9;15
6;privada;2s;PE;5,59;3,12;-;3;5;8;11
6;privada;2s;SD;3,59;10,31;-;-;-;3;43
6;privada;2s;SI;1,24;2,13;-;-;-;3;6
6;privada;1s;AC;29,06;16,47;72;34;32;22;-
6;privada;1s;IN;6,12;18,90;-;-;1;3;79
6;privada;1s;OM;34,53;19,87;-;20;36;49;64
6;privada;1s;PE;9,35;7,99;-;3;8;15;27
6;privada;1s;SD;8,65;21,14;-;-;3;8;89
6;privada;1s;SI;2,88;3,44;-;-;3;4;13
6;publica;2s;AC;23,36;7,76;34;30;24;19;7
6;publica;2s;IN;3,88;6,77;-;-;1;4;24
6;publica;2s;OM;9,28;7,67;0,60;4;8;11;32
6;publica;2s;PE;3,80;3,41;-;2;3;5;14
6;publica;2s;SD;3,56;5,22;-;-;3;4;20
6;publica;2s;SI;1,68;2,85;-;-;-;3;9
6;publica;1s;AC;31,76;15;58;43;32;20;2
6;publica;1s;IN;7,40;15;-;-;2;6;53
6;publica;1s;OM;32,60;21;2;13;33;51;71
6;publica;1s;PE;4,48;3;-;2;4;7;13
6;publica;1s;SD;8,04;7;-;2;6;15;20
6;publica;1s;SI;3,56;6;-;-;3;6;21
7;privada;2s;AC;28,20;6,83;41;33;28;23;17
7;privada;2s;IN;0,90;1,16;-;-;-;2;3
7;privada;2s;OM;7,60;5,74;-;4;8;11;23
7;privada;2s;PE;4,60;2,98;0,05;2;5;7;11
7;privada;2s;SD;1,35;1,81;-;-;-;3;6
7;privada;2s;SI;1,95;2,79;-;-;-;3;9
7;privada;1s;AC;41,11;12,01;60;51;43;29;25
7;privada;1s;IN;1,95;1,74;-;-;2;3;6
7;privada;1s;OM;29,89;15,76;2;18;32;44;54
7;privada;1s;PE;8,21;5,41;-;4;9;13;17
7;privada;1s;SD;5,37;5,20;-;-;5;7;18
7;privada;1s;SI;4,16;4,95;-;-;3;10;15
7;publica;2s;AC;26,67;9,01;36;34;30;19;8
7;publica;2s;IN;1,24;3,56;-;-;-;1;15
7;publica;2s;OM;7,95;9,00;-;3;5;10;32
7;publica;2s;PE;4,71;3,33;-;2;4;7;12
7;publica;2s;SD;2,33;3,42;-;-;-;4;12
7;publica;2s;SI;2,38;3,57;-;-;-;4;14
7;publica;1s;AC;39,57;19,33;73,20;58;40;22;10
7;publica;1s;IN;2,19;4,20;-;-;1;2;15
7;publica;1s;OM;26,19;22,45;2,20;7;22;51;65
7;publica;1s;PE;9,10;5,74;-;5;9;14;19
7;publica;1s;SD;9,24;8,35;-;3;9;14;28
7;publica;1s;SI;5,33;8,38;-;-;3;7;31
8;privada;2s;AC;31,29;4,87;39;35;32;29;21
8;privada;2s;IN;0,54;0,99;-;-;-;1;3
8;privada;2s;OM;5,11;5,07;-;2;3;6;19
8;privada;2s;PE;4,68;2,58;1;3;5;6;10
8;privada;2s;SD;1,82;2,74;-;-;-;3;9
8;privada;2s;SI;1,96;2,63;-;-;-;3;9
8;privada;1s;AC;45,63;11,78;71;55;45;36;27
8;privada;1s;IN;1,56;2,10;-;-;1;2;8
8;privada;1s;OM;22,70;13,48;2;12;21;34;46
8;privada;1s;PE;8,59;5,07;1;5;9;12;20
8;privada;1s;SD;7,65;7,07;-;3;6;12;26
8;privada;1s;SI;5,31;6,89;-;-;3;9;23
8;publica;2s;AC;30,47;4,06;38;34;31;26;25
8;publica;2s;IN;1,05;2,22;-;-;-;1;9
8;publica;2s;OM;6,68;5,50;-;2;6;10;18
8;publica;2s;PE;4,63;3,09;-;2;4;7;10
8;publica;2s;SD;1,53;1,67;-;-;-;3;4
8;publica;2s;SI;0,84;1,74;-;-;-;-;5
8;publica;1s;AC;38,37;14,13;67;49;33;28;16
8;publica;1s;IN;1,95;3,35;-;-;-;2;11
8;publica;1s;OM;30,79;20,89;2;14;26;51;62
8;publica;1s;PE;6,95;5,15;-;3;7;10;21
8;publica;1s;SD;5,78;7,81;-;-;3;9;29
8;publica;1s;SI;6,94;10,91;-;-;3;10;45
9;privada;2s;AC;33,50;4,87;40;38;34;29;24
9;privada;2s;IN;0,71;1,08;-;-;-;2;4
9;privada;2s;OM;4,62;5,21;-;1;3;8;19
9;privada;2s;PE;3,88;2,81;-;2;3;6;10
9;privada;2s;SD;0,52;1,16;-;-;-;-;3
9;privada;2s;SI;1,32;1,91;-;-;-;3;6
9;privada;1s;AC;50,17;13,55;74;63;45;40;23
9;privada;1s;IN;2,54;3,03;-;-;1;6;9
9;privada;1s;OM;18,92;15,48;2;8;13;32;55
9;privada;1s;PE;8,50;5,76;2;4;7;11;23
9;privada;1s;SD;5,18;5,18;-;-;3;11;14
9;privada;1s;SI;5,05;4,34;-;2;4;6;15
9;publica;2s;AC;32,40;8,50;45;39;31;27;11,65
9;publica;2s;IN;0,55;1,05;-;-;-;1;3,90
9;publica;2s;OM;4,55;4,60;-;-;4;8;14,85
9;publica;2s;PE;3,85;2,68;-;2;3,50;5;10,85
9;publica;2s;SD;2,45;3,06;-;-;-;6;8
9;publica;2s;SI;1,10;2,36;-;-;-;-;6,95
9;publica;1s;AC;47,70;12,14;63;57;50;36;25
9;publica;1s;IN;1,70;1,83;-;-;1;3;6
9;publica;1s;OM;22,55;15,60;2;9;20;36;56
9;publica;1s;PE;7,15;5,64;-;3;6;10;25
9;publica;1s;SD;6,90;7,47;-;-;5;10;24
9;publica;1s;SI;4,80;5,09;-;1;3;6;18
10;privada;2s;AC;33,92;4,27;41;36;34;32;23
10;privada;2s;IN;0,42;0,98;-;-;-;0,3;4
10;privada;2s;OM;2,62;3,16;-;-;-;3;12
10;privada;2s;PE;5,31;2,58;2;4;5;6;12
10;privada;2s;SD;1,35;2,77;-;-;-;3;10
10;privada;2s;SI;1,65;2,65;-;-;-;3;8
10;privada;1s;AC;49,23;9,14;67;55;49;43;31
10;privada;1s;IN;1,08;1,23;-;-;1;1;4
10;privada;1s;OM;21,88;12,91;2;12;19;31;44
10;privada;1s;PE;8,77;4,17;2;5;9;12;17
10;privada;1s;SD;5,58;5,26;-;-;4;9;17
10;privada;1s;SI;3,77;3,97;-;-;3;6;14
10;publica;2s;AC;36,22;2,82;43;37;36;34;31
10;publica;2s;IN;1,83;7,72;-;-;-;1;29
10;publica;2s;OM;2,96;3,07;-;1;2;4;11
10;publica;2s;PE;3,75;2,15;-;1;2;4;8
10;publica;2s;SD;0,96;1,70;-;-;-;3;6
10;publica;2s;SI;0,92;1,38;-;-;-;3;3
10;publica;1s;AC;51,46;10,64;72;69;51;47;31
10;publica;1s;IN;0,96;1,45;-;-;1;1;6
10;publica;1s;OM;20,33;12,14;1;13;20;28;47
10;publica;1s;PE;6,46;4,20;1;3;6;9;18
10;publica;1s;SD;4,74;4,40;-;-;3;9;13
10;publica;1s;SI;5,09;5,11;-;-;4;7;20
11;privada;2s;AC;35,52;5,83;44;41;36;31;22
11;privada;2s;IN;0,43;1,30;-;-;-;-;5
11;privada;2s;OM;3,70;3,98;-;1;2;6;15
11;privada;2s;PE;3,48;2,42;-;2;3;4;9
11;privada;2s;SD;1,48;2,57;-;-;-;3;8
11;privada;2s;SI;0,43;0,84;-;-;-;-;4
11;privada;1s;AC;56,52;12,40;79;65;59;49;33
11;privada;1s;IN;0,39;0,58;-;-;-;1;2
11;privada;1s;OM;15,65;9,74;0,20;9;14;23;38
11;privada;1s;PE;7,43;4,88;1,20;4;7;9;22
11;privada;1s;SD;6,22;7,00;-;3;3;7;28
11;privada;1s;SI;4,17;4,00;-;-;3;9;11
11;publica;2s;AC;34,41;8,55;44;40;38;29;12
11;publica;2s;IN;0,14;0,46;-;-;-;-;2
11;publica;2s;OM;5,86;8,58;-;-;3;7;30
11;publica;2s;PE;3,27;2,05;-;2;3;5;7
11;publica;2s;SD;0,68;1,28;-;-;1;1;3
11;publica;2s;SI;0,68;1,28;-;-;-;1;3
11;publica;1s;AC;50,95;16,30;73;66,25;54;37;14
11;publica;1s;IN;1,23;2,15;-;-;-;2;8
11;publica;1s;OM;20,86;19,64;-;4;17;32;73
11;publica;1s;PE;5,82;3,86;-;3;5;8;16
11;publica;1s;SD;6,86;6,13;-;-;6;12;19
11;publica;1s;SI;5,18;4,31;-;2;4;9;13
12;privada;2s;AC;36,67;3,37;42;39;37;34;29
12;privada;2s;IN;0,07;0,25;-;-;-;-;1
12;privada;2s;OM;2,73;2,68;-;-;3;5;7
12;privada;2s;PE;3,27;2,40;-;1;3;5;9
12;privada;2s;SD;1,07;1,58;-;-;-;3;4
12;privada;2s;SI;1,27;2,86;-;-;-;-;9
12;privada;1s;AC;53,53;10,01;72;63;52;46;34
12;privada;1s;IN;0,67;0,72;-;-;1;1;2
12;privada;1s;OM;18,33;11,86;1;8;16;31;36
12;privada;1s;PE;8,20;3,12;4;6;8;9;17
12;privada;1s;SD;4,00;5,04;-;-;3;8;15
12;privada;1s;SI;6,13;8,79;-;-;6;7;36
12;publica;2s;AC;37,61;4,04;45;41;37;35;30
12;publica;2s;IN;0,22;0,42;-;-;-;-;1
12;publica;2s;OM;2,50;2,77;-;-;2;4;9
12;publica;2s;PE;3,39;2,66;-;1;3;5;9
12;publica;2s;SD;0,56;1,75;-;-;-;-;7
12;publica;2s;SI;0,83;1,38;-;-;-;3;3
12;publica;1s;AC;57,67;11,14;78;67;58;49;42
12;publica;1s;IN;0,17;0,38;-;-;-;-;1
12;publica;1s;OM;18,44;13,08;4;6;15;30;45
12;publica;1s;PE;5,78;4,47;-;3;5;10;14
12;publica;1s;SD;4,50;5,50;-;-;3;6;19
12;publica;1s;SI;4,06;5,41;-;-;2;8;17`;

  function normNum(s) { s = (s || "").trim().replace(/\(\?\)/g, "").replace(",", "."); if (s === "-" || s === "") return null; const v = parseFloat(s); return isNaN(v) ? null : v; }
  const NORMAS = (function () {
    const N = {};
    NORMAS_RAW.trim().split("\n").forEach((line) => {
      const p = line.split(";"); if (p.length < 11) return;
      const idade = p[0].trim(), escola = p[1].trim(), parte = p[2].trim() === "2s" ? "a" : "b", med = p[3].trim();
      N[idade] = N[idade] || {}; N[idade][escola] = N[idade][escola] || {}; N[idade][escola][parte] = N[idade][escola][parte] || {};
      N[idade][escola][parte][med] = { m: normNum(p[4]), dp: normNum(p[5]), p: [normNum(p[6]), normNum(p[7]), normNum(p[8]), normNum(p[9]), normNum(p[10])] };
    });
    return N;
  })();
  const MEDIDAS_ORD = ["AC", "IN", "OM", "PE", "SD", "SI"];
  const MEDIDA_L = { AC: "Acertos", IN: "Intrusões", OM: "Omissões", PE: "Perseverações", SD: "Seq. diretas (erros)", SI: "Seq. indiretas (erros)" };
  const MAIOR_MELHOR = { AC: true, IN: false, OM: false, PE: false, SD: false, SI: false };
  const FAIXA_LBL = ["≥ P95", "entre P75 e P95", "entre P50 e P75", "entre P25 e P50", "entre P5 e P25", "< P5"];
  // Direção de leitura por FAIXA de desempenho (as tabelas são orientadas por percentil de
  // DESEMPENHO: P95 = melhores; P5 = piores — vale igual para acertos e para erros). Serve para
  // o examinador não ler "≥ P95" com zero erros como se fosse ruim.
  const FAIXA_DIR = ["melhor que a média", "melhor que a média", "acima da média", "abaixo da média", "abaixo da média", "muito abaixo da média"];
  // Interpretação normativa respeitando a DIREÇÃO da medida:
  //  - Acertos: maior = melhor (pior = valor menor).
  //  - Erros (IN/OM/PE/SD/SI): maior = pior.
  // Classifica por FAIXA de percentil (sem interpolar) + escore Z. Rótulos do manual:
  // Déficit (< P5, ou Z ≤ −1,5 em acertos / Z ≥ +1,5 em erros) · Alerta (P5–P25) · Adequado.
  function interpretar(med, raw, norma) {
    if (!norma || raw == null) return null;
    const erro = !MAIOR_MELHOR[med];
    const M = norma.m, DP = norma.dp, cut = norma.p; // cut = [P95, P75, P50, P25, P5] (P95 = melhor desempenho)
    const z = (DP && DP > 0 && M != null) ? Math.round(((raw - M) / DP) * 100) / 100 : null;
    let band = 5; // 0=≥P95 … 5=<P5 (desempenho: erro alto ⟺ bruto alto ⟺ desempenho baixo)
    for (let i = 0; i < 5; i++) { const cc = cut[i] == null ? 0 : cut[i]; const meets = erro ? (raw <= cc) : (raw >= cc); if (meets) { band = i; break; } }
    const deficitZ = z != null && (erro ? z >= 1.5 : z <= -1.5);
    let cls;
    if (band === 5 || deficitZ) cls = { l: "Déficit", col: "#DC2626" };
    else if (band === 4) cls = { l: "Alerta", col: "#F59E0B" };
    else cls = { l: "Adequado", col: "#16A34A" };
    return { z, band, faixa: FAIXA_LBL[band], dir: FAIXA_DIR[band], cls };
  }

  // ---------------- Serviço ISOLADO de áudio dos estímulos (Web Audio) ----------------
  // Toda a geração de bipes vive aqui, agendada pelo RELÓGIO do AudioContext (nunca por
  // setInterval — que acumula desvio ao longo de 90 estímulos). Trocar depois pelo áudio
  // oficial da editora significa só reescrever este objeto, sem tocar no resto do fluxo.
  const GanAudioSvc = (function () {
    let ctx = null;
    function getCtx() { if (!ctx) { const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return null; try { ctx = new AC(); } catch (e) { return null; } } return ctx; }
    function isRunning() { return !!ctx && ctx.state === "running"; }
    // Deve ser chamado dentro de um gesto do usuário (política de autoplay).
    function unlock() {
      const c = getCtx(); if (!c) return Promise.resolve(false);
      if (c.state === "running") return Promise.resolve(true);
      try { return c.resume().then(() => c.state === "running").catch(() => false); } catch (e) { return Promise.resolve(false); }
    }
    // Bipe curto (~80 ms, senoidal 800 Hz) com ataque/decaimento suave (sem estalo).
    // Retorna o oscilador, para permitir interrompê-lo ao parar a faixa.
    function beepAt(when, out, opts) {
      const c = getCtx(); if (!c) return null;
      const freq = (opts && opts.freq) || 800; const durMs = (opts && opts.durMs) || 80; const peak = (opts && opts.peak != null) ? opts.peak : 0.28;
      const o = c.createOscillator(); const g = c.createGain();
      o.type = "sine"; o.frequency.setValueAtTime(freq, when);
      const dur = durMs / 1000;
      g.gain.setValueAtTime(0.0001, when);
      g.gain.exponentialRampToValueAtTime(peak, when + 0.012);       // ataque suave
      g.gain.setValueAtTime(peak, when + Math.max(0.02, dur - 0.03));
      g.gain.exponentialRampToValueAtTime(0.0001, when + dur);        // decaimento suave
      o.connect(g); g.connect(out);
      o.start(when); o.stop(when + dur + 0.03);
      return o;
    }
    // Agenda `count` bipes a cada `intervalSec`, todos no relógio do AudioContext.
    // `outs`: nós de saída (ex.: alto-falantes e/ou destino de gravação, com ganhos próprios).
    // Retorna { startTime, endTime, times:[abs], stop() } — stop() PARA de fato os osciladores
    // já agendados (não só zera o ganho), de modo que os bipes cessam ao interromper a faixa.
    function scheduleBeeps(opts) {
      const c = getCtx(); if (!c) return null;
      const outs = opts.outs || [c.destination];
      const lead = opts.lead != null ? opts.lead : 0.25;
      const master = c.createGain(); master.gain.value = 1;
      outs.forEach((n) => { if (n) master.connect(n); });
      const t0 = c.currentTime + lead;
      const times = []; const oscs = [];
      for (let k = 0; k < opts.count; k++) { const when = t0 + k * opts.intervalSec; times.push(when); const o = beepAt(when, master, opts.beep); if (o) oscs.push(o); }
      let stopped = false;
      return {
        startTime: t0, endTime: t0 + opts.count * opts.intervalSec, times, master,
        stop() {
          if (stopped) return; stopped = true;
          const now = c.currentTime;
          try { master.gain.cancelScheduledValues(now); master.gain.setValueAtTime(0.0001, now); } catch (e) { /* */ }
          // Interrompe imediatamente os osciladores — inclusive os agendados para o futuro.
          oscs.forEach((o) => { try { o.stop(now); } catch (e) { /* */ } try { o.disconnect(); } catch (e) { /* */ } });
          setTimeout(() => { try { master.disconnect(); } catch (e) { /* */ } }, 60);
        },
      };
    }
    function now() { const c = getCtx(); return c ? c.currentTime : 0; }
    return { getCtx, isRunning, unlock, scheduleBeeps, now };
  })();

  // ---------------- Player de áudio gravado ----------------
  // Suporta stamps (ms por estímulo): seekReq {idx,nonce} posiciona no trecho do quadrado
  // (correto em qualquer velocidade, pois currentTime é sempre em segundos de mídia reais);
  // onActiveIdx informa o quadrado do momento atual para destaque na grade.
  function AudioPlayer({ src, stamps, seekReq, onActiveIdx }) {
    const ref = useRef(null);
    const [playing, setPlaying] = useState(false);
    const [cur, setCur] = useState(0); const [dur, setDur] = useState(0); const [rate, setRate] = useState(1);
    const activeRef = useRef(-1);
    const reportActive = (t) => {
      if (!onActiveIdx || !stamps || !stamps.length) return;
      let idx = -1; const ms = t * 1000;
      for (let i = 0; i < stamps.length; i++) { if (stamps[i] != null && stamps[i] <= ms + 60) idx = i; else break; }
      if (idx !== activeRef.current) { activeRef.current = idx; onActiveIdx(idx); }
    };
    useEffect(() => {
      const a = ref.current; if (!a) return; let fixing = false;
      const setD = () => { if (isFinite(a.duration) && a.duration > 0) setDur(a.duration); };
      const onLoaded = () => { if (!isFinite(a.duration) || a.duration === 0) { fixing = true; try { a.currentTime = 1e101; } catch (e) { /* */ } } else setD(); };
      const onTime = () => { if (!fixing) { setCur(a.currentTime || 0); reportActive(a.currentTime || 0); } };
      const onDur = () => { if (fixing && isFinite(a.duration)) { fixing = false; setD(); try { a.currentTime = 0; } catch (e) { /* */ } setCur(0); } };
      const onEnd = () => { setPlaying(false); activeRef.current = -1; if (onActiveIdx) onActiveIdx(-1); };
      a.addEventListener("loadedmetadata", onLoaded); a.addEventListener("durationchange", onDur); a.addEventListener("timeupdate", onTime); a.addEventListener("ended", onEnd);
      if (a.readyState >= 1) onLoaded();
      return () => { a.removeEventListener("loadedmetadata", onLoaded); a.removeEventListener("durationchange", onDur); a.removeEventListener("timeupdate", onTime); a.removeEventListener("ended", onEnd); };
    }, [src]);
    // Reposicionamento por quadrado (correção): pula para o timestamp e toca.
    useEffect(() => {
      if (!seekReq || !stamps) return;
      const a = ref.current; if (!a) return;
      const ms = stamps[seekReq.idx];
      if (ms == null) return;
      try { a.currentTime = ms / 1000; setCur(ms / 1000); reportActive(ms / 1000); a.play(); setPlaying(true); } catch (e) { /* */ }
    }, [seekReq && seekReq.nonce]);
    const toggle = () => { const a = ref.current; if (!a) return; if (a.paused) { a.play(); setPlaying(true); } else { a.pause(); setPlaying(false); } };
    const seek = (e) => { const a = ref.current; if (!a) return; const t = parseFloat(e.target.value); a.currentTime = t; setCur(t); reportActive(t); };
    const setSpeed = (r) => { const a = ref.current; if (a) a.playbackRate = r; setRate(r); };
    return (
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 12px", borderRadius: 10, background: "var(--surface-2)", border: "1px solid var(--border)", marginBottom: 10, flexWrap: "wrap" }}>
        <audio ref={ref} src={src} preload="metadata" />
        <button type="button" onClick={toggle} style={{ width: 34, height: 34, flex: "none", borderRadius: "50%", border: "none", cursor: "pointer", background: "var(--primary)", color: "#fff", fontSize: 14 }}>{playing ? "⏸" : "▶"}</button>
        <input type="range" min={0} max={dur || 0} step="0.01" value={cur > (dur || 0) ? (dur || 0) : cur} onChange={seek} style={{ flex: 1, minWidth: 120, accentColor: "var(--primary)", cursor: "pointer" }} />
        <span style={{ fontSize: 12, fontWeight: 700, color: "var(--text-2)", flex: "none" }}>{fmtTempo(cur)} / {fmtTempo(dur)}</span>
        <div style={{ display: "flex", gap: 4, flex: "none" }}>{[1, 0.75, 0.5].map((r) => (<button key={r} type="button" onClick={() => setSpeed(r)} style={{ fontSize: 11.5, fontWeight: 800, padding: "3px 7px", borderRadius: 7, cursor: "pointer", border: "1px solid " + (rate === r ? "var(--primary)" : "var(--border-2)"), background: rate === r ? "var(--primary)" : "transparent", color: rate === r ? "#fff" : "var(--text-2)" }}>{r}x</button>))}</div>
      </div>
    );
  }

  // ---------------- Faixa de TREINO (bipes só nos alto-falantes, sem gravar) ----------------
  function TreinoBipes({ durationSec, intervalSec, label }) {
    const total = Math.max(1, Math.floor(durationSec / intervalSec));
    const [fase, setFase] = useState("idle"); // idle | playing | blocked
    const [idx, setIdx] = useState(0);
    const rafRef = useRef(null); const schedRef = useRef(null);
    function stop() { if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } if (schedRef.current) { try { schedRef.current.stop(); } catch (e) { /* */ } schedRef.current = null; } setFase("idle"); setIdx(0); }
    function loop() {
      const s = schedRef.current; if (!s) return;
      const now = GanAudioSvc.now();
      let n = 0; for (let i = 0; i < s.times.length; i++) { if (s.times[i] <= now) n++; else break; }
      setIdx(n);
      if (now >= s.endTime + 0.05) { stop(); return; }
      rafRef.current = requestAnimationFrame(loop);
    }
    async function start() {
      stop();
      const ok = await GanAudioSvc.unlock();
      if (!ok) { setFase("blocked"); return; }
      const c = GanAudioSvc.getCtx();
      schedRef.current = GanAudioSvc.scheduleBeeps({ count: total, intervalSec, outs: [c.destination], beep: { peak: 0.28 } });
      setFase("playing"); setIdx(0);
      rafRef.current = requestAnimationFrame(loop);
    }
    useEffect(() => () => stop(), []);
    return (
      <div style={{ border: "1px solid var(--border)", borderRadius: 12, padding: "12px 14px", marginBottom: 10, background: "var(--surface)" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
          <b style={{ fontSize: 13.5 }}>{label}</b>
          <span style={{ fontSize: 12, color: "var(--text-3)" }}>{fmtTempo(durationSec)} · 1 som a cada {intervalSec}s · {total} estímulos</span>
          <div style={{ marginLeft: "auto", display: "flex", gap: 8 }}>
            {fase === "playing"
              ? <button type="button" className="btn btn-sm" style={{ background: "#DC2626", color: "#fff" }} onClick={stop}><Icon name="x" size={15} /> Parar</button>
              : <button type="button" className="btn btn-coral btn-sm" onClick={start}><Icon name="headset" size={15} /> Iniciar treino</button>}
          </div>
        </div>
        {fase === "playing" && (
          <div style={{ marginTop: 8 }}>
            <div style={{ height: 8, borderRadius: 6, background: "var(--surface-3)", overflow: "hidden" }}><div style={{ width: (100 * idx / total) + "%", height: "100%", background: "var(--primary)", transition: "width .12s" }} /></div>
            <div style={{ fontSize: 12, fontWeight: 700, color: "var(--text-2)", textAlign: "center", marginTop: 4 }}>Estímulo {idx} / {total}</div>
          </div>
        )}
        {fase === "blocked" && <div style={{ marginTop: 8, padding: "8px 12px", borderRadius: 10, background: "var(--accent-soft)", border: "1px solid var(--accent)", color: "var(--accent-strong)", fontWeight: 700, fontSize: 12.5 }}>O navegador bloqueou o áudio. Toque em “Iniciar treino” novamente para liberar.</div>}
      </div>
    );
  }

  // Opções do gravador: força Opus em ~32 kbps (voz) para reduzir muito o tamanho.
  function recorderOpts() {
    const opts = { audioBitsPerSecond: 32000 };
    const cands = ["audio/webm;codecs=opus", "audio/ogg;codecs=opus", "audio/webm"];
    for (const t of cands) { if (window.MediaRecorder && MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(t)) { opts.mimeType = t; break; } }
    return opts;
  }
  // Espaço livre estimado no navegador (MB). Retorna null se indisponível.
  async function freeStorageMB() {
    try { if (navigator.storage && navigator.storage.estimate) { const { usage, quota } = await navigator.storage.estimate(); if (quota) return (quota - (usage || 0)) / (1024 * 1024); } } catch (e) { /* */ }
    return null;
  }

  // ---------------- Tarefa + Gravação (bipes mixados digitalmente no stream) ----------------
  // Uma única ação: toca os estímulos (nos alto-falantes) e grava mic + bipes num só stream,
  // via MediaStreamAudioDestinationNode. Os bipes entram por MIXAGEM (não dependem de o mic
  // captar o alto-falante — o que falha com fone ou com cancelamento de eco). Guarda os
  // timestamps (ms desde o início) de cada estímulo e o total realmente emitido.
  function TarefaGravador({ intervalSec, count, hasAudio, onDone }) {
    const [fase, setFase] = useState("idle"); // idle | recording | blocked | saving
    const [idx, setIdx] = useState(0);
    const [erro, setErro] = useState("");
    const [quotaWarn, setQuotaWarn] = useState("");
    const refs = useRef({}); // { stream, mr, dest, micSrc, spk, rec, sched, raf, recStart, stamps }

    function cleanupNodes() {
      const r = refs.current;
      if (r.raf) { cancelAnimationFrame(r.raf); r.raf = null; }
      if (r.sched) { try { r.sched.stop(); } catch (e) { /* */ } r.sched = null; }
      try { r.micSrc && r.micSrc.disconnect(); } catch (e) { /* */ }
      try { r.spk && r.spk.disconnect(); } catch (e) { /* */ }
      try { r.rec && r.rec.disconnect(); } catch (e) { /* */ }
    }
    function finalize(emitted) {
      const r = refs.current;
      if (r.raf) { cancelAnimationFrame(r.raf); r.raf = null; }
      if (r.sched) { try { r.sched.stop(); } catch (e) { /* */ } }
      refs.current.emitted = emitted;
      setFase("saving");
      const mr = r.mr;
      if (mr && mr.state !== "inactive") { try { mr.stop(); } catch (e) { /* */ } } // dispara onstop → onDone
    }
    function loop() {
      const r = refs.current; const s = r.sched; if (!s) return;
      const now = GanAudioSvc.now();
      let n = 0; for (let i = 0; i < s.times.length; i++) { if (s.times[i] <= now) n++; else break; }
      setIdx(n);
      if (now >= s.endTime + 0.1) { finalize(count); return; }
      r.raf = requestAnimationFrame(loop);
    }
    async function start() {
      setErro("");
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !window.MediaRecorder) { setFase("blocked"); setErro("Gravação não suportada neste navegador."); return; }
      const unlocked = await GanAudioSvc.unlock();
      if (!unlocked) { setFase("blocked"); setErro("O navegador bloqueou o áudio (política de autoplay). Toque em “Liberar áudio e iniciar” para autorizar."); return; }
      const freeMB = await freeStorageMB();
      setQuotaWarn(freeMB != null && freeMB < 15 ? "Pouco espaço no navegador (~" + Math.max(0, Math.round(freeMB)) + " MB livres). Use “Liberar espaço” antes de gravar." : "");
      let stream;
      try {
        stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false } });
      } catch (e) { setFase("blocked"); setErro("Microfone bloqueado. Autorize o microfone e toque em “Liberar áudio e iniciar”."); return; }

      try {
        const c = GanAudioSvc.getCtx();
        const dest = c.createMediaStreamDestination();
        const micSrc = c.createMediaStreamSource(stream);
        micSrc.connect(dest); // voz → gravação (NÃO vai aos alto-falantes: evita realimentação)
        // Bipes: alto-falantes (nível normal) e gravação (mais baixo que a voz, para não mascarar os números)
        const spk = c.createGain(); spk.gain.value = 1.0; spk.connect(c.destination);
        const rec = c.createGain(); rec.gain.value = 0.22; rec.connect(dest);

        const mr = new MediaRecorder(dest.stream, recorderOpts()); const chunks = [];
        mr.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
        mr.onstop = () => {
          try { stream.getTracks().forEach((t) => t.stop()); } catch (e) { /* */ }
          cleanupNodes();
          const blob = new Blob(chunks, { type: mr.mimeType || "audio/webm" });
          const fr = new FileReader();
          fr.onloadend = () => { onDone({ audio: fr.result, stamps: refs.current.stamps || [], total: refs.current.emitted || count }); setFase("idle"); setIdx(0); };
          fr.readAsDataURL(blob);
        };

        mr.start();
        const recStart = c.currentTime; // referência t=0 da gravação (≈ início do MediaRecorder)
        const sched = GanAudioSvc.scheduleBeeps({ count, intervalSec, outs: [spk, rec], lead: 0.35, beep: { peak: 0.28 } });
        const stamps = sched.times.map((t) => Math.round((t - recStart) * 1000));
        refs.current = { stream, mr, dest, micSrc, spk, rec, sched, raf: null, recStart, stamps, emitted: 0 };
        setFase("recording"); setIdx(0);
        refs.current.raf = requestAnimationFrame(loop);
      } catch (e) {
        try { console.error("[GAN] iniciar tarefa:", e); } catch (_) { /* */ }
        try { stream.getTracks().forEach((t) => t.stop()); } catch (_) { /* */ }
        setFase("blocked"); setErro("Não foi possível iniciar a gravação neste navegador (" + ((e && e.name) || "erro") + "). Tente novamente ou use outro navegador.");
      }
    }
    function stopEarly() {
      const r = refs.current; const s = r.sched;
      let emitted = count;
      if (s) { const now = GanAudioSvc.now(); emitted = 0; for (let i = 0; i < s.times.length; i++) { if (s.times[i] <= now) emitted++; else break; } }
      finalize(emitted);
    }
    useEffect(() => () => { const r = refs.current; try { r.stream && r.stream.getTracks().forEach((t) => t.stop()); } catch (e) { /* */ } try { r.mr && r.mr.state !== "inactive" && r.mr.stop(); } catch (e) { /* */ } cleanupNodes(); }, []);

    const rec = fase === "recording";
    return (
      <div>
        <div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
          {rec
            ? <button type="button" className="btn btn-sm" style={{ background: "#DC2626", color: "#fff" }} onClick={stopEarly}><Icon name="x" size={15} /> Parar tarefa</button>
            : <button type="button" className="btn btn-coral btn-sm" onClick={start} disabled={fase === "saving"}><Icon name="headset" size={15} /> {fase === "saving" ? "Salvando…" : (fase === "blocked" ? "Liberar áudio e iniciar" : (hasAudio ? "Refazer tarefa (regravar)" : "Iniciar tarefa e gravar"))}</button>}
          {rec && <span style={{ fontSize: 12.5, color: "#DC2626", fontWeight: 700 }}>● gravando com bipes… (Opus ~32 kbps)</span>}
          {!rec && fase !== "blocked" && hasAudio && <span style={{ fontSize: 12, color: "var(--primary)", fontWeight: 700, display: "inline-flex", alignItems: "center", gap: 4 }}><Icon name="clipboardCheck" size={13} /> tarefa gravada</span>}
        </div>
        {rec && (
          <div style={{ marginTop: 8 }}>
            <div style={{ height: 8, borderRadius: 6, background: "var(--surface-3)", overflow: "hidden" }}><div style={{ width: (100 * idx / count) + "%", height: "100%", background: "#DC2626", transition: "width .12s" }} /></div>
            <div style={{ fontSize: 12, fontWeight: 700, color: "var(--text-2)", textAlign: "center", marginTop: 4 }}>Estímulo {idx} / {count}</div>
          </div>
        )}
        {fase === "blocked" && erro && <div style={{ display: "flex", alignItems: "flex-start", gap: 7, background: "#FEE2E2", border: "1px solid #DC2626", borderRadius: 10, padding: "9px 12px", marginTop: 8 }}>
          <span style={{ fontSize: 14 }}>🔒</span>
          <p style={{ margin: 0, fontSize: 12.5, color: "#991B1B", fontWeight: 700, lineHeight: 1.5 }}>{erro}</p>
        </div>}
        {quotaWarn && <div style={{ display: "flex", alignItems: "flex-start", gap: 7, background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 10, padding: "8px 11px", marginTop: 8 }}>
          <span style={{ fontSize: 14 }}>⚠️</span>
          <p style={{ margin: 0, fontSize: 12, color: "var(--accent-strong)", fontWeight: 700, lineHeight: 1.5 }}>{quotaWarn}</p>
        </div>}
        <p className="help-text" style={{ marginTop: 8, marginBottom: 0 }}>Use fone de ouvido no examinador se possível. Os bipes são mixados digitalmente na gravação — ficam registrados mesmo com fone.</p>
      </div>
    );
  }

  // ---------------- Componente principal ----------------
  function Gan({ onBack, onOpenPatient }) {
    const novaEtapa = () => ({ audio: "", stamps: [], cells: new Array(90).fill(null), estimulos: 0, override: {}, treino: { t1: ["", "", "", "", "", ""], t2ap: false, t2: ["", "", "", "", "", ""] } });
    const novoDados = () => ({ a: novaEtapa(), b: novaEtapa(), conclusao: "", examinador: (window.__USER && window.__USER.name) || "" });
    const [view, setView] = useState("ident"); // ident | menu | a | b | sintese
    const [pid, setPid] = useState(""); const [avalId, setAvalId] = useState(null); const [readonly, setReadonly] = useState(false);
    const [idade, setIdade] = useState(""); const [escolaTipo, setEscolaTipo] = useState(""); const [date, setDate] = useState((window.todayISO ? window.todayISO() : "2026-06-04"));
    const [status, setStatus] = useState({ a: "nao", b: "nao" });
    const [concluida, setConcluida] = useState({ a: false, b: false });
    const [dados, setDados] = useState(novoDados());
    const [savedJson, setSavedJson] = useState(""); const [savedAt, setSavedAt] = useState(null);
    const [dirty, setDirty] = useState(false); const [saving, setSaving] = useState(false);
    const [toast, setToast] = useState(null); const [leaveGuard, setLeaveGuard] = useState(null); const [showClear, setShowClear] = useState(false); const [storageMgr, setStorageMgr] = useState(false);
    const [err, setErr] = useState("");
    const [instrOpen, setInstrOpen] = useState(true);
    const [glossOpen, setGlossOpen] = useState(false);
    const [cursor, setCursor] = useState({ a: 0, b: 0 });      // quadrado atual (registro)
    const [modo, setModo] = useState({ a: "registro", b: "registro" }); // registro | correcao
    const [selCell, setSelCell] = useState(null);              // {parte, idx} selecionado para reclassificar
    const [intr, setIntr] = useState("");                      // campo de intrusão do teclado
    const [playIdx, setPlayIdx] = useState({ a: -1, b: -1 });  // quadrado destacado durante a reprodução
    const [seekReq, setSeekReq] = useState({ a: null, b: null }); // pedido de reposicionamento por quadrado

    const patient = (window.PATIENTS || []).find((p) => p.id === pid);
    const listaAvals = pid ? avalsDoPaciente(pid) : [];

    useEffect(() => { if (!dirty) return; const h = (e) => { e.preventDefault(); e.returnValue = ""; return ""; }; window.addEventListener("beforeunload", h); return () => window.removeEventListener("beforeunload", h); }, [dirty]);
    useEffect(() => { if (!toast) return; const id = setTimeout(() => setToast(null), toast.kind === "err" ? 5000 : 2600); return () => clearTimeout(id); }, [toast]);

    function goTop() { const el = document.querySelector(".main-scroll"); if (el) el.scrollTop = 0; }
    function limparInstancia() { setAvalId(null); setReadonly(false); setStatus({ a: "nao", b: "nao" }); setConcluida({ a: false, b: false }); setDados(novoDados()); setSavedJson(""); setSavedAt(null); setDirty(false); setCursor({ a: 0, b: 0 }); }
    function resetTudo() { limparInstancia(); setPid(""); setIdade(""); setEscolaTipo(""); setDate((window.todayISO ? window.todayISO() : "2026-06-04")); setErr(""); setView("ident"); }
    function pickPatient(id) { setPid(id); const p = (window.PATIENTS || []).find((x) => x.id === id); setIdade(p && p.age ? String(p.age) : ""); setEscolaTipo((p && p.school_type) || ""); limparInstancia(); }

    function novaAvaliacao() {
      if (!pid) { setErr("Selecione o paciente."); return; }
      if (!idade) { setErr("Informe a idade."); return; }
      if (!escolaTipo) { setErr("Selecione o tipo de escola."); return; }
      const id = newAvalId(); const base = novoDados();
      try { putAval({ id, pid, patientName: patient ? patient.name : "", idade, escolaTipo, date, status: { a: "nao", b: "nao" }, dados: base, finalizada: false, savedAt: null, createdAt: Date.now(), finishedAt: null }); } catch (e) { /* */ }
      setAvalId(id); setReadonly(false); setStatus({ a: "nao", b: "nao" }); setConcluida({ a: false, b: false }); setDados(base); setSavedJson(JSON.stringify(base)); setSavedAt(null); setDirty(false);
      setErr(""); setView("menu"); goTop();
    }
    function abrirAval(a, ro) {
      setAvalId(a.id); setReadonly(!!ro); setPid(a.pid); setIdade(a.idade || ""); setEscolaTipo(a.escolaTipo || ""); setDate(a.date || (window.todayISO ? window.todayISO() : "2026-06-04"));
      const st = a.status || { a: "nao", b: "nao" }; setStatus(st); setConcluida({ a: st.a === "concluida", b: st.b === "concluida" });
      const d = a.dados || novoDados(); setDados(d); setSavedJson(JSON.stringify(d)); setSavedAt(a.savedAt); setDirty(false);
      setErr(""); setView("menu"); goTop();
    }

    function setEtapa(pk, patch) { if (readonly) return; setDados((d) => ({ ...d, [pk]: { ...d[pk], ...patch } })); setDirty(true); }
    function setDadosField(field, v) { if (readonly) return; setDados((d) => ({ ...d, [field]: v })); setDirty(true); }
    function setCell(pk, i, v) { if (readonly) return; setDados((d) => { const cells = d[pk].cells.slice(); cells[i] = v; return { ...d, [pk]: { ...d[pk], cells } }; }); setDirty(true); }
    function toggleConcluida(pk) { if (readonly) return; setConcluida((c) => ({ ...c, [pk]: !c[pk] })); setDirty(true); }

    // teclado numérico
    function digita(pk, v) {
      const i = cursor[pk];
      setCell(pk, i, v);
      const est = dados[pk].estimulos || CFG[pk].estimulos;
      if (i < Math.min(89, est - 1)) setCursor((c) => ({ ...c, [pk]: i + 1 }));
    }

    const partHasData = (pk) => { const d = dados[pk]; return d.audio || d.cells.some((v) => v !== null) || (d.treino && (d.treino.t1 || d.treino.t2)); };
    const computeStatus = () => ({ a: concluida.a ? "concluida" : (partHasData("a") ? "andamento" : "nao"), b: concluida.b ? "concluida" : (partHasData("b") ? "andamento" : "nao") });

    function persistAval(finalizar) {
      const when = new Date().toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
      const nextStatus = computeStatus(); const prev = loadAval(avalId);
      const a = { id: avalId, pid, patientName: patient ? patient.name : "", idade, escolaTipo, date, status: nextStatus, dados, finalizada: !!finalizar, savedAt: when, createdAt: (prev && prev.createdAt) || Date.now(), finishedAt: finalizar ? Date.now() : (prev && prev.finishedAt) || null };
      putAval(a); return { when, nextStatus, a };
    }
    // Verifica a coerência das partes com dados registrados; devolve mensagem se inconsistente.
    function checarCoerencia() {
      for (const pk of ["a", "b"]) {
        const d = dados[pk]; const est = d.estimulos || CFG[pk].estimulos;
        if (!partHasData(pk)) continue; // parte sem registro não bloqueia
        const c = contar(applyOverrideLocal(autoClassify(d.cells, est), d.override));
        const coer = coerenciaGAN(c, est);
        if (!coer.ok) return `Parte ${pk.toUpperCase()}: a soma das classificações (${coer.soma}) não fecha com o total de estímulos (${est}). Revise a grade antes de salvar.`;
      }
      return null;
    }
    async function salvar() {
      if (readonly) return false; if (!avalId) { setErr("Inicie uma avaliação primeiro."); return false; }
      const inc = checarCoerencia();
      if (inc) { setToast({ kind: "err", text: inc }); return false; }
      setSaving(true); setToast(null);
      try { const { when, nextStatus } = persistAval(false); setStatus(nextStatus); setSavedJson(JSON.stringify(dados)); setSavedAt(when); setDirty(false); setToast({ kind: "ok", text: "Avaliação salva" }); return true; }
      catch (e) {
        setToast({ kind: "err", text: window.classifyStorageError ? window.classifyStorageError(e) : "Não foi possível salvar — tente novamente." });
        const quota = e && (e.name === "QuotaExceededError" || e.code === 22 || e.code === 1014 || /quota|exceeded/i.test(e.message || ""));
        if (quota) setStorageMgr(true);
        return false;
      }
      finally { setSaving(false); }
    }
    function gravarNoProntuario(a) {
      if (!window.addAvaliacao) return;
      const ea = a.dados.a, eb = a.dados.b;
      const ca = contar(applyOverride(autoClassify(ea.cells, ea.estimulos || CFG.a.estimulos), ea.override));
      const cb = contar(applyOverride(autoClassify(eb.cells, eb.estimulos || CFG.b.estimulos), eb.override));
      const result = `A: ${ca.acerto} acertos · B: ${cb.acerto} acertos`;
      const note = `GAN (${a.idade} anos, ${a.escolaTipo === "publica" ? "pública" : "privada"}). Parte A — acertos ${ca.acerto}, intrusões ${ca.intrusao}, omissões ${ca.omissao}, perseverações ${ca.perseveracao}. Parte B — acertos ${cb.acerto}, intrusões ${cb.intrusao}, omissões ${cb.omissao}, perseverações ${cb.perseveracao}. (escores brutos)`;
      window.addAvaliacao({ pid: a.pid, name: INSTRUMENT_NAME, date: a.date, result, note, sheet: { kind: "gan-final", avalId: a.id, idade: a.idade, escolaTipo: a.escolaTipo, resumoA: ca, resumoB: cb } });
    }
    async function finalizar() {
      if (readonly || !avalId) return; setSaving(true); setToast(null);
      try { const { a } = persistAval(true); gravarNoProntuario(a); setDirty(false); setToast({ kind: "ok", text: "Avaliação finalizada e salva no prontuário" }); resetTudo(); goTop(); }
      catch (e) { console.error("[GAN] finalizar:", e); setToast({ kind: "err", text: "Não foi possível finalizar — tente novamente." }); }
      finally { setSaving(false); }
    }

    function guardedLeave(proceed) { if (dirty) setLeaveGuard({ proceed }); else proceed(); }
    function discardAndLeave() { try { setDados(JSON.parse(savedJson)); } catch (e) { /* */ } setConcluida({ a: status.a === "concluida", b: status.b === "concluida" }); setDirty(false); const p = leaveGuard && leaveGuard.proceed; setLeaveGuard(null); if (p) p(); }
    async function saveAndLeave() { const ok = await salvar(); if (!ok) return; const p = leaveGuard && leaveGuard.proceed; setLeaveGuard(null); if (p) p(); }
    async function salvarEAvancar(nextView) { if (readonly) { setView(nextView); goTop(); return; } const ok = await salvar(); if (!ok) return; setView(nextView); goTop(); }
    function apagarDoPaciente() { if (!pid) return; try { const all = loadAvals(); Object.keys(all).forEach((id) => { if (all[id] && all[id].pid === pid) delete all[id]; }); saveAvals(all); } catch (e) { /* */ } setShowClear(false); limparInstancia(); setToast({ kind: "ok", text: "Avaliações desta criança apagadas." }); }

    // "Liberar espaço": lista as avaliações GAN com áudio local e seu tamanho aproximado.
    function storageList() {
      let all = {}; try { all = loadAvals(); } catch (e) { /* */ }
      return Object.keys(all).map((id) => {
        const a = all[id]; const dA = (a && a.dados && a.dados.a) || {}; const dB = (a && a.dados && a.dados.b) || {};
        const len = ((dA.audio || "").length) + ((dB.audio || "").length);
        return { id, a, bytes: Math.round(len * 0.75), hasAudio: len > 0 };
      }).filter((x) => x.hasAudio).sort((x, y) => y.bytes - x.bytes);
    }
    function limparAudio(id) {
      try { const all = loadAvals(); const a = all[id]; if (!a || !a.finalizada) return; if (a.dados) { if (a.dados.a) a.dados.a = { ...a.dados.a, audio: "" }; if (a.dados.b) a.dados.b = { ...a.dados.b, audio: "" }; } saveAvals(all); } catch (e) { /* */ }
      if (id === avalId) setDados((d) => ({ ...d, a: { ...d.a, audio: "" }, b: { ...d.b, audio: "" } }));
      setToast({ kind: "ok", text: "Áudio removido do armazenamento local." });
    }
    const fmtBytes = (b) => b >= 1024 * 1024 ? (b / (1024 * 1024)).toFixed(1) + " MB" : Math.max(1, Math.round(b / 1024)) + " KB";

    function applyOverrideLocal(auto, override) { return auto.map((t, i) => (override && override[i]) ? override[i] : t); }

    const accordion = (open, toggle, icon, title) => (
      <button type="button" onClick={toggle} style={{ width: "100%", display: "flex", alignItems: "center", gap: 10, background: "none", border: "none", padding: "14px 16px", cursor: "pointer", textAlign: "left", color: "var(--text)" }}>
        <span style={SI}><Icon name={icon} size={18} /></span><b style={{ flex: 1, fontSize: 15 }}>{title}</b>
        <Icon name="chevDown" size={18} style={{ transform: open ? "rotate(180deg)" : "none", transition: "transform .2s" }} />
      </button>
    );
    const FalaInstr = ({ children }) => (
      <div style={{ background: "var(--primary-soft)", borderLeft: "4px solid var(--primary)", borderRadius: 12, padding: "12px 15px", margin: "8px 0" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 5 }}><span style={{ ...SI, width: 26, height: 26, borderRadius: 8 }}><Icon name="chat" size={15} /></span><b style={{ color: "var(--primary)", fontSize: 14 }}>Diga à criança:</b></div>
        <p style={{ margin: 0, fontStyle: "italic", fontSize: 15, lineHeight: 1.65, color: "var(--text)" }}>“{children}”</p>
      </div>
    );

    // ------- Render de uma parte (A ou B) -------
    function renderParte(pk) {
      const cfg = CFG[pk]; const d = dados[pk];
      const est = d.estimulos || cfg.estimulos;
      const auto = autoClassify(d.cells, est);
      const eff = applyOverrideLocal(auto, d.override);
      const c = contar(eff);
      const coer = coerenciaGAN(c, est);
      const preench = preenchidosGAN(d.cells, est);
      const completa = preench === est;
      const cur = cursor[pk]; const mo = modo[pk];
      const setModoPk = (m) => setModo((o) => ({ ...o, [pk]: m }));
      const cellVal = (v) => v === "om" ? "–" : (v == null ? "" : v);

      return (
        <>
          {/* Instruções */}
          <div className="form-card" style={{ padding: 0, overflow: "hidden" }}>
            {accordion(instrOpen, () => setInstrOpen((o) => !o), "fileText", "Instruções de Aplicação")}
            {instrOpen && (
              <div style={{ padding: "0 16px 16px" }}>
                <p className="help-text" style={{ marginTop: 0 }}><b>Orientações gerais</b></p>
                <ul style={{ margin: "0 0 8px", paddingLeft: 18 }}>{GERAIS.map((t, i) => <li key={i} style={{ fontSize: 13, lineHeight: 1.55, marginBottom: 4, color: "var(--text-2)" }}>{t}</li>)}</ul>
                <FalaInstr>{pk === "a" ? INSTR.a : INSTR.b}</FalaInstr>
                <p className="help-text" style={{ margin: "0 0 4px" }}>Após o treino correto:</p>
                <FalaInstr>{pk === "a" ? INSTR.aOk : INSTR.bOk}</FalaInstr>
                <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 12, padding: "10px 13px" }}>
                  <span style={{ fontSize: 16 }}>⚠️</span>
                  <p style={{ margin: 0, fontSize: 12.5, color: "var(--accent-strong)", fontWeight: 700, lineHeight: 1.5 }}>Critérios de erro no treino (repetir se ocorrer): (1) números de sequências conhecidas; (2) ausência de número(s) para algum som.</p>
                </div>
              </div>
            )}
          </div>

          {/* Estímulos sonoros — TREINO (a tarefa fica no card de gravação, com os bipes mixados) */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="headset" size={18} /></span> Estímulos sonoros — Treino</div>
            <p className="help-text" style={{ marginTop: 0 }}>Bipes gerados pelo app (senoidal ~800 Hz, ~80 ms), agendados pelo relógio do áudio. Módulo isolado — trocável pelo áudio oficial depois. A <b>faixa da tarefa</b> é disparada junto com a gravação (abaixo).</p>
            <TreinoBipes durationSec={cfg.treinoDur} intervalSec={cfg.intervalo} label={"Treino " + pk.toUpperCase()} />
          </div>

          {/* Registro do treino — 6 quadrados por tentativa (não pontua) */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="clipboardCheck" size={18} /></span> Registro do treino</div>
            {(() => {
              const arr6 = (x) => (Array.isArray(x) ? x.concat(["", "", "", "", "", ""]).slice(0, 6) : ["", "", "", "", "", ""]);
              const t1 = arr6(d.treino.t1), t2 = arr6(d.treino.t2);
              const linha = (vals, onSet) => (
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 4 }}>
                  {vals.map((v, i) => (
                    <input key={i} className="ctrl" value={v} onChange={(e) => { const a = vals.slice(); a[i] = e.target.value; onSet(a); }} disabled={readonly}
                      style={{ width: 46, height: 46, textAlign: "center", fontWeight: 800, fontSize: 16, padding: 0, flex: "none" }} />
                  ))}
                </div>
              );
              return (
                <>
                  <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)" }}>1ª Tentativa — Respostas</label>
                  {linha(t1, (a) => setEtapa(pk, { treino: { ...d.treino, t1: a } }))}
                  <label style={{ display: "inline-flex", alignItems: "center", gap: 7, fontSize: 13, fontWeight: 700, color: "var(--text-2)", marginTop: 12, cursor: "pointer" }}>
                    <input type="checkbox" checked={d.treino.t2ap} onChange={(e) => setEtapa(pk, { treino: { ...d.treino, t2ap: e.target.checked } })} disabled={readonly} /> 2ª Tentativa: aplicada
                  </label>
                  {d.treino.t2ap && (
                    <>
                      <label style={{ fontSize: 12.5, fontWeight: 700, color: "var(--text-3)", display: "block", marginTop: 8 }}>2ª Tentativa — Respostas</label>
                      {linha(t2, (a) => setEtapa(pk, { treino: { ...d.treino, t2: a } }))}
                    </>
                  )}
                  <p className="help-text" style={{ marginBottom: 0, marginTop: 10 }}>As respostas do treino não entram na pontuação.</p>
                </>
              );
            })()}
          </div>

          {/* Tarefa + Gravação (bipes mixados) */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="headset" size={18} /></span> Tarefa e gravação (Parte {pk.toUpperCase()})</div>
            {!readonly && <TarefaGravador intervalSec={cfg.intervalo} count={cfg.estimulos} hasAudio={!!d.audio}
              onDone={({ audio, stamps, total }) => { setEtapa(pk, { audio, stamps, estimulos: total }); setToast({ kind: "ok", text: "Tarefa gravada — " + total + " estímulos emitidos." }); }} />}
            {d.audio ? (
              <div style={{ marginTop: 10 }}>
                <AudioPlayer src={d.audio} stamps={d.stamps || []} seekReq={seekReq[pk]} onActiveIdx={(i) => setPlayIdx((p) => ({ ...p, [pk]: i }))} />
                {(d.stamps && d.stamps.length > 0) && <p className="help-text" style={{ marginBottom: 0 }}>Na Correção, toque num quadrado para ouvir o trecho daquele estímulo; o quadrado do momento atual fica destacado durante a reprodução.</p>}
              </div>
            ) : <p className="help-text" style={{ marginBottom: 0 }}>Inicie a tarefa para tocar os estímulos e gravar a evocação da criança (com os bipes embutidos).</p>}
          </div>

          {/* Grade + teclado / correção */}
          <div className="form-card">
            <div className="sec-head" style={{ flexWrap: "wrap", gap: 8 }}>
              <span className="si"><Icon name="clipboardList" size={18} /></span> {mo === "registro" ? "Registro da grade" : "Correção"}
              <div style={{ display: "flex", gap: 6, marginLeft: "auto" }}>
                <button type="button" className={"btn btn-sm " + (mo === "registro" ? "btn-primary" : "btn-ghost")} onClick={() => setModoPk("registro")}>Registro</button>
                <button type="button" className={"btn btn-sm " + (mo === "correcao" ? "btn-primary" : "btn-ghost")} onClick={() => setModoPk("correcao")}>Correção</button>
              </div>
            </div>

            <div className="grid-2" style={{ marginBottom: 10 }}>
              <div className="fld" style={{ marginBottom: 0 }}><label>Estímulos emitidos na parte</label>
                <input className="ctrl" type="number" min="1" max="90" value={d.estimulos || cfg.estimulos} onChange={(e) => setEtapa(pk, { estimulos: Math.max(1, Math.min(90, parseInt(e.target.value || cfg.estimulos, 10))) })} disabled={readonly} />
              </div>
            </div>

            {/* Grade — 10 colunas, preenchimento horizontal (layout do protocolo impresso) */}
            <p className="help-text" style={{ marginTop: 0, marginBottom: 8 }}><b>Tarefa — Intervalo de {pk === "a" ? "dois segundos" : "um segundo"}</b> (anotar números na horizontal, da esquerda para a direita).</p>
            <div style={{ overflowX: "auto" }}>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(10, minmax(0, 1fr))", gap: 5, minWidth: 380 }}>
                {d.cells.slice(0, est).map((v, i) => {
                  const t = eff[i];
                  const isCursor = mo === "registro" && i === cur;
                  const isSel = selCell && selCell.parte === pk && selCell.idx === i;
                  const isPlaying = playIdx[pk] === i;
                  const hasStamp = d.stamps && d.stamps[i] != null;
                  const bg = (mo === "correcao" && t) ? TIPOS[t].col : (v != null ? "var(--surface-2)" : "var(--surface)");
                  const fg = (mo === "correcao" && t) ? "#fff" : "var(--text)";
                  const seekTo = () => { if (hasStamp) setSeekReq((s) => ({ ...s, [pk]: { idx: i, nonce: ((s[pk] && s[pk].nonce) || 0) + 1 } })); };
                  return (
                    <button key={i} type="button"
                      onClick={() => { if (mo === "registro") setCursor((cc) => ({ ...cc, [pk]: i })); else { setSelCell({ parte: pk, idx: i }); seekTo(); } }}
                      style={{ height: 38, borderRadius: 7, cursor: "pointer", fontSize: 13, fontWeight: 800, background: bg, color: fg, border: "2px solid " + (isPlaying ? "#0EA5E9" : (isCursor || isSel ? "var(--primary)" : "var(--border)")), boxShadow: isPlaying ? "0 0 0 3px rgba(14,165,233,.35)" : "none", position: "relative" }}>
                      <span style={{ position: "absolute", top: 1, left: 3, fontSize: 8, fontWeight: 700, opacity: 0.6 }}>{i + 1}</span>
                      {cellVal(v)}
                    </button>
                  );
                })}
              </div>
            </div>

            {/* Teclado (modo registro) */}
            {mo === "registro" && !readonly && (
              <div style={{ marginTop: 12, position: "sticky", bottom: 0, background: "var(--surface)", paddingTop: 8, borderTop: "1px solid var(--border)" }}>
                <p className="help-text" style={{ marginTop: 0 }}>Quadrado atual: <b>{cur + 1}</b>. Toque um número para preencher e avançar.</p>
                <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(52px, 1fr))", gap: 6 }}>
                  {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((num) => (
                    <button key={num} type="button" className="btn btn-ghost btn-sm" style={{ fontWeight: 800, fontSize: 16, padding: "10px 0" }} onClick={() => digita(pk, num)}>{num}</button>
                  ))}
                  <button type="button" className="btn btn-ghost btn-sm" style={{ padding: "10px 0" }} onClick={() => digita(pk, "om")}>Omissão</button>
                </div>
                <div style={{ display: "flex", gap: 8, marginTop: 8, flexWrap: "wrap", alignItems: "center" }}>
                  <button type="button" className="btn btn-ghost btn-sm" onClick={() => setCursor((cc) => ({ ...cc, [pk]: Math.max(0, cur - 1) }))}><Icon name="chevL" size={15} /> Voltar</button>
                  <button type="button" className="btn btn-ghost btn-sm" onClick={() => setCursor((cc) => ({ ...cc, [pk]: Math.min(Math.min(89, est - 1), cur + 1) }))}>Avançar <Icon name="chevR" size={15} /></button>
                  <span style={{ marginLeft: 8, fontSize: 12.5, color: "var(--text-3)" }}>Intrusão (fora de 1–10):</span>
                  <input className="ctrl" style={{ width: 90 }} type="number" value={intr} onChange={(e) => setIntr(e.target.value)} placeholder="nº" />
                  <button type="button" className="btn btn-ghost btn-sm" onClick={() => { const v = parseInt(intr, 10); if (!isNaN(v)) { digita(pk, v); setIntr(""); } }}>Registrar</button>
                </div>
              </div>
            )}

            {/* Reclassificar (modo correção) */}
            {mo === "correcao" && (
              <div style={{ marginTop: 12 }}>
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginBottom: 8 }}>
                  {TIPO_KEYS.map((k) => (<span key={k} style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 11.5, fontWeight: 700, color: "var(--text-2)" }}><i style={{ width: 11, height: 11, borderRadius: 3, background: TIPOS[k].col, display: "inline-block" }} />{TIPOS[k].l}</span>))}
                </div>
                {selCell && selCell.parte === pk && !readonly && (
                  <div style={{ border: "1px solid var(--border)", borderRadius: 10, padding: "10px 12px", background: "var(--surface-2)" }}>
                    <p className="help-text" style={{ marginTop: 0 }}>Reclassificar quadrado <b>{selCell.idx + 1}</b> ({cellVal(d.cells[selCell.idx]) || "—"}):</p>
                    <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                      {TIPO_KEYS.map((k) => (<button key={k} type="button" onClick={() => setEtapa(pk, { override: { ...d.override, [selCell.idx]: k } })} style={{ fontSize: 12, fontWeight: 700, padding: "5px 10px", borderRadius: 8, cursor: "pointer", border: "1.5px solid " + TIPOS[k].col, background: eff[selCell.idx] === k ? TIPOS[k].col : "transparent", color: eff[selCell.idx] === k ? "#fff" : TIPOS[k].col }}>{TIPOS[k].l}</button>))}
                      <button type="button" className="btn btn-ghost btn-xs" onClick={() => { const ov = { ...d.override }; delete ov[selCell.idx]; setEtapa(pk, { override: ov }); }}>Auto</button>
                    </div>
                  </div>
                )}
                {!selCell && <p className="help-text" style={{ marginBottom: 0 }}>Toque num quadrado para ajustar sua classificação. O julgamento final é seu.</p>}
              </div>
            )}
          </div>

          {/* Análise Quantitativa (ordem/agrupamento do protocolo) */}
          <div className="form-card" style={{ background: "var(--primary-soft)", border: "1px solid var(--primary)" }}>
            <div className="sec-head"><span className="si"><Icon name="chart" size={18} /></span> Análise Quantitativa — Parte {pk.toUpperCase()} (escores brutos)</div>
            <div className="hay-scores"><div className="hsc"><span>Acertos</span><b>{c.acerto}</b></div></div>

            <p className="section-label" style={{ margin: "12px 0 4px" }}>ERROS</p>
            <div className="hay-scores">
              <div className="hsc"><span>Intrusões</span><b>{c.intrusao}</b></div>
              <div className="hsc"><span>Perseverações</span><b>{c.perseveracao}</b></div>
              <div className="hsc"><span>Omissões</span><b>{c.omissao}</b></div>
            </div>

            <p className="section-label" style={{ margin: "12px 0 4px" }}>SEQUÊNCIAS DIRETAS</p>
            <div className="hay-scores">
              <div className="hsc"><span>Número de erros</span><b>{c.sdErr}</b></div>
              <div className="hsc"><span>Número de sequências</span><b>{c.sdSeq}</b></div>
            </div>

            <p className="section-label" style={{ margin: "12px 0 4px" }}>SEQUÊNCIAS INDIRETAS</p>
            <div className="hay-scores">
              <div className="hsc"><span>Número de erros</span><b>{c.siErr}</b></div>
              <div className="hsc"><span>Número de sequências</span><b>{c.siSeq}</b></div>
            </div>

            <div style={{ marginTop: 12, fontSize: 12.5, fontWeight: 700, color: coer.ok ? "var(--text-3)" : "#DC2626", display: "flex", alignItems: "center", gap: 7, flexWrap: "wrap" }}>
              <span>Conferência: {c.acerto} + {c.intrusao} + {c.omissao} + {c.perseveracao} + {c.sdErr} + {c.siErr}{(c.par + c.impar + c.pronta) > 0 ? " + " + (c.par + c.impar + c.pronta) + " (seq. qualit.)" : ""} = <b>{coer.soma}</b> de <b>{est}</b> estímulos.</span>
              {coer.ok ? <span style={{ color: "var(--primary)" }}>✓ coerente</span> : <span style={{ display: "inline-flex", alignItems: "center", gap: 5 }}><Icon name="x" size={14} /> inconsistência</span>}
            </div>
            {!coer.ok && <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "#FEE2E2", border: "1px solid #DC2626", borderRadius: 10, padding: "9px 12px", marginTop: 8 }}>
              <span style={{ fontSize: 15 }}>⚠️</span>
              <p style={{ margin: 0, fontSize: 12.5, color: "#991B1B", fontWeight: 700, lineHeight: 1.5 }}>A soma das classificações ({coer.soma}) não fecha com o total de estímulos ({est}). Revise a grade e o campo “Estímulos emitidos na parte” antes de salvar.</p>
            </div>}
          </div>

          {/* Análise Qualitativa (não são erros) */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="sparkles" size={18} /></span> Análise Qualitativa</div>
            <p className="help-text" style={{ marginTop: 0 }}>Não são erros — apenas estratégias de evocação.</p>
            <div className="hay-scores">
              <div className="hsc"><span>Sequências ímpares</span><b>{c.imparSeq}</b></div>
              <div className="hsc"><span>Sequências pares</span><b>{c.parSeq}</b></div>
              <div className="hsc"><span>Sequências prontas e/ou repetidas</span><b>{c.prontaSeq}</b></div>
            </div>
          </div>

          {/* Interpretação normativa (percentis por idade × escola) */}
          {(() => {
            const nAll = NORMAS[idade] && NORMAS[idade][escolaTipo] && NORMAS[idade][escolaTipo][pk];
            const rawMap = { AC: c.acerto, IN: c.intrusao, OM: c.omissao, PE: c.perseveracao, SD: c.sdErr, SI: c.siErr };
            return (
              <div className="form-card">
                <div className="sec-head"><span className="si"><Icon name="chart" size={18} /></span> Interpretação normativa — {idade} anos · {escolaTipo === "publica" ? "pública" : "privada"} · Parte {pk.toUpperCase()}</div>
                <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 10, padding: "9px 12px", marginBottom: 10 }}>
                  <span style={{ fontSize: 15 }}>⚠️</span>
                  <p style={{ margin: 0, fontSize: 12.5, color: "var(--accent-strong)", fontWeight: 700, lineHeight: 1.5 }}>Normas em fase de validação — confira a classificação com as tabelas do manual.</p>
                </div>
                {!completa ? (
                  <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "var(--surface-2)", border: "1px dashed var(--border-2)", borderRadius: 10, padding: "12px 14px" }}>
                    <span style={{ fontSize: 15 }}>📝</span>
                    <p style={{ margin: 0, fontSize: 13, color: "var(--text-2)", fontWeight: 600, lineHeight: 1.55 }}>Preencha a grade de registro para gerar a interpretação normativa — <b>{preench} de {est}</b> quadrados preenchidos. Marque cada estímulo com o número dito, uma intrusão ou <b>Omissão</b>.</p>
                  </div>
                ) : !nAll ? <p className="help-text" style={{ margin: 0 }}>Sem norma cadastrada para esta idade/escola.</p> : (
                  <div style={{ overflowX: "auto" }}>
                    <table className="hay-sheet" style={{ minWidth: 420 }}>
                      <thead><tr><th>Medida</th><th>Bruto</th><th>Z</th><th>Faixa de percentil</th><th>Classificação</th></tr></thead>
                      <tbody>
                        {MEDIDAS_ORD.map((med) => {
                          const b = interpretar(med, rawMap[med], nAll[med]);
                          return (
                            <tr key={med}>
                              <td>{MEDIDA_L[med]}</td>
                              <td className="c">{rawMap[med]}</td>
                              <td className="c">{b && b.z != null ? (b.z > 0 ? "+" : "") + b.z.toFixed(2) : "—"}</td>
                              <td className="c">{b ? <span>{b.faixa}<br /><span style={{ fontSize: 10.5, color: "var(--text-3)", fontWeight: 700 }}>{b.dir}</span></span> : "—"}</td>
                              <td>{b ? <span style={{ color: b.cls.col, fontWeight: 800 }}>{b.cls.l}</span> : "—"}</td>
                            </tr>
                          );
                        })}
                      </tbody>
                    </table>
                  </div>
                )}
                <p className="help-text" style={{ marginBottom: 0 }}>Direção respeitada: acertos (pior = menor) · erros — intrusões, omissões, perseverações, sequências diretas/indiretas (pior = maior). Classificação por faixa (sem interpolar) + escore Z. O julgamento final é do profissional.</p>
              </div>
            );
          })()}
        </>
      );
    }

    // ------- Síntese dos Resultados (habilitada com A e B concluídas) -------
    function computeSintese() {
      const mk = (pk) => {
        const d = dados[pk]; const est = d.estimulos || CFG[pk].estimulos;
        const c = contar(applyOverrideLocal(autoClassify(d.cells, est), d.override));
        const coer = coerenciaGAN(c, est);
        const preench = preenchidosGAN(d.cells, est);
        const completa = preench === est;
        const norma = NORMAS[idade] && NORMAS[idade][escolaTipo] && NORMAS[idade][escolaTipo][pk];
        const rawMap = { AC: c.acerto, IN: c.intrusao, OM: c.omissao, PE: c.perseveracao, SD: c.sdErr, SI: c.siErr };
        // Bloqueia Z/percentil/classificação enquanto a grade não estiver completa (item 2).
        const rows = MEDIDAS_ORD.map((med) => {
          const r = completa ? (interpretar(med, rawMap[med], norma && norma[med]) || { z: null, band: null, faixa: null, dir: null, cls: null }) : { z: null, band: null, faixa: null, dir: null, cls: null };
          return { med, raw: rawMap[med], z: r.z, faixa: r.faixa, dir: r.dir, cls: r.cls };
        });
        return { c, rows, hasNorma: !!norma, completa, preench, est, coer };
      };
      return { a: mk("a"), b: mk("b") };
    }

    function exportSintese(via) {
      const syn = computeSintese();
      const ctx = {
        patientName: patient ? patient.name : "", idade,
        escolaLabel: escolaTipo === "publica" ? "Pública" : "Privada",
        date, examinador: dados.examinador || "", conclusao: dados.conclusao || "", syn,
      };
      const html = via === "familia" ? buildSinteseFamiliaHTML(ctx) : buildSinteseClinicaHTML(ctx);
      const w = window.open("", "_blank");
      if (!w) { setToast({ kind: "err", text: "Permita pop-ups para exportar o PDF." }); return; }
      w.document.write(html); w.document.close(); w.focus();
      setTimeout(() => { try { w.print(); } catch (e) { /* usuário imprime manualmente */ } }, 350);
    }

    function renderSintese() {
      const syn = computeSintese();
      const perfil = computePerfil(syn);
      const condCell = (row, completa) => (
        <td style={{ verticalAlign: "top" }}>
          <div style={{ fontSize: 12.5, lineHeight: 1.5 }}>
            <div>Bruto: <b>{row.raw}</b></div>
            {completa ? (
              <>
                <div style={{ color: "var(--text-3)" }}>Z: {row.z != null ? (row.z > 0 ? "+" : "") + row.z.toFixed(2) : "—"}</div>
                <div style={{ color: "var(--text-3)" }}>Percentil: {row.faixa || "—"}{row.dir ? <><br /><span style={{ fontSize: 10.5 }}>{row.dir}</span></> : null}</div>
                <div style={{ marginTop: 3 }}>{row.cls ? <span style={{ color: row.cls.col, fontWeight: 800 }}>{row.cls.l}</span> : "—"}</div>
              </>
            ) : <div style={{ color: "var(--text-3)", fontStyle: "italic", marginTop: 2 }}>Grade incompleta — interpretação bloqueada</div>}
          </div>
        </td>
      );
      const condResumo = (titulo, g) => {
        const total = g.Adequado.length + g.Alerta.length + g["Déficit"].length;
        return (
          <div style={{ marginBottom: 14 }}>
            <p style={{ margin: "0 0 6px", fontSize: 13.5, fontWeight: 800 }}>{titulo}: {g.Adequado.length} Adequado · {g.Alerta.length} Alerta · {g["Déficit"].length} Déficit{g.sem.length ? " · " + g.sem.length + " sem norma" : ""}</p>
            {total > 0 && (
              <div style={{ display: "flex", height: 12, borderRadius: 6, overflow: "hidden", border: "1px solid var(--border)" }}>
                {FAIXAS_ORD.map((f) => g[f].length > 0 ? <div key={f} style={{ flex: g[f].length, background: CLS_COL[f] }} title={f} /> : null)}
              </div>
            )}
            <div style={{ marginTop: 6 }}>
              {FAIXAS_ORD.filter((f) => g[f].length > 0).map((f) => (
                <div key={f} style={{ display: "flex", alignItems: "flex-start", gap: 7, fontSize: 12.5, marginBottom: 2 }}>
                  <i style={{ width: 10, height: 10, borderRadius: 3, background: CLS_COL[f], display: "inline-block", marginTop: 3, flex: "none" }} />
                  <span><b style={{ color: CLS_COL[f] }}>{f}</b>: {g[f].map((x) => MEDIDA_L[x.med]).join(", ")}</span>
                </div>
              ))}
              {g.sem.length > 0 && <p className="help-text" style={{ margin: "2px 0 0" }}>Sem norma cadastrada: {g.sem.map((x) => MEDIDA_L[x.med]).join(", ")}.</p>}
            </div>
          </div>
        );
      };
      const alteradasNode = (titulo, g) => {
        const total = g.Alerta.length + g["Déficit"].length;
        return (
          <div style={{ marginBottom: 8 }}>
            <p style={{ margin: "0 0 4px", fontSize: 13, fontWeight: 800 }}>{titulo}</p>
            {total === 0
              ? <p className="help-text" style={{ margin: 0 }}>Todas as medidas dentro do esperado para a idade e tipo de escola.</p>
              : <ul style={{ margin: "0 0 4px", paddingLeft: 18 }}>
                  {g.Alerta.map((x) => <li key={"al" + x.med} style={{ fontSize: 12.5, marginBottom: 3 }}><b style={{ color: CLS_COL.Alerta }}>Alerta</b> — {MEDIDA_L[x.med]}: bruto {x.raw}, {x.faixa || "—"}{x.dir ? " (" + x.dir + ")" : ""}</li>)}
                  {g["Déficit"].map((x) => <li key={"df" + x.med} style={{ fontSize: 12.5, marginBottom: 3 }}><b style={{ color: CLS_COL["Déficit"] }}>Déficit</b> — {MEDIDA_L[x.med]}: bruto {x.raw}, {x.faixa || "—"}{x.dir ? " (" + x.dir + ")" : ""}</li>)}
                </ul>}
          </div>
        );
      };
      const attnBox = (txt) => (
        <div style={{ display: "flex", alignItems: "flex-start", gap: 7, background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 10, padding: "8px 11px", marginTop: 8 }}>
          <span style={{ fontSize: 14 }}>⚠️</span>
          <p style={{ margin: 0, fontSize: 12, color: "var(--accent-strong)", fontWeight: 700, lineHeight: 1.5 }}>{txt}</p>
        </div>
      );
      return (
        <>
          {/* Aviso de validação (somente via clínica) */}
          <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 12, padding: "10px 13px", marginBottom: 12 }}>
            <span style={{ fontSize: 15 }}>⚠️</span>
            <p style={{ margin: 0, fontSize: 12.5, color: "var(--accent-strong)", fontWeight: 700, lineHeight: 1.5 }}>Normas em fase de validação — confira a classificação com as tabelas do manual.</p>
          </div>

          {/* Examinador */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="users" size={18} /></span> Cabeçalho do laudo</div>
            <div className="fld" style={{ marginBottom: 0 }}><label>Examinador(a)</label>
              <input className="ctrl" value={dados.examinador || ""} onChange={(e) => setDadosField("examinador", e.target.value)} disabled={readonly} placeholder="Nome do profissional responsável" />
            </div>
          </div>

          {/* Tabela principal — duas condições */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="chart" size={18} /></span> Resultados por condição</div>
            <div style={{ overflowX: "auto" }}>
              <table className="hay-sheet" style={{ minWidth: 480 }}>
                <thead><tr><th>Medida</th><th>Dois segundos (2 s)</th><th>Um segundo (1 s)</th></tr></thead>
                <tbody>
                  {MEDIDAS_ORD.map((med, i) => (
                    <tr key={med}>
                      <td style={{ verticalAlign: "top", fontWeight: 700 }}>{MEDIDA_L[med]}</td>
                      {condCell(syn.a.rows[i], syn.a.completa)}
                      {condCell(syn.b.rows[i], syn.b.completa)}
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            {(!syn.a.completa || !syn.b.completa) && <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "var(--surface-2)", border: "1px dashed var(--border-2)", borderRadius: 10, padding: "10px 13px", marginTop: 10 }}>
              <span style={{ fontSize: 15 }}>📝</span>
              <p style={{ margin: 0, fontSize: 12.5, color: "var(--text-2)", fontWeight: 600, lineHeight: 1.5 }}>Interpretação normativa bloqueada em uma condição com grade incompleta{!syn.a.completa ? " · Dois segundos: " + syn.a.preench + " de " + syn.a.est : ""}{!syn.b.completa ? " · Um segundo: " + syn.b.preench + " de " + syn.b.est : ""}. Volte às etapas e conclua o registro (marque cada estímulo, inclusive Omissão).</p>
            </div>}
            <p className="help-text" style={{ marginBottom: 0, marginTop: 10 }}>
              Escore Z = (escore da criança − média do grupo normativo) ÷ desvio-padrão, por idade × tipo de escola (Tabelas 2–15).
              Classificação: <b style={{ color: "#DC2626" }}>Déficit</b> (Z ≤ −1,5 em acertos ou Z ≥ +1,5 nos erros, ou percentil ≤ P5) · <b style={{ color: "#F59E0B" }}>Alerta</b> (abaixo do P25) · <b style={{ color: "#16A34A" }}>Adequado</b>. Nos erros, valor maior = pior. Percentis por faixa (sem interpolação).
            </p>
            {(!syn.a.hasNorma || !syn.b.hasNorma) && <p className="help-text" style={{ marginBottom: 0, color: "var(--accent-strong)" }}>Sem norma cadastrada para {idade} anos / {escolaTipo === "publica" ? "pública" : "privada"} em uma das condições.</p>}
          </div>

          {/* Análise qualitativa */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="sparkles" size={18} /></span> Análise qualitativa</div>
            <div style={{ overflowX: "auto" }}>
              <table className="hay-sheet" style={{ minWidth: 380 }}>
                <thead><tr><th>Estratégia</th><th>Dois segundos</th><th>Um segundo</th></tr></thead>
                <tbody>
                  <tr><td>Sequências ímpares</td><td className="c">{syn.a.c.imparSeq}</td><td className="c">{syn.b.c.imparSeq}</td></tr>
                  <tr><td>Sequências pares</td><td className="c">{syn.a.c.parSeq}</td><td className="c">{syn.b.c.parSeq}</td></tr>
                  <tr><td>Sequências prontas/repetidas</td><td className="c">{syn.a.c.prontaSeq}</td><td className="c">{syn.b.c.prontaSeq}</td></tr>
                </tbody>
              </table>
            </div>
            <p className="help-text" style={{ marginBottom: 0, marginTop: 10 }}>Não são consideradas erro — trata-se de estratégia para manter a aleatoriedade da evocação.</p>
          </div>

          {/* Glossário de interpretação (accordion) */}
          <div className="form-card" style={{ padding: 0, overflow: "hidden" }}>
            {accordion(glossOpen, () => setGlossOpen((o) => !o), "fileText", "Glossário de interpretação (manual)")}
            {glossOpen && (
              <div style={{ padding: "0 16px 16px" }}>
                {GLOSSARIO.map((g, i) => (
                  <p key={i} style={{ margin: "0 0 10px", fontSize: 13, lineHeight: 1.6, color: "var(--text-2)" }}><b style={{ color: "var(--text)" }}>{g.t}:</b> {g.d}</p>
                ))}
                <p className="help-text" style={{ marginBottom: 0, fontStyle: "italic" }}>Interpretação sugerida com base no manual. O julgamento clínico é do profissional.</p>
              </div>
            )}
          </div>

          {/* Perfil Geral no Teste — consolida A e B (sem escore composto) */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="chart" size={18} /></span> Perfil Geral no Teste</div>
            <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 10, padding: "9px 12px", marginBottom: 14 }}>
              <span style={{ fontSize: 14 }}>ℹ️</span>
              <p style={{ margin: 0, fontSize: 12.5, color: "var(--text-2)", fontWeight: 600, lineHeight: 1.5 }}>O GAN não possui escore total. Cada medida é interpretada isoladamente em relação à norma de idade e tipo de escola.</p>
            </div>

            {(!syn.a.completa || !syn.b.completa) && <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "#FEE2E2", border: "1px solid #DC2626", borderRadius: 10, padding: "9px 12px", marginBottom: 12 }}>
              <span style={{ fontSize: 14 }}>⚠️</span>
              <p style={{ margin: 0, fontSize: 12.5, color: "#991B1B", fontWeight: 700, lineHeight: 1.5 }}>Perfil parcial: há condição com grade incompleta{!syn.a.completa ? " (Dois segundos: " + syn.a.preench + "/" + syn.a.est + ")" : ""}{!syn.b.completa ? " (Um segundo: " + syn.b.preench + "/" + syn.b.est + ")" : ""}. A classificação por faixa fica indisponível até o registro ser concluído.</p>
            </div>}

            {/* 1. Distribuição por classificação */}
            <p className="section-label" style={{ marginTop: 0 }}>DISTRIBUIÇÃO DAS MEDIDAS POR CLASSIFICAÇÃO</p>
            {syn.a.completa ? condResumo("Dois segundos", perfil.a) : <p className="help-text" style={{ marginTop: 0 }}><b>Dois segundos:</b> grade incompleta ({syn.a.preench} de {syn.a.est}) — distribuição indisponível.</p>}
            {syn.b.completa ? condResumo("Um segundo", perfil.b) : <p className="help-text" style={{ marginTop: 0 }}><b>Um segundo:</b> grade incompleta ({syn.b.preench} de {syn.b.est}) — distribuição indisponível.</p>}

            {/* 2. Medidas alteradas */}
            <p className="section-label">MEDIDAS ALTERADAS (ALERTA OU DÉFICIT)</p>
            {syn.a.completa ? alteradasNode("Dois segundos", perfil.a) : <p className="help-text" style={{ marginTop: 0 }}><b>Dois segundos:</b> registro incompleto — sem classificação.</p>}
            {syn.b.completa ? alteradasNode("Um segundo", perfil.b) : <p className="help-text" style={{ marginTop: 0 }}><b>Um segundo:</b> registro incompleto — sem classificação.</p>}

            {/* 3. Comparação A × B */}
            <p className="section-label">COMPARAÇÃO ENTRE AS CONDIÇÕES (A × B)</p>
            <div style={{ background: "var(--primary-soft)", border: "1px solid var(--primary)", borderRadius: 12, padding: "12px 14px", marginBottom: 12 }}>
              <div className="hay-scores">
                <div className="hsc"><span>Acertos — dois segundos</span><b>{perfil.acA}</b></div>
                <div className="hsc"><span>Acertos — um segundo</span><b>{perfil.acB}</b></div>
                <div className="hsc"><span>Diferença (2 s − 1 s)</span><b>{perfil.diffAC > 0 ? "+" : ""}{perfil.diffAC}</b></div>
              </div>
              <div className="hay-scores" style={{ marginTop: 8 }}>
                <div className="hsc"><span>Total de erros — dois segundos</span><b>{perfil.errA}</b></div>
                <div className="hsc"><span>Total de erros — um segundo</span><b>{perfil.errB}</b></div>
              </div>
              <p className="help-text" style={{ marginTop: 10, marginBottom: 0 }}>A criança tende a apresentar melhor desempenho na versão de dois segundos, pela menor pressão de tempo. A comparação entre as duas permite analisar se há dificuldade com o aumento da demanda de recursos executivos.</p>
              {perfil.quedaExpressiva && attnBox("Queda expressiva de acertos de dois para um segundo — ponto de atenção a considerar na análise (não é conclusão).")}
              {perfil.maisErros1s && attnBox("Aumento de erros na condição de um segundo em relação à de dois segundos — ponto de atenção (não é conclusão).")}
            </div>

            {/* 4. Leitura por componente executivo */}
            <p className="section-label">LEITURA POR COMPONENTE EXECUTIVO (HIPÓTESES)</p>
            {perfil.componentes.length === 0
              ? <p className="help-text" style={{ marginTop: 0 }}>Nenhuma medida alterada — sem hipóteses de componente executivo a destacar.</p>
              : <>
                  <ul style={{ margin: "0 0 6px", paddingLeft: 18 }}>
                    {perfil.componentes.map((comp) => (
                      <li key={comp.nome} style={{ fontSize: 13, marginBottom: 5, lineHeight: 1.5 }}>
                        O desempenho <b>pode estar associado a</b> {comp.nome.toLowerCase()} <span style={{ color: "var(--text-3)" }}>(medidas: {comp.meds.map((m) => MEDIDA_L[m]).join(", ")})</span>.
                      </li>
                    ))}
                  </ul>
                  <p className="help-text" style={{ margin: 0 }}>Hipóteses descritivas com base no manual. Não indicam condição ou transtorno.</p>
                </>}

            {/* 5. Aviso fixo */}
            <div style={{ display: "flex", alignItems: "flex-start", gap: 8, background: "var(--accent-soft)", border: "1px solid var(--accent)", borderRadius: 10, padding: "9px 12px", marginTop: 14 }}>
              <span style={{ fontSize: 14 }}>⚠️</span>
              <p style={{ margin: 0, fontSize: 12.5, color: "var(--accent-strong)", fontWeight: 700, lineHeight: 1.5 }}>Instrumento experimental, indicado para compor bateria neuropsicológica junto a outros testes. Não constitui diagnóstico.</p>
            </div>
          </div>

          {/* Conclusão clínica (texto livre) */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="clipboardList" size={18} /></span> Conclusão do profissional</div>
            <p className="help-text" style={{ marginTop: 0 }}>Único campo conclusivo da tela. Registre sua interpretação, hipóteses e encaminhamentos. Nenhum texto é gerado automaticamente. Assinado com o nome do examinador na exportação.</p>
            <textarea className="ctrl" style={{ minHeight: 140, resize: "vertical" }} value={dados.conclusao || ""} onChange={(e) => setDadosField("conclusao", e.target.value)} disabled={readonly}
              placeholder="Integre os achados das duas condições, a comparação e a análise qualitativa com a história clínica e demais instrumentos…" />
          </div>

          {/* Exportação */}
          <div className="form-card">
            <div className="sec-head"><span className="si"><Icon name="fileText" size={18} /></span> Exportar em PDF</div>
            <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
              <button type="button" className="btn btn-primary btn-sm" onClick={() => exportSintese("clinica")}><Icon name="fileText" size={16} /> Via clínica (prontuário)</button>
              <button type="button" className="btn btn-ghost btn-sm" onClick={() => exportSintese("familia")}><Icon name="users" size={16} /> Via para a família</button>
            </div>
            <p className="help-text" style={{ marginBottom: 0, marginTop: 10 }}>A via da família é simplificada (uma página, sem escore Z, percentil ou jargão) e não exibe o aviso de validação.</p>
          </div>
        </>
      );
    }

    return (
      <div className="form-wrap">
        <div className="hay-top">
          <button className="pt-back" onClick={() => guardedLeave(onBack)}><Icon name="chevL" size={16} /> Voltar para avaliações</button>
        </div>
        <div className="page-head" style={{ marginBottom: 14 }}>
          <div><h1><Icon name="activity" size={25} /> GAN — Geração Aleatória de Números <span style={{ fontSize: 15, fontWeight: 700, color: "var(--text-3)" }}>(6 a 12 anos)</span></h1><p className="sub">Funções executivas por evocação aleatória de números ao ritmo de estímulos sonoros.</p></div>
        </div>

        {(view === "a" || view === "b" || view === "menu" || view === "sintese") && (
          <>
            <div className="hay-sheet-id" style={{ marginBottom: 6 }}>
              <span><b>Nome:</b> {patient ? patient.name : "—"}</span>
              <span><b>Idade:</b> {idade} anos</span>
              <span><b>Escola:</b> {escolaTipo === "publica" ? "Pública" : "Privada"}</span>
              <span><b>Data de aplicação:</b> {date}</span>
              {(dados.examinador || "") && <span><b>Examinador:</b> {dados.examinador}</span>}
              {readonly && <span style={{ color: "var(--text-3)" }}><b>Modo consulta</b></span>}
            </div>
            <p style={{ fontSize: 11.5, color: "var(--text-3)", margin: "0 0 14px", fontStyle: "italic" }}>Geração Aleatória de Números (GAN) — Protocolo de Registro (Jacobsen et al., 2016)</p>
          </>
        )}

        {/* ---------- Identificação ---------- */}
        {view === "ident" && (
          <>
            <div className="form-card">
              <div className="sec-head"><span className="si"><Icon name="users" size={18} /></span> Identificação</div>
              <div className="fld"><label>Paciente <span className="req">*</span></label><window.PatientPicker value={pid} onChange={pickPatient} /></div>
              <div className="grid-2">
                <div className="fld"><label>Idade <span className="req">*</span></label>
                  <select className="ctrl" value={idade} onChange={(e) => setIdade(e.target.value)}>
                    <option value="">Selecione…</option>{[6, 7, 8, 9, 10, 11, 12].map((a) => <option key={a} value={a}>{a} anos</option>)}
                  </select>
                </div>
                <div className="fld"><label>Tipo de escola <span className="req">*</span></label>
                  <select className="ctrl" value={escolaTipo} onChange={(e) => setEscolaTipo(e.target.value)}>
                    <option value="">Selecione…</option><option value="privada">Privada</option><option value="publica">Pública</option>
                  </select>
                </div>
              </div>
              <div className="fld" style={{ marginBottom: 0 }}><label>Data</label><input className="ctrl" type="date" value={date} onChange={(e) => setDate(e.target.value)} /></div>
              <p className="help-text" style={{ marginBottom: 0 }}>Idade e tipo de escola são necessários para as normas (escore Z e percentis), que serão adicionadas numa próxima etapa.</p>
            </div>

            {err && <p className="form-err" style={{ display: "flex", alignItems: "center", gap: 7, color: "#EF4444", fontWeight: 700, fontSize: 13.5, margin: "12px 0 0" }}><Icon name="x" size={15} /> {err}</p>}

            <div style={{ textAlign: "center", marginTop: 10 }}>
              <button type="button" onClick={() => setStorageMgr(true)} style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-3)", fontSize: 12.5, fontWeight: 700, display: "inline-flex", alignItems: "center", gap: 6 }}><Icon name="headset" size={14} /> Liberar espaço (áudios armazenados)</button>
            </div>

            <div className="hay-nav">
              <button className="btn btn-ghost btn-sm" onClick={onBack}><Icon name="chevL" size={16} /> Cancelar</button>
              <button className="btn btn-primary btn-sm" onClick={novaAvaliacao} disabled={!pid}><Icon name="plus" size={16} /> Iniciar nova avaliação</button>
            </div>

            {pid && listaAvals.length > 0 && (
              <>
                <p className="section-label">AVALIAÇÕES DESTA CRIANÇA</p>
                {listaAvals.map((a) => {
                  const done = PARTES.filter((pp) => a.status && a.status[pp.key] === "concluida").length;
                  return (
                    <div className="team-row" key={a.id}>
                      <span className="ic-icon" style={{ width: 38, height: 38, flex: "none" }}><Icon name="activity" size={18} /></span>
                      <div className="tr-info" style={{ flex: 1 }}>
                        <b>{a.patientName || "—"} {a.finalizada && <span className="tag ok" style={{ marginLeft: 4, fontSize: 10.5, padding: "1px 7px" }}><i className="d"></i>Finalizada</span>}</b>
                        <span>{a.date} · {a.idade} anos · {done}/{PARTES.length} partes{a.finalizada ? "" : " · em andamento"}</span>
                      </div>
                      {a.finalizada
                        ? <button className="btn btn-ghost btn-sm" onClick={() => abrirAval(a, true)}><Icon name="eye" size={15} /> Ver</button>
                        : <button className="btn btn-primary btn-sm" onClick={() => abrirAval(a, false)}><Icon name="arrowRight" size={15} /> Retomar</button>}
                    </div>
                  );
                })}
                <div style={{ marginTop: 14, textAlign: "center" }}>
                  <button type="button" onClick={() => setShowClear(true)} style={{ background: "none", border: "none", cursor: "pointer", color: "#DC2626", fontSize: 12.5, fontWeight: 700, display: "inline-flex", alignItems: "center", gap: 6 }}><Icon name="x" size={14} /> Apagar as avaliações de {patient ? patient.name : "esta criança"}</button>
                </div>
              </>
            )}
          </>
        )}

        {/* ---------- Menu de etapas ---------- */}
        {view === "menu" && (
          <>
            <p className="section-label">ETAPAS DO TESTE — A → B</p>
            <div>
              {PARTES.map((pt, i) => {
                const st = STATUS[status[pt.key]];
                return (
                  <React.Fragment key={pt.key}>
                    <button type="button" className="instr-card" onClick={() => { setView(pt.key); goTop(); }} style={{ cursor: "pointer", textAlign: "left", width: "100%", marginBottom: 0 }}>
                      <div className="ic-head" style={{ marginBottom: 0, width: "100%", alignItems: "center" }}>
                        <span className="ic-icon" style={{ position: "relative" }}><Icon name="activity" size={22} /><span style={{ position: "absolute", top: -7, left: -7, width: 20, height: 20, borderRadius: "50%", background: "var(--primary)", color: "#fff", fontSize: 11, fontWeight: 800, display: "grid", placeItems: "center", border: "2px solid var(--surface)" }}>{pt.n}</span></span>
                        <div style={{ flex: 1, minWidth: 0 }}><h4 style={{ marginBottom: 2 }}>{pt.titulo}</h4><p className="desc" style={{ margin: 0 }}>{pt.desc}</p></div>
                        <span className={"tag " + st.tag} style={{ flex: "none" }}><i className="d"></i>{st.l}</span>
                      </div>
                    </button>
                    {i < PARTES.length - 1 && <div style={{ display: "grid", placeItems: "center", color: "var(--text-3)", margin: "6px 0" }}><Icon name="chevDown" size={20} /></div>}
                  </React.Fragment>
                );
              })}
            </div>

            {(() => {
              const pronta = status.a === "concluida" && status.b === "concluida";
              return (
                <>
                  <div style={{ display: "grid", placeItems: "center", color: "var(--text-3)", margin: "6px 0" }}><Icon name="chevDown" size={20} /></div>
                  <button type="button" className="instr-card" disabled={!pronta} onClick={() => { if (pronta) { setView("sintese"); goTop(); } }}
                    style={{ cursor: pronta ? "pointer" : "not-allowed", textAlign: "left", width: "100%", marginBottom: 0, opacity: pronta ? 1 : 0.55 }}>
                    <div className="ic-head" style={{ marginBottom: 0, width: "100%", alignItems: "center" }}>
                      <span className="ic-icon"><Icon name="chart" size={22} /></span>
                      <div style={{ flex: 1, minWidth: 0 }}><h4 style={{ marginBottom: 2 }}>Síntese dos Resultados</h4><p className="desc" style={{ margin: 0 }}>{pronta ? "Tabela comparativa, interpretação normativa e exportação em PDF (via clínica e via família)." : "Disponível após concluir as Partes A e B."}</p></div>
                      <span className={"tag " + (pronta ? "ok" : "muted")} style={{ flex: "none" }}><i className="d"></i>{pronta ? "Disponível" : "Bloqueada"}</span>
                    </div>
                  </button>
                </>
              );
            })()}

            <div className="hay-nav" style={{ marginTop: 16 }}>
              <button className="btn btn-ghost btn-sm" onClick={() => guardedLeave(() => resetTudo())}><Icon name="chevL" size={16} /> Voltar aos dados</button>
              {onOpenPatient && pid && <button className="btn btn-ghost btn-sm" onClick={() => onOpenPatient(pid)}><Icon name="users" size={16} /> Ver no prontuário</button>}
            </div>
          </>
        )}

        {/* ---------- Partes ---------- */}
        {(view === "a" || view === "b") && (
          <>
            <div style={{ marginBottom: 12 }}><h2 style={{ fontSize: 17, fontWeight: 800, margin: 0, display: "flex", alignItems: "center", gap: 8 }}><Icon name="activity" size={20} /> {CFG[view].titulo}</h2></div>
            {renderParte(view)}

            {readonly ? (
              <div className="hay-nav" style={{ flexWrap: "wrap", gap: 10 }}>
                <button className="btn btn-ghost btn-sm" onClick={() => { setView("menu"); goTop(); }}><Icon name="chevL" size={16} /> Voltar às etapas</button>
                <span style={{ marginLeft: "auto", fontSize: 12.5, fontWeight: 700, color: "var(--text-3)" }}><Icon name="eye" size={14} /> Modo consulta</span>
              </div>
            ) : (
              <div className="hay-nav" style={{ flexWrap: "wrap", gap: 10 }}>
                <button className="btn btn-ghost btn-sm" onClick={() => guardedLeave(() => { setView("menu"); goTop(); })}><Icon name="chevL" size={16} /> Voltar às etapas</button>
                <label style={{ display: "inline-flex", alignItems: "center", gap: 7, fontSize: 13.5, fontWeight: 700, color: "var(--text-2)", cursor: "pointer" }}>
                  <input type="checkbox" checked={concluida[view]} onChange={() => toggleConcluida(view)} /> Marcar Parte {view.toUpperCase()} como concluída
                </label>
                <div style={{ display: "flex", alignItems: "center", gap: 10, marginLeft: "auto", flexWrap: "wrap" }}>
                  {dirty ? <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12.5, fontWeight: 700, color: "var(--accent-strong)" }}><span style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--accent)" }} /> Alterações não salvas</span> : (savedAt && <span style={{ fontSize: 12.5, fontWeight: 700, color: "var(--primary)" }}>Salvo ✓ {savedAt}</span>)}
                  <button className="btn btn-primary btn-sm" onClick={salvar} disabled={saving || !dirty}><Icon name="clipboardCheck" size={16} /> {saving ? "Salvando…" : "Salvar avaliação"}</button>
                  {concluida[view] && (PARTES.find((p) => p.key === view).next
                    ? <button className="btn btn-primary btn-sm" onClick={() => salvarEAvancar(PARTES.find((p) => p.key === view).next)} disabled={saving}>{saving ? "Salvando…" : "Ir para Parte B"} <Icon name="arrowRight" size={16} /></button>
                    : <button className="btn btn-primary btn-sm" onClick={() => salvarEAvancar("sintese")} disabled={saving}>{saving ? "Salvando…" : "Ver conclusão do teste"} <Icon name="arrowRight" size={16} /></button>)}
                </div>
              </div>
            )}
          </>
        )}

        {/* ---------- Síntese dos Resultados ---------- */}
        {view === "sintese" && (
          <>
            <div style={{ marginBottom: 12 }}><h2 style={{ fontSize: 17, fontWeight: 800, margin: 0, display: "flex", alignItems: "center", gap: 8 }}><Icon name="chart" size={20} /> Síntese dos Resultados</h2></div>
            {renderSintese()}

            {readonly ? (
              <div className="hay-nav" style={{ flexWrap: "wrap", gap: 10 }}>
                <button className="btn btn-ghost btn-sm" onClick={() => { setView("menu"); goTop(); }}><Icon name="chevL" size={16} /> Voltar às etapas</button>
                <span style={{ marginLeft: "auto", fontSize: 12.5, fontWeight: 700, color: "var(--text-3)" }}><Icon name="eye" size={14} /> Modo consulta</span>
              </div>
            ) : (
              <div className="hay-nav" style={{ flexWrap: "wrap", gap: 10 }}>
                <button className="btn btn-ghost btn-sm" onClick={() => guardedLeave(() => { setView("menu"); goTop(); })}><Icon name="chevL" size={16} /> Voltar às etapas</button>
                <div style={{ display: "flex", alignItems: "center", gap: 10, marginLeft: "auto", flexWrap: "wrap" }}>
                  {dirty ? <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12.5, fontWeight: 700, color: "var(--accent-strong)" }}><span style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--accent)" }} /> Alterações não salvas</span> : (savedAt && <span style={{ fontSize: 12.5, fontWeight: 700, color: "var(--primary)" }}>Salvo ✓ {savedAt}</span>)}
                  <button className="btn btn-primary btn-sm" onClick={salvar} disabled={saving || !dirty}><Icon name="clipboardCheck" size={16} /> {saving ? "Salvando…" : "Salvar avaliação"}</button>
                  <button className="btn btn-primary btn-sm" onClick={finalizar} disabled={saving}><Icon name="clipboardCheck" size={16} /> {saving ? "Salvando…" : "Finalizar avaliação"}</button>
                </div>
              </div>
            )}
          </>
        )}

        {leaveGuard && (
          <div className="modal-scrim" onClick={() => setLeaveGuard(null)}>
            <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 440 }}>
              <div className="modal-head"><div><h3>Alterações não salvas</h3><p>Há alterações não salvas nesta avaliação.</p></div><button className="x" onClick={() => setLeaveGuard(null)} aria-label="Fechar"><Icon name="x" size={18} /></button></div>
              <div className="modal-body">
                <p className="help-text" style={{ marginTop: 0 }}>Deseja salvar antes de sair?</p>
                <div className="form-actions" style={{ marginTop: 14, display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
                  <button className="btn btn-ghost btn-sm" onClick={() => setLeaveGuard(null)}>Cancelar</button>
                  <button className="btn btn-ghost btn-sm" onClick={discardAndLeave}>Sair sem salvar</button>
                  <button className="btn btn-primary btn-sm" onClick={saveAndLeave} disabled={saving}><Icon name="clipboardCheck" size={15} /> {saving ? "Salvando…" : "Salvar e sair"}</button>
                </div>
              </div>
            </div>
          </div>
        )}
        {showClear && (
          <div className="modal-scrim" onClick={() => setShowClear(false)}>
            <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 440 }}>
              <div className="modal-head"><div><h3>Apagar as avaliações desta criança?</h3><p>Esta ação não pode ser desfeita.</p></div><button className="x" onClick={() => setShowClear(false)} aria-label="Fechar"><Icon name="x" size={18} /></button></div>
              <div className="modal-body">
                <p className="help-text" style={{ marginTop: 0 }}>Todas as avaliações GAN de <b>{patient ? patient.name : "esta criança"}</b> serão apagadas permanentemente. As demais crianças não são afetadas.</p>
                <div className="form-actions" style={{ marginTop: 14, display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
                  <button className="btn btn-ghost btn-sm" onClick={() => setShowClear(false)}>Cancelar</button>
                  <button className="btn btn-sm" style={{ background: "#DC2626", color: "#fff" }} onClick={apagarDoPaciente}><Icon name="x" size={15} /> Apagar</button>
                </div>
              </div>
            </div>
          </div>
        )}
        {storageMgr && (() => {
          const lista = storageList();
          const total = lista.reduce((s, x) => s + x.bytes, 0);
          return (
            <div className="modal-scrim" onClick={() => setStorageMgr(false)}>
              <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 560 }}>
                <div className="modal-head"><div><h3>Liberar espaço</h3><p>Áudios GAN neste dispositivo — {fmtBytes(total)} no total.</p></div><button className="x" onClick={() => setStorageMgr(false)} aria-label="Fechar"><Icon name="x" size={18} /></button></div>
                <div className="modal-body">
                  <p className="help-text" style={{ marginTop: 0 }}>Só é possível remover áudios de avaliações <b>finalizadas</b> (os escores já estão no prontuário). Enquanto não houver envio ao servidor, a remoção é definitiva — o áudio é apagado apenas deste aparelho.</p>
                  {lista.length === 0 ? <p className="help-text" style={{ marginBottom: 0 }}>Nenhum áudio armazenado localmente.</p> : lista.map((x) => (
                    <div className="team-row" key={x.id}>
                      <span className="ic-icon" style={{ width: 38, height: 38, flex: "none" }}><Icon name="headset" size={18} /></span>
                      <div className="tr-info" style={{ flex: 1 }}>
                        <b>{(x.a.patientName || "—")} {x.a.finalizada ? <span className="tag ok" style={{ marginLeft: 4, fontSize: 10.5, padding: "1px 7px" }}><i className="d"></i>Finalizada</span> : <span className="tag warn" style={{ marginLeft: 4, fontSize: 10.5, padding: "1px 7px" }}><i className="d"></i>Em andamento</span>}</b>
                        <span>{x.a.date} · {x.a.idade} anos · {fmtBytes(x.bytes)}</span>
                      </div>
                      {x.a.finalizada
                        ? <button className="btn btn-sm" style={{ background: "#DC2626", color: "#fff" }} onClick={() => limparAudio(x.id)}><Icon name="x" size={15} /> Apagar áudio</button>
                        : <span style={{ fontSize: 11.5, color: "var(--text-3)", fontWeight: 700 }}>não removível</span>}
                    </div>
                  ))}
                  <div className="form-actions" style={{ marginTop: 14, display: "flex", justifyContent: "flex-end" }}>
                    <button className="btn btn-ghost btn-sm" onClick={() => setStorageMgr(false)}>Fechar</button>
                  </div>
                </div>
              </div>
            </div>
          );
        })()}
        {toast && (
          <div style={{ position: "fixed", left: "50%", bottom: 22, transform: "translateX(-50%)", zIndex: 90, display: "flex", alignItems: "center", gap: 9, padding: "11px 16px", borderRadius: 12, boxShadow: "var(--shadow-lg)", fontSize: 13.5, fontWeight: 700, color: "#fff", background: toast.kind === "err" ? "#DC2626" : "var(--primary)" }}>
            <Icon name={toast.kind === "err" ? "x" : "clipboardCheck"} size={16} /> {toast.text}
          </div>
        )}
      </div>
    );
  }

  // helper usado no resumo do prontuário (fora do componente)
  function applyOverride(auto, override) { return auto.map((t, i) => (override && override[i]) ? override[i] : t); }

  // ---------------- Síntese: textos fixos + geração de PDF (via clínica / família) ----------------
  const GLOSSARIO = [
    { t: "Total de acertos", d: "representa o quanto a criança conseguiu manter a aleatoriedade na execução da tarefa, o que demanda memória de trabalho (manter em mente as instruções e os números já evocados enquanto planeja os seguintes) e inibição (evitar a tendência a falar sequências)." },
    { t: "Intrusões", d: "podem estar associadas a dificuldades de controle inibitório, de automonitoramento, de adesão a regras e de memória de trabalho." },
    { t: "Omissões", d: "podem estar relacionadas a dificuldades de atenção, iniciação, memória de trabalho, planejamento e/ou velocidade de processamento." },
    { t: "Perseverações e sequências diretas e indiretas", d: "podem sugerir dificuldades de inibição e de memória de trabalho." },
  ];
  const MEDIDA_FAM = { AC: "Acertos (números ditos ao acaso)", IN: "Números fora de 1 a 10", OM: "Momentos sem resposta", PE: "Números repetidos", SD: "Sequências crescentes (ex.: 1-2-3)", SI: "Sequências decrescentes (ex.: 9-8-7)" };
  const FAM_LBL = { "Adequado": { l: "Esperado", col: "#15803D" }, "Alerta": { l: "Atenção", col: "#B45309" }, "Déficit": { l: "Abaixo do esperado", col: "#B91C1C" } };
  function famDe(cls) { return (cls && FAM_LBL[cls.l]) || { l: "—", col: "#666" }; }
  function piorCls(ca, cb) { const rk = (c) => c ? (c.l === "Déficit" ? 2 : c.l === "Alerta" ? 1 : 0) : -1; const ra = rk(ca), rb = rk(cb); if (ra < 0 && rb < 0) return null; return rb > ra ? cb : ca; }
  function esc(s) { return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
  function zTxt(z) { return z == null ? "—" : (z > 0 ? "+" : "") + z.toFixed(2); }
  function clsTxt(cls) { return cls ? `<span style="color:${cls.col};font-weight:700">${esc(cls.l)}</span>` : "—"; }

  // Componentes executivos que o manual associa a cada medida (para leitura por hipótese).
  const COMPONENTES = [
    { nome: "Memória de trabalho", meds: ["AC", "IN", "OM", "PE", "SD", "SI"] },
    { nome: "Controle inibitório", meds: ["AC", "IN", "PE", "SD", "SI"] },
    { nome: "Atenção / velocidade de processamento", meds: ["OM"] },
    { nome: "Automonitoramento e adesão a regras", meds: ["IN"] },
  ];
  const CLS_COL = { "Adequado": "#16A34A", "Alerta": "#F59E0B", "Déficit": "#DC2626" };
  const FAIXAS_ORD = ["Adequado", "Alerta", "Déficit"];

  function perfilCond(cond) {
    const g = { Adequado: [], Alerta: [], "Déficit": [], sem: [] };
    MEDIDAS_ORD.forEach((med, i) => {
      const r = cond.rows[i];
      if (!r.cls) g.sem.push({ med, raw: r.raw, faixa: r.faixa, dir: r.dir });
      else (g[r.cls.l] || g.sem).push({ med, raw: r.raw, faixa: r.faixa, dir: r.dir });
    });
    return g;
  }
  // Consolida A e B SEM escore composto — apenas contagens, medidas alteradas,
  // comparação de acertos/erros e agrupamento por componente executivo (hipóteses).
  function computePerfil(syn) {
    const a = perfilCond(syn.a), b = perfilCond(syn.b);
    const alt = {};
    MEDIDAS_ORD.forEach((med, i) => {
      const ca = syn.a.rows[i].cls, cb = syn.b.rows[i].cls;
      if ((ca && ca.l !== "Adequado") || (cb && cb.l !== "Adequado")) alt[med] = true;
    });
    const errSum = (c) => c.intrusao + c.omissao + c.perseveracao + c.sdErr + c.siErr;
    const errA = errSum(syn.a.c), errB = errSum(syn.b.c);
    const acA = syn.a.c.acerto, acB = syn.b.c.acerto;
    const diffAC = acA - acB;
    const dropPct = acA > 0 ? (acA - acB) / acA : 0;
    const quedaExpressiva = acB < acA && dropPct >= 0.25;
    const maisErros1s = errB > errA;
    const componentes = COMPONENTES.map((comp) => ({ nome: comp.nome, meds: comp.meds.filter((m) => alt[m]) })).filter((c) => c.meds.length > 0);
    return { a, b, alteradas: Object.keys(alt), componentes, errA, errB, acA, acB, diffAC, quedaExpressiva, maisErros1s };
  }

  function buildSinteseClinicaHTML(ctx) {
    const { patientName, idade, escolaLabel, date, examinador, conclusao, syn } = ctx;
    const condHtml = (row, completa) => completa
      ? `Bruto: <b>${esc(row.raw)}</b> · Z: ${zTxt(row.z)}<br/>Percentil: ${esc(row.faixa || "—")}${row.dir ? ` <span style="color:#888">(${esc(row.dir)})</span>` : ""}<br/>${clsTxt(row.cls)}`
      : `Bruto: <b>${esc(row.raw)}</b><br/><i style="color:#888">Grade incompleta — interpretação bloqueada</i>`;
    const mainRows = MEDIDAS_ORD.map((med, i) => `<tr><td><b>${esc(MEDIDA_L[med])}</b></td><td>${condHtml(syn.a.rows[i], syn.a.completa)}</td><td>${condHtml(syn.b.rows[i], syn.b.completa)}</td></tr>`).join("");
    const qualRow = (label, va, vb) => `<tr><td>${esc(label)}</td><td class="c">${esc(va)}</td><td class="c">${esc(vb)}</td></tr>`;
    const gloss = GLOSSARIO.map((g) => `<p class="gl"><b>${esc(g.t)}:</b> ${esc(g.d)}</p>`).join("");
    const conc = conclusao && conclusao.trim() ? esc(conclusao).replace(/\n/g, "<br/>") : "<span style='color:#888'>—</span>";
    // Perfil Geral (sem escore composto)
    const perfil = computePerfil(syn);
    const barHtml = (g) => { const total = g.Adequado.length + g.Alerta.length + g["Déficit"].length; if (!total) return ""; const segs = FAIXAS_ORD.filter((f) => g[f].length > 0).map((f) => `<span style="flex:${g[f].length};background:${CLS_COL[f]}"></span>`).join(""); return `<div class="bar">${segs}</div>`; };
    const contHtml = (g) => `${g.Adequado.length} Adequado · ${g.Alerta.length} Alerta · ${g["Déficit"].length} Déficit${g.sem.length ? ` · ${g.sem.length} sem norma` : ""}`;
    const altHtml = (g, completa) => { if (!completa) return `<p class="note"><i>Registro incompleto — sem classificação.</i></p>`; const arr = g.Alerta.map((x) => ({ ...x, f: "Alerta" })).concat(g["Déficit"].map((x) => ({ ...x, f: "Déficit" }))); if (!arr.length) return `<p class="note">Todas as medidas dentro do esperado para a idade e tipo de escola.</p>`; return `<ul class="pl">${arr.map((x) => `<li><b style="color:${CLS_COL[x.f]}">${x.f}</b> — ${esc(MEDIDA_L[x.med])}: bruto ${esc(x.raw)}, ${esc(x.faixa || "—")}${x.dir ? ` (${esc(x.dir)})` : ""}</li>`).join("")}</ul>`; };
    const compHtml = perfil.componentes.length ? `<ul class="pl">${perfil.componentes.map((c) => `<li>O desempenho <b>pode estar associado a</b> ${esc(c.nome.toLowerCase())} (medidas: ${esc(c.meds.map((m) => MEDIDA_L[m]).join(", "))}).</li>`).join("")}</ul><p class="note"><i>Hipóteses descritivas com base no manual. Não indicam condição ou transtorno.</i></p>` : `<p class="note">Nenhuma medida alterada — sem hipóteses de componente executivo a destacar.</p>`;
    const attn = [];
    if (perfil.quedaExpressiva) attn.push("Queda expressiva de acertos de dois para um segundo — ponto de atenção (não é conclusão).");
    if (perfil.maisErros1s) attn.push("Aumento de erros na condição de um segundo em relação à de dois segundos — ponto de atenção (não é conclusão).");
    const attnHtml = attn.length ? `<br/>${attn.map((t) => "⚠️ " + esc(t)).join("<br/>")}` : "";
    return `<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><title>Síntese dos Resultados — GAN — ${esc(patientName)}</title>
      <style>
        body{font-family:Arial,Helvetica,sans-serif;color:#142826;padding:24px;font-size:12px}
        h1{font-size:16px;text-align:center;margin:0 0 2px}
        .ref{text-align:center;font-size:10.5px;color:#666;margin:0 0 12px;font-style:italic}
        h2{font-size:13px;background:#E9F0EE;padding:6px 8px;border-radius:6px;margin:18px 0 6px}
        .meta{margin:8px 0 4px;line-height:1.6}
        table{width:100%;border-collapse:collapse;margin-top:4px}
        th,td{border:1px solid #B9C9C5;padding:6px 8px;text-align:left;vertical-align:top}
        th{background:#D8E5E2;font-size:11px}
        td.c{text-align:center}
        .warn{background:#FEF3C7;border:1px solid #F59E0B;color:#92400E;padding:8px 12px;border-radius:6px;margin:10px 0;font-weight:700;font-size:11px}
        .note{font-size:11px;color:#444;line-height:1.5;margin:8px 0}
        .gl{font-size:11.5px;line-height:1.55;margin:0 0 8px}
        .conc{border:1px solid #B9C9C5;border-radius:6px;padding:10px 12px;min-height:60px;line-height:1.55}
        .pl{margin:4px 0 8px;padding-left:18px;font-size:11.5px;line-height:1.55}
        .bar{display:flex;height:12px;border-radius:6px;overflow:hidden;border:1px solid #B9C9C5;margin:4px 0 10px}
        .bar span{display:block}
        .sub2{font-weight:700;font-size:11.5px;margin:10px 0 2px}
        .foot{margin-top:16px;font-size:10px;color:#666}
        @media print{.warn,.bar,.bar span{-webkit-print-color-adjust:exact;print-color-adjust:exact}}
      </style></head><body>
      <h1>SÍNTESE DOS RESULTADOS — GAN</h1>
      <p class="ref">Geração Aleatória de Números (GAN) — Jacobsen et al., 2016</p>
      <div class="meta"><b>Criança:</b> ${esc(patientName) || "—"} &nbsp;&nbsp; <b>Idade:</b> ${esc(idade) || "—"}${idade ? " anos" : ""} &nbsp;&nbsp; <b>Escola:</b> ${esc(escolaLabel)}<br/><b>Data de aplicação:</b> ${esc(date)} &nbsp;&nbsp; <b>Examinador(a):</b> ${esc(examinador) || "—"}</div>
      <div class="warn">Normas em fase de validação — confira a classificação com as tabelas do manual.</div>
      <h2>Resultados por condição</h2>
      <table><tr><th>Medida</th><th>Dois segundos (2 s)</th><th>Um segundo (1 s)</th></tr>${mainRows}</table>
      <p class="note">Escore Z = (escore da criança − média do grupo normativo) ÷ desvio-padrão, por idade × tipo de escola (Tabelas 2–15). Classificação: Déficit (Z ≤ −1,5 em acertos ou Z ≥ +1,5 nos erros, ou percentil ≤ P5) · Alerta (abaixo do P25) · Adequado. Nos erros, valor maior = pior. Percentis por faixa, sem interpolação.</p>
      <h2>Análise qualitativa</h2>
      <table><tr><th>Estratégia</th><th>Dois segundos</th><th>Um segundo</th></tr>
      ${qualRow("Sequências ímpares", syn.a.c.imparSeq, syn.b.c.imparSeq)}
      ${qualRow("Sequências pares", syn.a.c.parSeq, syn.b.c.parSeq)}
      ${qualRow("Sequências prontas/repetidas", syn.a.c.prontaSeq, syn.b.c.prontaSeq)}</table>
      <p class="note">Não são consideradas erro — trata-se de estratégia para manter a aleatoriedade da evocação.</p>
      <h2>Perfil Geral no Teste</h2>
      <p class="note">O GAN não possui escore total. Cada medida é interpretada isoladamente em relação à norma de idade e tipo de escola.</p>
      ${(!syn.a.completa || !syn.b.completa) ? `<div class="warn">Perfil parcial: há condição com grade incompleta${!syn.a.completa ? ` (Dois segundos: ${syn.a.preench}/${syn.a.est})` : ""}${!syn.b.completa ? ` (Um segundo: ${syn.b.preench}/${syn.b.est})` : ""}. A classificação por faixa fica indisponível até o registro ser concluído.</div>` : ""}
      <p class="sub2">Distribuição das medidas por classificação</p>
      <p class="note">Dois segundos: ${syn.a.completa ? contHtml(perfil.a) : `grade incompleta (${syn.a.preench}/${syn.a.est}) — distribuição indisponível`}</p>${syn.a.completa ? barHtml(perfil.a) : ""}
      <p class="note">Um segundo: ${syn.b.completa ? contHtml(perfil.b) : `grade incompleta (${syn.b.preench}/${syn.b.est}) — distribuição indisponível`}</p>${syn.b.completa ? barHtml(perfil.b) : ""}
      <p class="sub2">Medidas alteradas — dois segundos</p>${altHtml(perfil.a, syn.a.completa)}
      <p class="sub2">Medidas alteradas — um segundo</p>${altHtml(perfil.b, syn.b.completa)}
      <p class="sub2">Comparação entre as condições (A × B)</p>
      <p class="note">Acertos — dois segundos: <b>${esc(perfil.acA)}</b> · um segundo: <b>${esc(perfil.acB)}</b> · Diferença (2 s − 1 s): <b>${perfil.diffAC > 0 ? "+" : ""}${esc(perfil.diffAC)}</b>. Total de erros — dois segundos: <b>${esc(perfil.errA)}</b> · um segundo: <b>${esc(perfil.errB)}</b>.<br/>A criança tende a apresentar melhor desempenho na versão de dois segundos, pela menor pressão de tempo. A comparação entre as duas permite analisar se há dificuldade com o aumento da demanda de recursos executivos.${attnHtml}</p>
      <p class="sub2">Leitura por componente executivo (hipóteses)</p>${compHtml}
      <div class="warn">Instrumento experimental, indicado para compor bateria neuropsicológica junto a outros testes. Não constitui diagnóstico.</div>
      <h2>Glossário de interpretação (manual)</h2>
      ${gloss}
      <p class="note"><i>Interpretação sugerida com base no manual. O julgamento clínico é do profissional.</i></p>
      <h2>Conclusão clínica</h2>
      <div class="conc">${conc}</div>
      <p class="foot">Documento gerado pelo Educare para uso profissional. A interpretação normativa é de apoio e não substitui o julgamento clínico.</p>
      </body></html>`;
  }

  function buildSinteseFamiliaHTML(ctx) {
    const { patientName, idade, escolaLabel, date, examinador, conclusao, syn } = ctx;
    let cE = 0, cA = 0, cB = 0;
    const rows = MEDIDAS_ORD.map((med, i) => {
      const fam = famDe(piorCls(syn.a.rows[i].cls, syn.b.rows[i].cls));
      if (fam.l === "Esperado") cE++; else if (fam.l === "Atenção") cA++; else if (fam.l === "Abaixo do esperado") cB++;
      return `<tr><td>${esc(MEDIDA_FAM[med])}</td><td class="c">${esc(syn.a.rows[i].raw)}</td><td class="c">${esc(syn.b.rows[i].raw)}</td><td class="c"><b style="color:${fam.col}">${esc(fam.l)}</b></td></tr>`;
    }).join("");
    const pl = (n, s, p) => `${n} ${n === 1 ? s : p}`;
    const resumo = `${pl(cE, "área dentro do esperado", "áreas dentro do esperado")} · ${pl(cA, "em atenção", "em atenção")} · ${pl(cB, "abaixo do esperado", "abaixo do esperado")}`;
    const concFam = conclusao && conclusao.trim() ? `<div class="conc-fam"><b>Observações do profissional</b><p>${esc(conclusao).replace(/\n/g, "<br/>")}</p></div>` : "";
    return `<!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><title>Resultado — ${esc(patientName)}</title>
      <style>
        body{font-family:Arial,Helvetica,sans-serif;color:#142826;padding:28px;font-size:13px;max-width:720px;margin:0 auto}
        h1{font-size:17px;text-align:center;margin:0 0 4px}
        .sub{text-align:center;font-size:12px;color:#555;margin:0 0 16px}
        .meta{margin:6px 0 14px;line-height:1.6}
        .intro{background:#EAF3F1;border-radius:8px;padding:12px 14px;line-height:1.55;margin-bottom:14px}
        table{width:100%;border-collapse:collapse;margin:6px 0 14px}
        th,td{border:1px solid #C7D4D1;padding:8px 10px;text-align:left}
        th{background:#DCEAE7;font-size:12px}
        td.c{text-align:center}
        .legend{font-size:11.5px;color:#555;margin:0 0 16px}
        .resumo{background:#EEF4F2;border-radius:8px;padding:11px 14px;font-size:13px;font-weight:700;margin:0 0 14px}
        .conc-fam{border:1px solid #C7D4D1;border-radius:8px;padding:12px 14px;line-height:1.55;margin:0 0 18px}
        .conc-fam b{display:block;margin-bottom:5px}
        .conc-fam p{margin:0}
        .disc{border:1px solid #E0A800;background:#FFF8E6;border-radius:8px;padding:11px 14px;font-size:12px;line-height:1.55;margin-bottom:26px}
        .sign{margin-top:34px}
        .sline{border-top:1px solid #444;width:280px;margin-top:38px;padding-top:5px;font-size:12px;color:#444}
        @media print{.intro,.disc{-webkit-print-color-adjust:exact;print-color-adjust:exact}}
      </style></head><body>
      <h1>Resultado da avaliação</h1>
      <p class="sub">Geração Aleatória de Números (GAN)</p>
      <div class="meta"><b>Criança:</b> ${esc(patientName) || "—"} &nbsp;&nbsp; <b>Idade:</b> ${esc(idade) || "—"}${idade ? " anos" : ""}<br/><b>Data:</b> ${esc(date)}</div>
      <div class="intro">Esta tarefa avalia a atenção, a memória e o autocontrole da criança por meio de um jogo de dizer números ao acaso, seguindo um ritmo sonoro. A criança realiza o jogo em duas velocidades: mais devagar (a cada 2 segundos) e mais rápido (a cada 1 segundo).</div>
      <table>
        <tr><th>O que foi observado</th><th>Mais devagar</th><th>Mais rápido</th><th>Resultado</th></tr>
        ${rows}
      </table>
      <p class="legend"><b>Resultado:</b> Esperado = dentro do esperado para a idade · Atenção = merece acompanhar · Abaixo do esperado = pode precisar de apoio.</p>
      <p class="resumo">Resumo: ${resumo}.</p>
      ${concFam}
      <div class="disc">Este resultado é parte de uma avaliação e não constitui diagnóstico. A interpretação deve ser feita pelo profissional responsável.</div>
      <div class="sign">
        <div class="sline">${esc(examinador) || "Profissional responsável"}</div>
        <div class="sline">Assinatura</div>
      </div>
      </body></html>`;
  }

  window.Gan = Gan;
})();
