← Biblioteca componentes/conversao/diagnostico-perfil/ficha.md CódigoTela cheia
conversao/diagnostico-perfil

Diagnóstico de perfil em cinco perguntas

🟢 estavel complexidade alta prefixo perf- diagnostico assistente multi-etapas perfil-de-investidor formulario radio tipografia titulo-gigante acessibilidade aria-live sem-lib escuro

Incluir · as linhas que se colam num site novo, depois do core (CLAUDE.md §10)

<link rel="stylesheet" href="componentes/conversao/diagnostico-perfil/estilo.css">
<script src="componentes/conversao/diagnostico-perfil/script.js"></script>

markup.html · 322 linhas · 19,6 KB

<!-- ==========================================================================
     DIAGNÓSTICO DE PERFIL — cinco perguntas em corpo de manifesto
     Prefixo: perf-
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css + script.js

     A IDEIA. Uma pergunta por vez, ocupando a dobra inteira: o enunciado é a
     peça gráfica (a primitiva .display-xl do core), e as respostas são linhas
     grandes com número, texto e um filete que acende na cor de marca. Sem
     cartão, sem caixa, sem foto — a lista É o desenho. O irmão
     conversao/simulador-carteira resolve o mesmo momento da página com a forma
     oposta: painel ao lado de foto, campos variados, rosca no fim.

     A TROCA É O MOVIMENTO. A pergunta que sai desaparece com blur e subida; a
     que entra vem com blur a zero, palavra a palavra no enunciado. Não há mais
     animação nenhuma na peça.

     ORDEM DOS BLOCOS, E POR QUÊ. O que está visível aqui é o estado SEM
     JavaScript: as cinco perguntas empilhadas como um formulário comum, a nota
     que explica por que não há resultado, e o botão que leva à conversa. Quem
     esconde o que sobra é o script — nunca o CSS —, e é por isso que os passos
     2 a 5 não nascem com [hidden]. O que nasce com [hidden] é só o que não faz
     sentido sem script: a barra de passos, o botão Continuar e o resultado.

     COMPLIANCE. O resultado é leitura de conversa, não suitability: não
     substitui o questionário de perfil do investidor da abertura de conta, não
     é recomendação e não fala de rentabilidade. A alocação sai em FAIXA, nunca
     em número cravado. Nada é gravado: as respostas vivem em memória e morrem
     no recarregamento. Ver ficha.md, seção Compliance.

     PESOS. Cada resposta carrega data-perf-peso (0 a 3); a soma das cinco cai
     numa das quatro faixas declaradas em data-perf-min / data-perf-max dos
     perfis. Tudo ILUSTRATIVO — recalibrar com a área técnica do cliente.
     ========================================================================== -->

<section class="sec-deep sec-pad perf" data-perf>
  <div class="wrap">

    <header class="perf-head">
      <p class="eyebrow" data-rv>Diagnóstico</p>
      <h2 class="sec-title ttl perf-convite">Cinco perguntas curtas, e uma leitura do seu <em>perfil</em>.</h2>
      <p class="sec-lead perf-apoio" data-rv data-d="2">Leva menos de um minuto. Nada fica gravado — as respostas só existem enquanto esta página estiver aberta, e servem para começar a conversa em outro ponto.</p>
    </header>

    <div class="perf-painel" data-rv data-d="2">

      <!-- Indicador de passos: decorativo. A informação de verdade vai na
           região aria-live logo abaixo, que anuncia "Pergunta 3 de 5". -->
      <div class="perf-barra" hidden>
        <p class="perf-contador" aria-hidden="true"><span class="perf-atual">01</span> / <span class="perf-total">05</span></p>
        <button class="perf-voltar" type="button" hidden>Voltar</button>
      </div>

      <p class="perf-anuncio visually-hidden" role="status" aria-live="polite"></p>

      <div class="perf-palco">

        <form class="perf-form">

          <!-- ---------------------------------------------------- 01 -->
          <fieldset class="perf-passo">
            <legend class="perf-legenda perf-foco" tabindex="-1">
              <span class="display-xl perf-pergunta">Em quanto tempo esse dinheiro precisa estar disponível?</span>
            </legend>
            <div class="perf-ops">
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-horizonte" data-perf-peso="0">
                <span class="perf-op-num" aria-hidden="true">01</span>
                <span class="perf-op-txt">Nos próximos dois anos.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-horizonte" data-perf-peso="1">
                <span class="perf-op-num" aria-hidden="true">02</span>
                <span class="perf-op-txt">Entre dois e cinco anos.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-horizonte" data-perf-peso="2">
                <span class="perf-op-num" aria-hidden="true">03</span>
                <span class="perf-op-txt">De cinco a dez anos.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-horizonte" data-perf-peso="3">
                <span class="perf-op-num" aria-hidden="true">04</span>
                <span class="perf-op-txt">Não tenho data. É para depois de mim.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
            </div>
          </fieldset>

          <!-- ---------------------------------------------------- 02 -->
          <fieldset class="perf-passo">
            <legend class="perf-legenda perf-foco" tabindex="-1">
              <span class="display-xl perf-pergunta">A carteira cai 20% em três meses. O que você faz?</span>
            </legend>
            <div class="perf-ops">
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-queda" data-perf-peso="0">
                <span class="perf-op-num" aria-hidden="true">01</span>
                <span class="perf-op-txt">Vendo o que der, para parar a perda.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-queda" data-perf-peso="1">
                <span class="perf-op-num" aria-hidden="true">02</span>
                <span class="perf-op-txt">Ligo para o assessor antes de decidir qualquer coisa.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-queda" data-perf-peso="2">
                <span class="perf-op-num" aria-hidden="true">03</span>
                <span class="perf-op-txt">Não mexo. Já esperei antes e deu certo.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-queda" data-perf-peso="3">
                <span class="perf-op-num" aria-hidden="true">04</span>
                <span class="perf-op-txt">Procuro onde aportar mais.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
            </div>
          </fieldset>

          <!-- ---------------------------------------------------- 03 -->
          <fieldset class="perf-passo">
            <legend class="perf-legenda perf-foco" tabindex="-1">
              <span class="display-xl perf-pergunta">De onde veio a maior parte do patrimônio?</span>
            </legend>
            <div class="perf-ops">
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-origem" data-perf-peso="0">
                <span class="perf-op-num" aria-hidden="true">01</span>
                <span class="perf-op-txt">De salário e poupança, ao longo de muitos anos.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-origem" data-perf-peso="2">
                <span class="perf-op-num" aria-hidden="true">02</span>
                <span class="perf-op-txt">Da venda de um negócio ou de um imóvel.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-origem" data-perf-peso="3">
                <span class="perf-op-num" aria-hidden="true">03</span>
                <span class="perf-op-txt">Da família — veio de quem veio antes de mim.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
            </div>
          </fieldset>

          <!-- ---------------------------------------------------- 04 -->
          <fieldset class="perf-passo">
            <legend class="perf-legenda perf-foco" tabindex="-1">
              <span class="display-xl perf-pergunta">O que tira o seu sono?</span>
            </legend>
            <div class="perf-ops">
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-sono" data-perf-peso="0">
                <span class="perf-op-num" aria-hidden="true">01</span>
                <span class="perf-op-txt">Ver o valor cair, mesmo que ele volte depois.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-sono" data-perf-peso="1">
                <span class="perf-op-num" aria-hidden="true">02</span>
                <span class="perf-op-txt">Precisar do dinheiro e ele estar preso.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-sono" data-perf-peso="2">
                <span class="perf-op-num" aria-hidden="true">03</span>
                <span class="perf-op-txt">Ficar parado enquanto a oportunidade passa.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-sono" data-perf-peso="3">
                <span class="perf-op-num" aria-hidden="true">04</span>
                <span class="perf-op-txt">Deixar a família sem estrutura se algo acontecer comigo.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
            </div>
          </fieldset>

          <!-- ---------------------------------------------------- 05 -->
          <fieldset class="perf-passo">
            <legend class="perf-legenda perf-foco" tabindex="-1">
              <span class="display-xl perf-pergunta">O que você espera de um assessor?</span>
            </legend>
            <div class="perf-ops">
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-assessor" data-perf-peso="0">
                <span class="perf-op-num" aria-hidden="true">01</span>
                <span class="perf-op-txt">Que me segure na hora de tomar decisão ruim.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-assessor" data-perf-peso="2">
                <span class="perf-op-num" aria-hidden="true">02</span>
                <span class="perf-op-txt">Que monte um plano e me cobre dele.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
              <label class="perf-op">
                <input class="perf-radio" type="radio" name="perf-assessor" data-perf-peso="3">
                <span class="perf-op-num" aria-hidden="true">03</span>
                <span class="perf-op-txt">Que cuide da estrutura, do imposto e da sucessão.</span>
                <span class="perf-op-marca" aria-hidden="true"></span>
              </label>
            </div>
          </fieldset>

        </form>

        <!-- ---------------------------------------------------- resultado -->
        <!-- Nasce [hidden] porque sem script não há o que calcular: os quatro
             perfis empilhados seriam mentira, não estado honesto. -->
        <div class="perf-resultado" hidden>

          <!-- CONTEÚDO ILUSTRATIVO — faixas de alocação, textos de perfil e a
               conta de pontos são exemplo editorial. Trocar com a área técnica
               e com o compliance do cliente antes de publicar. -->
          <div class="perf-perfil" data-perf-min="0" data-perf-max="4" hidden>
            <p class="perf-rotulo" data-rv>Seu perfil</p>
            <h3 class="display-xl perf-nome perf-foco" tabindex="-1" data-rv>Guardião</h3>
            <p class="perf-leitura" data-rv data-d="1">Você já construiu. A pergunta que importa agora não é quanto cresce, é quanto se perde num ano ruim. Uma carteira sua começa pela liquidez e pela previsibilidade; o resto entra depois, e devagar.</p>
            <ul class="perf-tracos" data-rv data-d="2">
              <li class="perf-traco">Horizonte curto e com data marcada</li>
              <li class="perf-traco">Baixa tolerância a oscilação</li>
              <li class="perf-traco">Liquidez antes de retorno</li>
            </ul>
            <div class="perf-faixa" data-rv data-d="3">
              <p class="perf-faixa-tit">Ponto de partida da conversa</p>
              <ul class="perf-faixa-lista">
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Renda fixa</span> <span class="perf-faixa-valor">65–85%</span></li>
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Multimercado</span> <span class="perf-faixa-valor">5–20%</span></li>
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Renda variável</span> <span class="perf-faixa-valor">0–10%</span></li>
              </ul>
            </div>
          </div>

          <div class="perf-perfil" data-perf-min="5" data-perf-max="8" hidden>
            <p class="perf-rotulo" data-rv>Seu perfil</p>
            <h3 class="display-xl perf-nome perf-foco" tabindex="-1" data-rv>Construtor</h3>
            <p class="perf-leitura" data-rv data-d="1">Você ainda está acumulando, e o tempo joga a favor. Dá para conviver com oscilação em troca de um horizonte maior — desde que a parte que sustenta a vida no curto prazo esteja separada, e intocada.</p>
            <ul class="perf-tracos" data-rv data-d="2">
              <li class="perf-traco">Aporte recorrente, não evento único</li>
              <li class="perf-traco">Horizonte de cinco a dez anos</li>
              <li class="perf-traco">Oscilação aceita dentro de um plano escrito</li>
            </ul>
            <div class="perf-faixa" data-rv data-d="3">
              <p class="perf-faixa-tit">Ponto de partida da conversa</p>
              <ul class="perf-faixa-lista">
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Renda fixa</span> <span class="perf-faixa-valor">45–65%</span></li>
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Multimercado</span> <span class="perf-faixa-valor">10–25%</span></li>
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Renda variável</span> <span class="perf-faixa-valor">15–30%</span></li>
              </ul>
            </div>
          </div>

          <div class="perf-perfil" data-perf-min="9" data-perf-max="11" hidden>
            <p class="perf-rotulo" data-rv>Seu perfil</p>
            <h3 class="display-xl perf-nome perf-foco" tabindex="-1" data-rv>Navegador</h3>
            <p class="perf-leitura" data-rv data-d="1">Você já passou por uma queda e não vendeu. O trabalho aqui é menos de coragem e mais de rota: diversificar por geografia e por moeda, e ter uma política de investimentos escrita — para a decisão do mês não depender da notícia do mês.</p>
            <ul class="perf-tracos" data-rv data-d="2">
              <li class="perf-traco">Tolerância a oscilação já testada na prática</li>
              <li class="perf-traco">Diversificação internacional e por moeda</li>
              <li class="perf-traco">Rebalanceamento por regra, não por humor</li>
            </ul>
            <div class="perf-faixa" data-rv data-d="3">
              <p class="perf-faixa-tit">Ponto de partida da conversa</p>
              <ul class="perf-faixa-lista">
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Renda fixa</span> <span class="perf-faixa-valor">30–50%</span></li>
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Multimercado</span> <span class="perf-faixa-valor">15–30%</span></li>
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Renda variável e exterior</span> <span class="perf-faixa-valor">25–45%</span></li>
              </ul>
            </div>
          </div>

          <div class="perf-perfil" data-perf-min="12" data-perf-max="15" hidden>
            <p class="perf-rotulo" data-rv>Seu perfil</p>
            <h3 class="display-xl perf-nome perf-foco" tabindex="-1" data-rv>Legado</h3>
            <p class="perf-leitura" data-rv data-d="1">O dinheiro é maior que uma vida, e por isso o horizonte não é o seu. Aqui a conversa começa pela estrutura — holding familiar, sucessão, custódia — e a alocação vem depois, com espaço para o que não precisa de liquidez diária.</p>
            <ul class="perf-tracos" data-rv data-d="2">
              <li class="perf-traco">Horizonte de geração, sem data</li>
              <li class="perf-traco">Estrutura antes de alocação</li>
              <li class="perf-traco">Espaço para ativo ilíquido e para o exterior</li>
            </ul>
            <div class="perf-faixa" data-rv data-d="3">
              <p class="perf-faixa-tit">Ponto de partida da conversa</p>
              <ul class="perf-faixa-lista">
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Renda fixa</span> <span class="perf-faixa-valor">25–45%</span></li>
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Multimercado</span> <span class="perf-faixa-valor">15–30%</span></li>
                <li class="perf-faixa-item"><span class="perf-faixa-classe">Renda variável e alternativos</span> <span class="perf-faixa-valor">30–50%</span></li>
              </ul>
            </div>
          </div>

          <p class="nota perf-nota" data-rv data-d="3">Leitura de conversa, não suitability. Este diagnóstico não substitui o questionário de perfil do investidor exigido na abertura de conta, não é recomendação de investimento e não promete rentabilidade. As faixas são ilustrativas e mudam com a política de investimentos de cada família.</p>

          <div class="perf-fim" data-rv data-d="3">
            <a class="btn btn-primary perf-cta" href="#diagnostico">Falar com um assessor</a>
            <button class="btn btn-ghost perf-refazer" type="button">Refazer</button>
          </div>
        </div>

      </div>

      <div class="perf-acoes" hidden>
        <button class="btn btn-primary perf-continuar" type="button" disabled>Continuar</button>
      </div>

      <!-- Estado honesto sem JavaScript: as cinco perguntas acima continuam de
           pé como formulário comum, e a pessoa segue para a conversa levando as
           respostas na cabeça. O script esconde este bloco na inicialização. -->
      <div class="perf-sem-js">
        <p class="nota perf-nota">O perfil é calculado no seu navegador. Sem JavaScript as cinco perguntas continuam aqui para orientar a conversa — leve as suas respostas para o assessor.</p>
        <a class="btn btn-primary perf-cta" href="#diagnostico">Falar com um assessor</a>
      </div>

    </div>
  </div>
</section>

estilo.css · 427 linhas · 17,5 KB

/* ==========================================================================
   DIAGNÓSTICO DE PERFIL — prefixo perf-
   Cinco perguntas, uma por vez, em corpo de manifesto. O enunciado é a peça
   gráfica; as respostas são linhas grandes com número, texto e um filete que
   acende. Sem cartão, sem caixa, sem imagem.

   Tokens usados:
     --accent           filete aceso, resposta marcada, número da marcada
     --display          face do enunciado (via .display-xl) e dos valores
     --sans             face das respostas e do texto de apoio
     --ease --dur       curva e tempo do projeto
     --fg --fg-2        tinta do enunciado e das respostas
     --fg-muted         números, contador, ressalva
     --rule --rule-2    filete em repouso e divisória do resultado
     --sec-gap          respiro derivado, nunca clamp solto
     --radius-pill      só via .btn do core

   A REGRA QUE ORGANIZA O ARQUIVO: nada aqui esconde passo nenhum. Quem
   esconde é o script, com [hidden]. Sem JavaScript o CSS entrega as cinco
   perguntas empilhadas, e é isso que se quer.
   ========================================================================== */

.perf{
  /* Corpo do enunciado. Como no manifesto, o que se persegue é a FRAÇÃO da
     coluna que a pergunta ocupa (~3 linhas de 15ch), não o número: trocar a
     --display do tema por uma condensada pede corpo maior. */
  --perf-perg:clamp(30px,4.4vw,58px);
  --display-xl:var(--perf-perg);
  /* .95 é o padrão da primitiva, para título de uma ou duas palavras. Aqui
     são frases de três linhas com descendente ("q" de "quanto", "g" de
     "algo"): abaixo de 1.02 elas encostam na linha seguinte. */
  --display-xl-lh:1.02;

  /* Corpo da resposta. Grande de propósito — a lista é o desenho, não um
     formulário. Fica em --sans (peso leve) contra a display peso 700 do
     enunciado: é esse contraste que separa pergunta de resposta sem
     precisar de caixa, régua ou cartão. */
  --perf-op-fs:clamp(17px,1.9vw,25px);

  /* Coluna do número da resposta. Em em para acompanhar o corpo. */
  --perf-op-num-col:3.2em;

  /* Altura mínima do palco. As perguntas têm 3 ou 4 respostas e enunciados de
     1 a 4 linhas: sem piso, a dobra encolhe e cresce a cada troca e a página
     inteira dança sob quem está lendo. Medido a 1440x900 no tema
     liberta-wealth: o passo mais baixo dá 330px e o mais alto 479px — 149px
     de solavanco a cada avanço.

     Quem preenche --perf-palco-medido é o SCRIPT, que mede os cinco passos e
     escreve o maior. Um clamp fixo aqui erraria em metade das larguras: a
     altura do passo depende de quantas linhas a pergunta quebra, e isso muda
     com o corpo da letra, com a face do tema e com a tradução do texto. O
     clamp abaixo é só o piso de partida, válido no quadro entre a carga e a
     primeira medida. */
  --perf-palco-medido:clamp(300px,46vh,470px);
  --perf-palco-min:var(--perf-palco-medido);

  /* Saída do passo que sai. O script lê o MESMO número (SAIDA em script.js):
     mudar aqui pede mudar lá, senão o passo some antes ou depois da hora. */
  --perf-saida:.26s;

  /* Atraso entre palavras do enunciado que entra. 42ms x ~9 palavras = ~.38s
     de escalonamento, que é quanto dura a leitura da primeira palavra. */
  --perf-w-passo:42ms;

  /* Respiro entre a barra de passos e o enunciado. Mora no PASSO, e não no
     palco, porque é a altura do passo que o script mede para calcular o
     piso — padding no palco ficaria de fora da conta e o piso sairia curto
     pela exata medida deste respiro. */
  --perf-topo:clamp(28px,4vh,52px);

  /* Largura de leitura do parágrafo do resultado. */
  --perf-leitura-max:56ch;
}

/* ==========================================================================
   CABEÇALHO
   ========================================================================== */

.perf-convite{margin-top:16px}
.perf-apoio{max-width:52ch}

/* O painel inteiro é um bloco de reveal do core. O respiro que o separa do
   cabeçalho sai de --sec-gap, como o resto do ritmo vertical. */
.perf-painel{margin-top:calc(var(--sec-gap) * .34)}

/* ==========================================================================
   BARRA DE PASSOS
   Decorativa: [aria-hidden] no contador e a informação de verdade na região
   aria-live. Nasce [hidden] no markup e é o script que a mostra.
   ========================================================================== */

/* [hidden] vem da folha do navegador com especificidade (0,1,0). Toda regra
   de display por classe abaixo o venceria e mostraria o que devia sumir —
   a mesma pegadinha que o .hgal-nav[hidden] da galeria em scroll horizontal
   documenta. Uma regra só, no topo, cobre o componente inteiro. */
.perf [hidden]{display:none}

.perf-barra{
  display:flex;align-items:center;justify-content:space-between;gap:16px;
  padding-bottom:18px;
  border-bottom:1px solid var(--rule);
}

.perf-contador{
  font-family:var(--sans);font-size:11.5px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--fg-muted);
}
/* O número corrente acende; o total fica apagado. É o único lugar da peça
   com progresso, e ele é texto — barra que enche viraria gamificação, que é
   o registro errado para uma assessoria. */
.perf-atual{color:var(--accent)}

/* "Voltar" nunca é disabled: no primeiro passo ele simplesmente não existe.
   Botão apagado que não faz nada é pior que botão ausente. */
.perf-voltar{
  font-family:var(--sans);font-size:11.5px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--fg-muted);
  background:none;border:0;padding:0;
  transition:color .3s var(--ease);
}
.perf-voltar:hover{color:var(--accent)}

/* A região aria-live (.perf-anuncio) não tem regra própria: ela leva a
   primitiva .visually-hidden do core, que existe exatamente para isto —
   texto que o leitor de tela recebe e o olho não vê, com clip-path em vez de
   display:none, porque display:none e visibility:hidden tiram o elemento da
   árvore de acessibilidade e o anúncio nunca sairia. Recopiar as seis linhas
   aqui seria a cópia que o §6 do contrato manda promover. */

/* ==========================================================================
   PALCO
   ========================================================================== */

.perf-palco{
  position:relative;
  min-height:var(--perf-palco-min);
}

/* fieldset traz border, padding e margin da folha do navegador, e um
   min-inline-size:min-content que estoura a coluna em tela estreita quando
   uma resposta longa não quebra. Os quatro zeros são obrigatórios. */
.perf-passo{border:0;margin:0;padding:var(--perf-topo) 0 0;min-inline-size:0}

/* legend é o enunciado, e recebe o foco na troca de passo (tabindex="-1" no
   markup). Sem display:block ela é tratada como caixa especial da borda do
   fieldset e se recusa a virar bloco de largura cheia.

   width:fit-content não é capricho: é o que faz o anel de foco do core
   abraçar a PERGUNTA. Em largura cheia o anel saía com os 1096px da coluna
   em volta de um texto de 660px — quem chega de teclado via um retângulo
   solto, não o enunciado marcado. */
.perf-legenda{display:block;float:none;width:fit-content;max-width:100%;padding:0;margin:0}

/* A medida da pergunta em em, e não em ch nem px: presa ao corpo da letra,
   ela desenha SEMPRE as mesmas ~3 linhas, com qualquer display do tema e em
   qualquer largura de tela. Em ch a conta sairia errada — ch lê o corpo do
   elemento, e a medida que importa é a da mancha. */
.perf-pergunta{display:block;max-width:11.5em}

/* -------------------------------------------------------------- respostas */

/* font-size aqui, e não só nas respostas: é ele que faz o max-width em em
   valer o corpo da RESPOSTA. Sem isso a medida sairia calculada no corpo do
   texto herdado (17px) e a lista ficaria com pouco mais da metade da
   largura pretendida — medido: 385px onde se queria 625px. */
.perf-ops{
  margin-top:clamp(26px,4vh,46px);
  font-size:var(--perf-op-fs);
  max-width:25em;
}

/* A resposta inteira é o <label>. O <input> real está por baixo, esticado
   sobre a linha toda — ver .perf-radio. */
.perf-op{
  position:relative;display:grid;
  grid-template-columns:var(--perf-op-num-col) 1fr;
  align-items:baseline;
  padding-block:clamp(13px,1.5vh,19px);
}

/* O RADIO É DE VERDADE, e cobre a linha inteira em vez de ficar escondido
   num canto de 1px. Duas coisas dependem disso:
     1. o :focus-visible que o core desenha cai exatamente em volta da
        resposta — não num quadradinho perdido no canto;
     2. o clique em qualquer ponto da linha chega ao input, sem depender do
        for/id (que se repetiria se o componente aparecesse duas vezes).
   appearance:none, e não opacity:0: com opacity:0 o outline do foco some
   junto com o widget, e quem navega por teclado fica sem indicador. */
.perf-radio{
  -webkit-appearance:none;appearance:none;
  position:absolute;inset:0;width:100%;height:100%;
  margin:0;padding:0;border:0;background:none;
  cursor:pointer;
}

.perf-op-num{
  font-family:var(--sans);font-size:11.5px;letter-spacing:.18em;
  color:var(--fg-muted);
  transition:color .3s var(--ease);
}

.perf-op-txt{
  font-family:var(--sans);
  font-size:1em;line-height:1.26;letter-spacing:.005em;
  color:var(--fg-2);
  transition:color .3s var(--ease);
}

/* O filete. Fica numa caixa própria, esticada sobre a linha, porque o efeito
   precisa de dois traços sobrepostos: o de repouso em --rule e o aceso em
   --accent, que cresce da esquerda. pointer-events:none para não roubar o
   clique do input que está por baixo. */
.perf-op-marca{
  position:absolute;inset:0;pointer-events:none;
  border-bottom:1px solid var(--rule);
}
.perf-op-marca::after{
  content:"";position:absolute;left:0;right:0;bottom:-1px;height:1px;
  background:var(--accent);
  transform:scaleX(0);transform-origin:left;
  transition:transform .42s var(--ease);
}

.perf-op:hover .perf-op-marca::after{transform:scaleX(1)}
.perf-op:hover .perf-op-txt{color:var(--fg)}
.perf-op:hover .perf-op-num{color:var(--fg-2)}

/* Marcada. É o único estado com cor de marca no texto, e ele dura pouco: o
   avanço automático leva embora a pergunta 380ms depois. Esses 380ms existem
   justamente para esta linha aparecer. */
.perf-radio:checked ~ .perf-op-marca::after{transform:scaleX(1)}
.perf-radio:checked ~ .perf-op-txt{color:var(--accent)}
.perf-radio:checked ~ .perf-op-num{color:var(--accent)}

/* O filete cresce; em movimento reduzido ele acende. Colado à regra que
   desliga, como manda o §6.5 do contrato. */
@media(prefers-reduced-motion:reduce){
  .perf-op-marca::after{transition-duration:0s}
}

/* ==========================================================================
   A TROCA DE PERGUNTA — o movimento da peça
   Só existe com script: as regras estão todas sob .perf.is-live, classe que
   o script põe na seção ao iniciar. Sem ela o CSS não esconde nada e as
   cinco perguntas ficam empilhadas.
   ========================================================================== */

/* Sai com blur e subida. */
.perf.is-live .perf-passo.is-out,
.perf.is-live .perf-resultado.is-out{
  opacity:0;filter:blur(12px);transform:translateY(-22px);
  transition:opacity var(--perf-saida) var(--ease),
             filter var(--perf-saida) var(--ease),
             transform var(--perf-saida) var(--ease);
}

/* Entra palavra a palavra. O script fatia o enunciado em .perf-w e escreve
   --perf-i (o índice) no style de cada palavra — é o mesmo recurso do
   --frase-cor do conteudo/manifesto-com-imagens: o que varia por item mora
   no item, não numa cadeia de :nth-child que se desalinha quando o texto
   muda. */
.perf.is-live .perf-w{
  display:inline-block;
  opacity:0;filter:blur(12px);transform:translateY(.34em);
  transition:opacity .62s var(--ease),
             filter .62s var(--ease),
             transform .62s var(--ease);
  transition-delay:calc(var(--perf-i,0) * var(--perf-w-passo));
}
.perf.is-live .perf-passo.is-in .perf-w{opacity:1;filter:blur(0);transform:none}

/* As respostas entram como bloco, logo atrás da última palavra. */
.perf.is-live .perf-ops{
  opacity:0;filter:blur(10px);transform:translateY(16px);
  transition:opacity .7s var(--ease),
             filter .7s var(--ease),
             transform .7s var(--ease);
  transition-delay:.16s;
}
.perf.is-live .perf-passo.is-in .perf-ops{opacity:1;filter:blur(0);transform:none}

/* Movimento reduzido: a pergunta troca, mas não anda. O script já pula a
   saída animada; o que sobra é o estado inicial das palavras e da lista, que
   sem esta regra deixaria a pergunta invisível para sempre. */
@media(prefers-reduced-motion:reduce){
  .perf.is-live .perf-w,
  .perf.is-live .perf-ops{
    opacity:1;filter:none;transform:none;transition:none;
  }
}

/* ==========================================================================
   AÇÕES
   O botão nasce disabled e só libera quando o passo está respondido. Ele
   existe SEMPRE, e não só em movimento reduzido: o avanço automático é
   cortesia para quem clica, nunca a única saída. Ver o gotcha da ficha.
   ========================================================================== */

.perf-acoes{margin-top:clamp(30px,4.5vh,52px)}

.perf .btn:disabled{
  opacity:.34;cursor:default;
  background:none;color:var(--fg-muted);border-color:var(--rule);
}
.perf .btn:disabled:hover{
  background:none;color:var(--fg-muted);border-color:var(--rule);
  backdrop-filter:none;-webkit-backdrop-filter:none;
}

/* ==========================================================================
   RESULTADO
   ========================================================================== */

.perf-resultado{padding-top:var(--perf-topo)}

.perf-rotulo{
  font-family:var(--sans);font-size:11.5px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--fg-muted);
}

/* Mesma conta do .perf-legenda: sem fit-content o anel de foco do resultado
   sai com a largura da coluna em volta de uma palavra só. */
.perf-nome{margin-top:10px;width:fit-content;max-width:100%}

.perf-leitura{
  margin-top:clamp(20px,2.6vh,30px);
  max-width:var(--perf-leitura-max);
  font-size:var(--fs-body);line-height:1.62;color:var(--fg-2);
}

.perf-tracos{
  margin-top:clamp(22px,3vh,34px);
  max-width:var(--perf-leitura-max);
}
.perf-traco{
  padding-block:11px;
  border-top:1px solid var(--rule);
  font-size:var(--fs-body);color:var(--fg-2);
}

/* A faixa de alocação. FAIXA, e não número cravado: um "62%" leria como
   recomendação de carteira, que é exatamente o que esta peça não pode ser.
   O desenho ecoa a lista de respostas — mesma régua, mesmo filete — para o
   resultado parecer a continuação da conversa, e não outra seção. */
.perf-faixa{
  margin-top:clamp(28px,3.6vh,42px);
  max-width:var(--perf-leitura-max);
  padding-top:20px;
  border-top:1px solid var(--rule-2);
}
.perf-faixa-tit{
  font-family:var(--sans);font-size:11.5px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--fg-muted);
}
.perf-faixa-lista{margin-top:12px}
.perf-faixa-item{
  display:flex;align-items:baseline;justify-content:space-between;gap:20px;
  padding-block:12px;
  border-bottom:1px solid var(--rule);
}
.perf-faixa-classe{font-size:var(--fs-body);color:var(--fg-2)}
.perf-faixa-valor{
  font-family:var(--display);font-weight:700;
  font-size:clamp(17px,1.6vw,21px);letter-spacing:-.01em;
  color:var(--accent);
  white-space:nowrap;
}

/* A ressalva de compliance. Fica no passo do resultado, onde a pessoa lê o
   número — não em letra miúda no rodapé da página. */
/* Tipografia pela primitiva .nota do core; aqui só a posição e a medida — 64ch
   alinha a ressalva com a coluna de leitura do resultado. */
.perf-nota{
  margin-top:clamp(24px,3vh,34px);
  --nota-max:64ch;
}

.perf-fim{
  margin-top:clamp(26px,3.4vh,38px);
  display:flex;flex-wrap:wrap;gap:14px;
}

/* ==========================================================================
   SEM JAVASCRIPT
   Bloco visível por padrão; o script o esconde na inicialização. Nada aqui
   depende de @media nem de :has — o estado honesto é o estado de partida.
   ========================================================================== */

.perf-sem-js{
  margin-top:clamp(30px,4.5vh,52px);
  display:flex;flex-direction:column;align-items:flex-start;gap:20px;
}

/* ==========================================================================
   MOBILE
   ========================================================================== */

@media(max-width:900px){
  /* A pergunta abre a medida porque o corpo encolheu: 11.5em de um clamp que
     já bateu no piso desenhariam sete linhas curtas em vez de três largas. */
  .perf-pergunta{max-width:13em}
  /* Medido a 820px: sem este teto a lista ia aos 723px da coluna cheia contra
     uma pergunta de 469px, e a resposta ficava mais larga que o enunciado —
     o desenho da peça se inverte. */
  .perf-ops{max-width:31em}
}

@media(max-width:560px){
  .perf{
    --perf-op-num-col:2.6em;
    /* Em coluna estreita a resposta longa ocupa duas ou três linhas, e o
       escalonamento de 42ms por palavra vira espera visível numa pergunta de
       nove palavras. */
    --perf-w-passo:32ms;
    /* Sem piso no celular, e de propósito. O piso existe para a página não
       dançar sob quem lê quando a dobra ocupa meia tela; aqui a pergunta já
       ocupa a tela inteira e o piso só abriria um vão morto embaixo da
       resposta mais curta. Escrever --perf-palco-min (e não
       --perf-palco-medido) é o que faz esta regra vencer a medida que o
       script põe no style da seção. */
    --perf-palco-min:auto;
  }
  .perf-pergunta{max-width:none}
  .perf-fim .btn,
  .perf-sem-js .btn{width:100%}
}

script.js · 318 linhas · 12,6 KB

/* ==========================================================================
   DIAGNÓSTICO DE PERFIL — prefixo perf-
   Cinco perguntas, uma por vez, e um perfil nomeado no fim.

   O QUE ESTE SCRIPT FAZ, EM ORDEM:
     1. põe .is-live na seção — é essa classe que liga as regras de troca no
        CSS. Sem script, nenhuma delas existe e as cinco perguntas ficam
        empilhadas como formulário comum;
     2. esconde os passos 2 a 5 com [hidden] (nunca com opacity: campo
        invisível e tabulável é armadilha) e o bloco "sem JavaScript";
     3. fatia cada enunciado em palavras, para a entrada escalonada;
     4. troca de passo: saída com blur e subida, entrada palavra a palavra,
        foco no enunciado novo, anúncio em aria-live;
     5. soma os pesos e escolhe um dos quatro perfis pela faixa declarada no
        próprio HTML (data-perf-min / data-perf-max).

   NADA É GRAVADO. As respostas vivem nos próprios <input> e morrem no
   recarregamento: sem localStorage, sem cookie, sem envio. São dados
   financeiros de alguém, e captura é trabalho do conversao/cta-formulario-lead,
   para onde o botão do resultado aponta.

   MOVIMENTO REDUZIDO: a troca acontece sem saída animada e sem escalonamento
   de palavra (o CSS zera o estado inicial), e o avanço automático não existe —
   só o botão Continuar.

   AVANÇO AUTOMÁTICO, E POR QUE ELE É SÓ PARA QUEM CLICA: seta em grupo de
   radio MOVE E MARCA ao mesmo tempo. Quem navega por teclado marcaria a
   primeira opção só de passar por ela, e seria levado para a pergunta
   seguinte antes de chegar na resposta que queria. Por isso o avanço só
   dispara quando a última interação veio de ponteiro.
   ========================================================================== */

(function () {

  var raizes = document.querySelectorAll('[data-perf]');
  if (!raizes.length) return;                       // guard silencioso

  /* Bate com --perf-saida no estilo.css. Mudar um pede mudar o outro. */
  var SAIDA  = 260;
  /* Espera antes do avanço automático. Curta o bastante para não parecer
     travamento, longa o bastante para a linha marcada acender e ser vista. */
  var AVANCO = 380;

  function reduzido() {
    if (window.LIB && typeof window.LIB.reduce === 'boolean') return window.LIB.reduce;
    return matchMedia('(prefers-reduced-motion: reduce)').matches &&
           location.search.indexOf('motion=on') < 0;
  }

  function dois(n) { return (n < 10 ? '0' : '') + n; }

  /* Quebra o enunciado em palavras e numera cada uma em --perf-i. O índice
     mora no style da palavra, e não numa cadeia de :nth-child, porque o
     texto muda a cada projeto: cravar o atraso na posição faria a animação
     se desalinhar sozinha na primeira reescrita. */
  function fatiar(el) {
    var partes = el.textContent.split(/\s+/), i, n = 0, palavra;
    el.textContent = '';
    for (i = 0; i < partes.length; i++) {
      if (!partes[i]) continue;
      palavra = document.createElement('span');
      palavra.className = 'perf-w';
      palavra.style.setProperty('--perf-i', n);
      palavra.textContent = partes[i];
      el.appendChild(palavra);
      el.appendChild(document.createTextNode(' '));
      n++;
    }
  }

  function iniciar(raiz) {

    var passos = raiz.querySelectorAll('.perf-passo');
    if (!passos.length) return;

    var barra     = raiz.querySelector('.perf-barra');
    var atual     = raiz.querySelector('.perf-atual');
    var total     = raiz.querySelector('.perf-total');
    var voltar    = raiz.querySelector('.perf-voltar');
    var acoes     = raiz.querySelector('.perf-acoes');
    var seguir    = raiz.querySelector('.perf-continuar');
    var aviso     = raiz.querySelector('.perf-anuncio');
    var resultado = raiz.querySelector('.perf-resultado');
    var perfis    = raiz.querySelectorAll('.perf-perfil');
    var semJs     = raiz.querySelector('.perf-sem-js');
    var refazer   = raiz.querySelector('.perf-refazer');
    var form      = raiz.querySelector('.perf-form');
    var enunciados = raiz.querySelectorAll('.perf-pergunta');

    var i = 0;                 // passo corrente; passos.length = resultado
    var animando = false;
    var timer = 0;
    var viaPonteiro = false;
    var escolhido = null;
    var off = null, aceso = false;
    var largura = innerWidth;
    var k;

    /* O palco precisa de um piso, senão a dobra encolhe e cresce a cada
       avanço e a página inteira dança sob quem está lendo. O piso é o passo
       MAIS ALTO, e ele não cabe num clamp de CSS: depende de quantas linhas
       cada pergunta quebra, o que muda com o corpo da letra, com a face do
       tema e com a tradução do texto.

       A medida mostra e reesconde os cinco passos no MESMO bloco síncrono:
       o navegador só pinta no fim da tarefa, então nada pisca. O que custa
       são cinco reflows forçados — por isso a conta só roda quando a largura
       muda de verdade, nunca a cada quadro. */
    function medirPalco() {
      var alturas = [], escondido = [], k, maior = 0;
      for (k = 0; k < passos.length; k++) {
        escondido[k] = passos[k].hidden;
        if (escondido[k]) passos[k].hidden = false;
      }
      for (k = 0; k < passos.length; k++) alturas[k] = passos[k].getBoundingClientRect().height;
      for (k = 0; k < passos.length; k++) if (escondido[k]) passos[k].hidden = true;
      for (k = 0; k < alturas.length; k++) if (alturas[k] > maior) maior = alturas[k];
      if (maior > 0) raiz.style.setProperty('--perf-palco-medido', Math.ceil(maior) + 'px');
    }

    /* ---------------------------------------------------- inicialização */
    raiz.classList.add('is-live');
    for (k = 1; k < passos.length; k++) passos[k].hidden = true;
    for (k = 0; k < enunciados.length; k++) fatiar(enunciados[k]);
    if (semJs) semJs.hidden = true;
    if (barra) barra.hidden = false;
    if (acoes) acoes.hidden = false;
    if (total) total.textContent = dois(passos.length);
    atualizarBarra();
    medirPalco();

    /* A primeira pergunta acende quando a dobra chega, não na carga: assinar
       o laço único do core é o caminho do contrato (§7.4) — o componente não
       registra listener nem observer próprio. */
    function acender() {
      if (aceso) return;
      if (raiz.getBoundingClientRect().top > innerHeight * .92) return;
      aceso = true;
      passos[0].classList.add('is-in');
      if (off) { off(); off = null; }
    }
    if (window.LIB && window.LIB.onScroll) {
      off = window.LIB.onScroll(acender);
      if (aceso && off) { off(); off = null; }   // já estava na tela
      /* O laço do core já é acordado por resize (ele assina os dois eventos),
         então remedir o palco cabe aqui dentro e o componente continua sem
         listener próprio, como o §7.4 pede. A guarda de largura é o que
         impede cinco reflows por quadro de rolagem. */
      window.LIB.onScroll(function () {
        if (innerWidth === largura) return;
        largura = innerWidth;
        medirPalco();
      });
    } else {
      passos[0].classList.add('is-in');
    }

    /* ---------------------------------------------------------- utilidades */

    function telaDe(k) { return k < passos.length ? passos[k] : resultado; }

    function marcada(k) {
      return passos[k] ? passos[k].querySelector('.perf-radio:checked') : null;
    }

    function pontuar() {
      var soma = 0, m, k;
      for (k = 0; k < passos.length; k++) {
        m = marcada(k);
        if (m) soma += parseFloat(m.getAttribute('data-perf-peso')) || 0;
      }
      return soma;
    }

    /* As faixas moram no HTML (data-perf-min / data-perf-max) e não aqui: a
       calibragem é decisão editorial e de compliance do cliente, e quem a
       revisa lê markup, não script. */
    function perfilDe(pontos) {
      var k, min, max;
      for (k = 0; k < perfis.length; k++) {
        min = parseFloat(perfis[k].getAttribute('data-perf-min'));
        max = parseFloat(perfis[k].getAttribute('data-perf-max'));
        if (pontos >= min && pontos <= max) return perfis[k];
      }
      return perfis[perfis.length - 1] || null;
    }

    function anunciar(texto) { if (aviso) aviso.textContent = texto; }

    function cancelar() { if (timer) { clearTimeout(timer); timer = 0; } }

    function atualizarBarra() {
      var noFim = i >= passos.length;
      if (barra) barra.hidden = noFim;
      if (acoes) acoes.hidden = noFim;
      if (noFim) return;
      if (atual) atual.textContent = dois(i + 1);
      if (voltar) voltar.hidden = (i === 0);
      if (seguir) seguir.disabled = !marcada(i);
    }

    /* ------------------------------------------------------------ resultado */

    function montarResultado() {
      var k, filhos, nome;
      for (k = 0; k < perfis.length; k++) perfis[k].hidden = true;
      escolhido = perfilDe(pontuar());
      if (!escolhido) return;
      escolhido.hidden = false;
      /* Tira o .in de uma rodada anterior para o reveal do core poder rodar
         de novo — o core desobserva o elemento assim que ele entra. */
      filhos = escolhido.querySelectorAll('[data-rv]');
      for (k = 0; k < filhos.length; k++) filhos[k].classList.remove('in');
      nome = escolhido.querySelector('.perf-nome');
      anunciar('Diagnóstico concluído. Seu perfil: ' + (nome ? nome.textContent : '') + '.');
    }

    function limparResultado() {
      var k;
      for (k = 0; k < perfis.length; k++) perfis[k].hidden = true;
      escolhido = null;
    }

    /* ---------------------------------------------------------------- troca */

    function ir(destino) {
      if (animando || destino === i) return;
      if (destino < 0 || destino > passos.length) return;
      cancelar();

      var sai = telaDe(i), entra = telaDe(destino);
      if (!sai || !entra) return;
      animando = true;

      function fim() {
        sai.classList.remove('is-out');
        sai.hidden = true;
        if (sai === resultado) limparResultado();

        i = destino;
        if (entra === resultado) montarResultado();
        entra.hidden = false;

        /* Reflow: sem ele o navegador junta o "deixou de ser display:none" e
           o "ganhou .is-in" no mesmo recálculo, e a transição não roda. */
        void entra.offsetWidth;
        entra.classList.add('is-in');

        atualizarBarra();
        if (entra !== resultado) anunciar('Pergunta ' + (i + 1) + ' de ' + passos.length);

        var alvo = (entra === resultado && escolhido)
          ? escolhido.querySelector('.perf-foco')
          : entra.querySelector('.perf-foco');
        if (alvo) alvo.focus({ preventScroll: true });

        /* O reveal dos blocos do resultado é o do core: os filhos levam
           data-rv, e a varredura é idempotente. */
        if (entra === resultado && escolhido && window.LIB && window.LIB.scan) {
          window.LIB.scan(escolhido);
        }
        animando = false;
      }

      if (reduzido()) {
        fim();
      } else {
        sai.classList.add('is-out');
        setTimeout(fim, SAIDA);
      }
    }

    function avancar() { if (marcada(i)) ir(i + 1); }

    /* ---------------------------------------------------------------- eventos */

    raiz.addEventListener('pointerdown', function () { viaPonteiro = true; }, { passive: true });

    raiz.addEventListener('keydown', function (e) {
      viaPonteiro = false;
      if (e.key !== 'Enter') return;
      var alvo = e.target;
      if (!alvo || !alvo.classList || !alvo.classList.contains('perf-radio')) return;
      e.preventDefault();
      cancelar();
      avancar();
    });

    raiz.addEventListener('change', function (e) {
      var alvo = e.target;
      if (!alvo || !alvo.classList || !alvo.classList.contains('perf-radio')) return;
      if (seguir) seguir.disabled = false;
      if (!reduzido() && viaPonteiro) {
        cancelar();
        timer = setTimeout(function () { timer = 0; avancar(); }, AVANCO);
      }
    });

    if (seguir) seguir.addEventListener('click', function () { cancelar(); avancar(); });
    if (voltar) voltar.addEventListener('click', function () { cancelar(); ir(i - 1); });

    if (refazer) refazer.addEventListener('click', function () {
      var entradas = raiz.querySelectorAll('.perf-radio'), k;
      cancelar();
      for (k = 0; k < entradas.length; k++) entradas[k].checked = false;
      ir(0);
      anunciar('Diagnóstico reiniciado. Pergunta 1 de ' + passos.length + '.');
    });

    /* Não há botão de submit, então nada submete — mas Enter num campo de
       formulário ainda tentaria, e um recarregamento aqui apagaria as
       respostas em silêncio. */
    if (form) form.addEventListener('submit', function (e) { e.preventDefault(); });
  }

  for (var n = 0; n < raizes.length; n++) iniciar(raizes[n]);

})();

Uma dobra escura, sem imagem, em que a pergunta é a peça gráfica: enunciado na .display-xl do core, e abaixo três ou quatro respostas como linhas grandes — número, texto e um filete que acende na cor de marca. Sem cartão, sem caixa, sem barra de progresso: a lista é o desenho. Um contador 01 / 05 num canto, um "Voltar" pequeno, e o movimento da peça é a própria troca de pergunta — a que sai desaparece com blur e subida, a que entra vem palavra a palavra. No fim, um perfil nomeado entre quatro, com um parágrafo de leitura, três traços e uma faixa de alocação em texto.

O problema editorial é o do primeiro contato que não é um formulário. Quem chega ao site de uma assessoria não quer preencher nome, e-mail e telefone para descobrir se aquilo é para ele; quer se reconhecer. Cinco perguntas de uma linha, respondidas em menos de um minuto, entregam duas coisas ao mesmo tempo: a pessoa sai com um vocabulário para descrever o próprio caso, e o assessor começa a conversa em outro ponto. É por isso que a peça termina num botão e não num campo — a captura é trabalho do conversao/cta-formulario-lead, e a separação entre os dois é o que mantém este componente fora do terreno de dado pessoal.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Olho Uma palavra. "Diagnóstico", "Perfil", "Comece por aqui".
Convite h2.sec-title de uma linha, com o <em> na palavra que a pessoa está procurando. Diz quanto custa em tempo, e o apoio abaixo diz que nada é gravado — as duas objeções que fazem alguém não começar.
Enunciado Uma linha, no máximo 10 palavras, terminada em interrogação. Deve caber em 2 ou 3 linhas de .display-xl; acima disso vira parágrafo em corpo grande e a peça perde o registro. Escrito na segunda pessoa.
Resposta 3 ou 4 por pergunta, nunca 5. Frase humana em primeira pessoa ("Vendo o que der, para parar a perda"), não escala técnica ("baixa tolerância a risco"). Uma linha cada, com ponto final.
Ordem das respostas Sempre do mais conservador ao mais tolerante, na mesma direção em todas as perguntas. Alternar a direção para "testar atenção" é hostil, e estraga a leitura da lista.
Perfil Nome de uma palavra, substantivo, sóbrio. Guardião, Construtor, Navegador, Legado. Nada de metal, animal, planeta ou nível.
Leitura do perfil Um parágrafo de 3 a 4 linhas que descreve a situação, não elogia a pessoa. Termina apontando o próximo assunto da conversa.
Traços Exatamente três, sem verbo, sem ponto. São rótulos, não frases.
Faixa de alocação Três classes, sempre em faixa (45–65%), nunca em número cravado. Ver Compliance.

Especificação dos assets

Nenhum. É a única peça desta leva sem imagem, e isso é a forma: o que ocupa a dobra é tipografia. Uma foto ao lado da pergunta transformaria o componente no irmão simulador-carteira. O que a seção precisa é de espaço vertical — a 1440×900 ela mede 1202px de altura, com um palco de 515px que não muda entre as cinco perguntas — e de uma âncora de destino para o href="#diagnostico" do botão final.


Compliance

O que o consumidor tem de trocar antes de publicar:

  1. Os pesos e as faixas. data-perf-peso em cada resposta (0 a 3) e data-perf-min / data-perf-max em cada perfil são exemplo editorial, marcados no markup com <!-- CONTEÚDO ILUSTRATIVO -->. Recalibrar com a área técnica do cliente.
  2. As faixas de alocação. São ilustrativas e têm de sair como faixa, nunca como percentual cravado: 62% lê como carteira recomendada, 45–65% lê como ponto de partida de conversa. Se o compliance do cliente preferir, troque por rótulo qualitativo ("peso maior em renda fixa") e apague os percentuais.
  3. A ressalva. O .perf-nota fica no passo do resultado, onde a pessoa lê o perfil — não em letra miúda no rodapé. O texto que está lá diz o essencial: leitura de conversa, não substitui o questionário de perfil do investidor exigido na abertura de conta, não é recomendação e não promete rentabilidade. Passar pela área de compliance do cliente antes de publicar.
  4. A voz dos textos. Nenhum superlativo de mercado, nenhum nome de instituição real, nenhuma menção a rentabilidade — nem no perfil, nem nas respostas.

Dado pessoal. A peça não coleta nome, e-mail nem telefone, e não grava nada: as respostas vivem nos próprios <input> e morrem no recarregamento. Sem localStorage, sem cookie, sem envio. São dados financeiros de alguém, e captura é trabalho do cta-formulario-lead, para onde o botão do resultado aponta.


Como usar

<link rel="stylesheet" href="componentes/conversao/diagnostico-perfil/estilo.css">
<script src="componentes/conversao/diagnostico-perfil/script.js" defer></script>

O script precisa do core/motion.js carregado antes (lê LIB.reduce, LIB.onScroll e LIB.scan), e a página precisa de uma âncora #diagnostico.

Ganchos

Gancho Papel
data-perf A seção. Raiz de toda busca do script; sem ela, saída silenciosa.
data-perf-peso Peso da resposta, 0 a 3, no <input>. Ilustrativo.
data-perf-min / data-perf-max Faixa de pontos de cada perfil, no bloco do perfil. Mora no HTML porque quem revisa a calibragem lê markup.
--perf-perg Corpo do enunciado. Alimenta o --display-xl do core.
--display-xl-lh Parafuso do core; 1.02 aqui, contra o .95 padrão.
--perf-op-fs Corpo da resposta. É a unidade do max-width da lista.
--perf-topo Respiro acima do enunciado. Mora no passo, não no palco — ver gotcha.
--perf-palco-medido / --perf-palco-min Piso do palco: o script escreve o primeiro, o CSS lê o segundo.
--perf-saida Saída do passo, .26s. Bate com SAIDA no script.
--perf-w-passo / --perf-i Atraso entre palavras e o índice de cada uma, escrito pelo script.
.is-live Script vivo. Sem ela nada é escondido: é o estado sem JavaScript.
.is-in / .is-out O passo que entra e o que sai.
.perf-foco Quem recebe o foco na troca: o legend, e o nome no resultado.
.perf-anuncio role="status" aria-live="polite": "Pergunta 3 de 5". Leva a primitiva .visually-hidden do core, sem regra própria.
.perf-nota A ressalva de compliance.

Variações


Gotchas


Histórico

Projeto Onde Data Diferenças
autoral construído do zero na biblioteca 2026-09-04 —

Origem. Não há referência única e não há uma linha copiada. O padrão — uma pergunta por dobra em corpo de manifesto, respostas como linhas clicáveis numeradas, contador discreto e um perfil nomeado no fim — aparece em três famílias de site: onboarding de banco digital e de corretora (perfil de investidor, abertura de conta), assistente de recomendação em e-commerce de alto valor, e quiz editorial de veículo de imprensa. O que a biblioteca tirou de todas foi a forma da tela, não o código. Status externo: nunca rodou em produção nossa. O primeiro site que usar promove, e a ficha registra qual. O registro consolidado vai em projetos/autorais.md.

Decisões da tradução para o contrato: