<!-- ==========================================================================
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.
========================================================================== -->
<sectionclass="sec-deep sec-pad perf"data-perf>
<divclass="wrap">
<headerclass="perf-head">
<pclass="eyebrow"data-rv>Diagnóstico</p>
<h2class="sec-title ttl perf-convite">Cinco perguntas curtas, e uma leitura do seu <em>perfil</em>.</h2>
<pclass="sec-lead perf-apoio"data-rvdata-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>
<divclass="perf-painel"data-rvdata-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". -->
<divclass="perf-barra"hidden>
<pclass="perf-contador"aria-hidden="true"><spanclass="perf-atual">01</span> / <spanclass="perf-total">05</span></p>
<buttonclass="perf-voltar"type="button"hidden>Voltar</button>
</div>
<pclass="perf-anuncio visually-hidden"role="status"aria-live="polite"></p>
<divclass="perf-palco">
<formclass="perf-form">
<!-- ---------------------------------------------------- 01 -->
<fieldsetclass="perf-passo">
<legendclass="perf-legenda perf-foco"tabindex="-1">
<spanclass="display-xl perf-pergunta">Em quanto tempo esse dinheiro precisa estar disponível?</span>
</legend>
<divclass="perf-ops">
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-horizonte"data-perf-peso="0">
<spanclass="perf-op-num"aria-hidden="true">01</span>
<spanclass="perf-op-txt">Nos próximos dois anos.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-horizonte"data-perf-peso="1">
<spanclass="perf-op-num"aria-hidden="true">02</span>
<spanclass="perf-op-txt">Entre dois e cinco anos.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-horizonte"data-perf-peso="2">
<spanclass="perf-op-num"aria-hidden="true">03</span>
<spanclass="perf-op-txt">De cinco a dez anos.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-horizonte"data-perf-peso="3">
<spanclass="perf-op-num"aria-hidden="true">04</span>
<spanclass="perf-op-txt">Não tenho data. É para depois de mim.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
</div>
</fieldset>
<!-- ---------------------------------------------------- 02 -->
<fieldsetclass="perf-passo">
<legendclass="perf-legenda perf-foco"tabindex="-1">
<spanclass="display-xl perf-pergunta">A carteira cai 20% em três meses. O que você faz?</span>
</legend>
<divclass="perf-ops">
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-queda"data-perf-peso="0">
<spanclass="perf-op-num"aria-hidden="true">01</span>
<spanclass="perf-op-txt">Vendo o que der, para parar a perda.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-queda"data-perf-peso="1">
<spanclass="perf-op-num"aria-hidden="true">02</span>
<spanclass="perf-op-txt">Ligo para o assessor antes de decidir qualquer coisa.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-queda"data-perf-peso="2">
<spanclass="perf-op-num"aria-hidden="true">03</span>
<spanclass="perf-op-txt">Não mexo. Já esperei antes e deu certo.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-queda"data-perf-peso="3">
<spanclass="perf-op-num"aria-hidden="true">04</span>
<spanclass="perf-op-txt">Procuro onde aportar mais.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
</div>
</fieldset>
<!-- ---------------------------------------------------- 03 -->
<fieldsetclass="perf-passo">
<legendclass="perf-legenda perf-foco"tabindex="-1">
<spanclass="display-xl perf-pergunta">De onde veio a maior parte do patrimônio?</span>
</legend>
<divclass="perf-ops">
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-origem"data-perf-peso="0">
<spanclass="perf-op-num"aria-hidden="true">01</span>
<spanclass="perf-op-txt">De salário e poupança, ao longo de muitos anos.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-origem"data-perf-peso="2">
<spanclass="perf-op-num"aria-hidden="true">02</span>
<spanclass="perf-op-txt">Da venda de um negócio ou de um imóvel.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-origem"data-perf-peso="3">
<spanclass="perf-op-num"aria-hidden="true">03</span>
<spanclass="perf-op-txt">Da família — veio de quem veio antes de mim.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
</div>
</fieldset>
<!-- ---------------------------------------------------- 04 -->
<fieldsetclass="perf-passo">
<legendclass="perf-legenda perf-foco"tabindex="-1">
<spanclass="display-xl perf-pergunta">O que tira o seu sono?</span>
</legend>
<divclass="perf-ops">
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-sono"data-perf-peso="0">
<spanclass="perf-op-num"aria-hidden="true">01</span>
<spanclass="perf-op-txt">Ver o valor cair, mesmo que ele volte depois.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-sono"data-perf-peso="1">
<spanclass="perf-op-num"aria-hidden="true">02</span>
<spanclass="perf-op-txt">Precisar do dinheiro e ele estar preso.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-sono"data-perf-peso="2">
<spanclass="perf-op-num"aria-hidden="true">03</span>
<spanclass="perf-op-txt">Ficar parado enquanto a oportunidade passa.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-sono"data-perf-peso="3">
<spanclass="perf-op-num"aria-hidden="true">04</span>
<spanclass="perf-op-txt">Deixar a família sem estrutura se algo acontecer comigo.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
</div>
</fieldset>
<!-- ---------------------------------------------------- 05 -->
<fieldsetclass="perf-passo">
<legendclass="perf-legenda perf-foco"tabindex="-1">
<spanclass="display-xl perf-pergunta">O que você espera de um assessor?</span>
</legend>
<divclass="perf-ops">
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-assessor"data-perf-peso="0">
<spanclass="perf-op-num"aria-hidden="true">01</span>
<spanclass="perf-op-txt">Que me segure na hora de tomar decisão ruim.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-assessor"data-perf-peso="2">
<spanclass="perf-op-num"aria-hidden="true">02</span>
<spanclass="perf-op-txt">Que monte um plano e me cobre dele.</span>
<spanclass="perf-op-marca"aria-hidden="true"></span>
</label>
<labelclass="perf-op">
<inputclass="perf-radio"type="radio"name="perf-assessor"data-perf-peso="3">
<spanclass="perf-op-num"aria-hidden="true">03</span>
<spanclass="perf-op-txt">Que cuide da estrutura, do imposto e da sucessão.</span>
<spanclass="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. -->
<divclass="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. -->
<divclass="perf-perfil"data-perf-min="0"data-perf-max="4"hidden>
<pclass="perf-rotulo"data-rv>Seu perfil</p>
<h3class="display-xl perf-nome perf-foco"tabindex="-1"data-rv>Guardião</h3>
<pclass="perf-leitura"data-rvdata-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>
<ulclass="perf-tracos"data-rvdata-d="2">
<liclass="perf-traco">Horizonte curto e com data marcada</li>
<liclass="perf-traco">Baixa tolerância a oscilação</li>
<liclass="perf-traco">Liquidez antes de retorno</li>
</ul>
<divclass="perf-faixa"data-rvdata-d="3">
<pclass="perf-faixa-tit">Ponto de partida da conversa</p>
<ulclass="perf-faixa-lista">
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Renda fixa</span> <spanclass="perf-faixa-valor">65–85%</span></li>
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Multimercado</span> <spanclass="perf-faixa-valor">5–20%</span></li>
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Renda variável</span> <spanclass="perf-faixa-valor">0–10%</span></li>
</ul>
</div>
</div>
<divclass="perf-perfil"data-perf-min="5"data-perf-max="8"hidden>
<pclass="perf-rotulo"data-rv>Seu perfil</p>
<h3class="display-xl perf-nome perf-foco"tabindex="-1"data-rv>Construtor</h3>
<pclass="perf-leitura"data-rvdata-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>
<ulclass="perf-tracos"data-rvdata-d="2">
<liclass="perf-traco">Aporte recorrente, não evento único</li>
<liclass="perf-traco">Horizonte de cinco a dez anos</li>
<liclass="perf-traco">Oscilação aceita dentro de um plano escrito</li>
</ul>
<divclass="perf-faixa"data-rvdata-d="3">
<pclass="perf-faixa-tit">Ponto de partida da conversa</p>
<ulclass="perf-faixa-lista">
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Renda fixa</span> <spanclass="perf-faixa-valor">45–65%</span></li>
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Multimercado</span> <spanclass="perf-faixa-valor">10–25%</span></li>
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Renda variável</span> <spanclass="perf-faixa-valor">15–30%</span></li>
</ul>
</div>
</div>
<divclass="perf-perfil"data-perf-min="9"data-perf-max="11"hidden>
<pclass="perf-rotulo"data-rv>Seu perfil</p>
<h3class="display-xl perf-nome perf-foco"tabindex="-1"data-rv>Navegador</h3>
<pclass="perf-leitura"data-rvdata-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>
<ulclass="perf-tracos"data-rvdata-d="2">
<liclass="perf-traco">Tolerância a oscilação já testada na prática</li>
<liclass="perf-traco">Diversificação internacional e por moeda</li>
<liclass="perf-traco">Rebalanceamento por regra, não por humor</li>
</ul>
<divclass="perf-faixa"data-rvdata-d="3">
<pclass="perf-faixa-tit">Ponto de partida da conversa</p>
<ulclass="perf-faixa-lista">
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Renda fixa</span> <spanclass="perf-faixa-valor">30–50%</span></li>
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Multimercado</span> <spanclass="perf-faixa-valor">15–30%</span></li>
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Renda variável e exterior</span> <spanclass="perf-faixa-valor">25–45%</span></li>
</ul>
</div>
</div>
<divclass="perf-perfil"data-perf-min="12"data-perf-max="15"hidden>
<pclass="perf-rotulo"data-rv>Seu perfil</p>
<h3class="display-xl perf-nome perf-foco"tabindex="-1"data-rv>Legado</h3>
<pclass="perf-leitura"data-rvdata-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>
<ulclass="perf-tracos"data-rvdata-d="2">
<liclass="perf-traco">Horizonte de geração, sem data</li>
<liclass="perf-traco">Estrutura antes de alocação</li>
<liclass="perf-traco">Espaço para ativo ilíquido e para o exterior</li>
</ul>
<divclass="perf-faixa"data-rvdata-d="3">
<pclass="perf-faixa-tit">Ponto de partida da conversa</p>
<ulclass="perf-faixa-lista">
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Renda fixa</span> <spanclass="perf-faixa-valor">25–45%</span></li>
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Multimercado</span> <spanclass="perf-faixa-valor">15–30%</span></li>
<liclass="perf-faixa-item"><spanclass="perf-faixa-classe">Renda variável e alternativos</span> <spanclass="perf-faixa-valor">30–50%</span></li>
</ul>
</div>
</div>
<pclass="nota perf-nota"data-rvdata-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>
<divclass="perf-fim"data-rvdata-d="3">
<aclass="btn btn-primary perf-cta"href="#diagnostico">Falar com um assessor</a>
<buttonclass="btn btn-ghost perf-refazer"type="button">Refazer</button>
</div>
</div>
</div>
<divclass="perf-acoes"hidden>
<buttonclass="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. -->
<divclass="perf-sem-js">
<pclass="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>
<aclass="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 .3svar(--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) 00;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 .3svar(--ease);
}
.perf-op-txt{
font-family:var(--sans);
font-size:1em;line-height:1.26;letter-spacing:.005em;
color:var(--fg-2);
transition:color .3svar(--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 .42svar(--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 .62svar(--ease),
filter .62svar(--ease),
transform .62svar(--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 .7svar(--ease),
filter .7svar(--ease),
transform .7svar(--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 = resultadovar 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
No meio da página, depois de a proposta já ter sido apresentada e antes do bloco de conversão. É uma pausa ativa entre duas dobras de leitura passiva.
Quando o produto é uma conversa, e não uma compra: o resultado precisa de um humano depois dele.
Quando o público não é homogêneo e a página fala com todos ao mesmo tempo. O diagnóstico é o que permite dizer coisas diferentes a pessoas diferentes sem escrever quatro páginas.
Uma vez por página. Dois assistentes na mesma rolagem cansam antes do segundo.
Quando o que se quer é um painel ao lado de uma foto, com campos variados e um gráfico no fim: conversao/simulador-carteira. Ver o gotcha sobre por que os dois não são o mesmo componente.
Como abertura de página. A dobra de topo precisa dizer o que a empresa faz antes de perguntar qualquer coisa. E quando as perguntas são de resposta livre ou múltipla escolha: aqui todo passo é escolha única, e é o que sustenta o avanço automático e a navegação por seta.
Quando o objetivo é qualificar lead para o comercial, ou quando as respostas precisam sobreviver ao recarregamento. Isto não é um formulário disfarçado: nada sai do navegador e nada é gravado — decisão, não limitação. Para qualificar, use o conversao/cta-formulario-lead e pergunte lá, com consentimento.
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:
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.
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.
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.
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.
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
três ou sete perguntas — acrescente ou tire um .perf-passo. Nada no script conta cinco; só as faixas dos perfis precisam ser reescaladas.
dois perfis — apague dois .perf-perfil e cubra 0 a 15 sem buraco entre as faixas.
sem avanço automático — AVANCO = 0 no script. O botão Continuar já é o caminho completo.
em seção clara — .sec-paper no lugar de .sec-deep; tudo inverte por token, mas confira o contraste do filete aceso.
sem reveal de entrada — apague o data-rv do .perf-painel; a peça passa a valer com o JavaScript da página inteiro desligado.
resultado que abre o formulário — o botão final revela o cta-formulario-lead na mesma dobra, em vez de rolar até ele.
Gotchas
O estado sem JavaScript é o estado de PARTIDA do markup, e não um <noscript>. As cinco perguntas nascem visíveis e empilhadas; quem esconde os passos 2 a 5, a barra e o botão é o script. Medido com o script.js bloqueado: 5 fieldset visíveis, 18 radios, resultado e barra escondidos, a nota do .perf-sem-js de pé com o botão para #diagnostico, seção de 2889px, sem estouro horizontal. O inverso — CSS escondendo e script mostrando — dá uma casca vazia quando o script falha, que é o pior estado possível numa peça que ocupa uma dobra inteira.
Com o core/motion.js também fora, o painel fica em opacity:0. É o data-rv do core, que só ganha .in por JavaScript, e vale para todo componente da biblioteca — não é desta peça. Medido: painelOpacity: 0. Quem precisa sobreviver a bloqueador de script usa a variação sem reveal de entrada.
O avanço automático é só para quem clica, e essa é a decisão mais importante do arquivo. 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 à pergunta seguinte antes de chegar na resposta que queria. O script guarda a modalidade da última interação (pointerdown liga, keydown desliga) e só agenda o avanço quando veio de ponteiro. Medido: por mouse de verdade, 654ms do clique até a pergunta nova no lugar (380ms de espera para a linha marcada acender, mais 260ms de saída); por ArrowDown, nada acontece nem depois de 900ms. Avanço automático é agradável para quem clica e é hostil para quem navega devagar — a saída não é escolher um dos dois, é servir cada um pelo caminho dele.
O botão "Continuar" existe sempre, e não só em movimento reduzido. Ele nasce disabled e libera quando o passo é respondido — medido em todos os cinco passos. Em movimento reduzido ele é o único caminho (o avanço automático não roda: medido, 1000ms depois do clique a peça continua no passo 01). Escondê-lo quando o avanço automático está ligado deixaria quem chega de teclado sem nenhum controle visível de avanço, já que o avanço dele nunca dispara.
O radio está esticado sobre a linha inteira com appearance:none, e não escondido com opacity:0. Dois motivos, e o segundo é o que decide: com opacity:0 o anel de foco some junto com o widget — o elemento inteiro fica invisível, contorno incluído — e quem navega por teclado perde o indicador. Do jeito que está, o :focus-visible que o core desenha cai exatamente em volta da resposta: medido, 2px em --accent numa caixa de 625×59. E o clique em qualquer ponto da linha chega ao input sem depender de for/id, o que deixa o componente aparecer duas vezes na mesma página sem colisão de id.
O legend e o nome do perfil levam width:fit-content, e é por causa do anel de foco. Um legend em bloco ocupa a coluna inteira: medido, 1096px de anel em volta de um enunciado de 667px. Quem chega de teclado via um retângulo solto, não a pergunta marcada. Com fit-content o anel abraça o texto. Vale o mesmo para o h3 do resultado, onde a diferença é entre um anel de 1096px e um em volta da palavra "Navegador".
O piso do palco é medido pelo script, e nenhum clamp de CSS faz esse trabalho. As cinco perguntas medem 483, 515, 366, 397 e 366px: sem piso, cada avanço dá até 149px de solavanco e a página inteira dança sob quem está lendo. A altura de cada passo depende de quantas linhas o enunciado quebra, que muda com o corpo da letra, com a face do tema e com a tradução do texto — um número cravado erraria em metade das larguras. O script mostra e reesconde os cinco passos no mesmo bloco síncrono (o navegador só pinta no fim da tarefa, então nada pisca), escreve o maior em --perf-palco-medido, e refaz a conta quando a largura muda, assinando o laço único do core em vez de registrar listener próprio. Medido depois: 515px em todos os cinco passos.
O --perf-topo mora no passo, não no palco. É o respiro entre a barra e o enunciado, e parece detalhe. Não é: o script mede a altura do passo para calcular o piso, e padding no palco ficaria de fora da conta — o piso sairia curto pela exata medida do respiro, e os dois passos mais altos continuariam empurrando a página. Aconteceu, e o sintoma foi um palco que ficava em 479px enquanto o passo 02 pedia 515px.
A media query de celular escreve --perf-palco-min, e não --perf-palco-medido. O script põe a medida no style da seção, e style inline vence qualquer regra de folha. Escrever o mesmo nome na media query não desligaria o piso no celular — a variável intermediária é o que permite ao CSS ter a última palavra sobre a medida do script. E o piso é desligado abaixo de 560px de propósito: ali a pergunta já ocupa a tela inteira, e o piso só abriria um vão morto embaixo da resposta mais curta.
A troca de passo não usa data-rv, e não pode usar. O data-rv do core é observer de entrada na tela: ele dispara uma vez, desobserva e nunca mais volta. Uma peça que troca de conteúdo cinco vezes no mesmo lugar precisa de transição por classe. O que o core continua fazendo aqui é o que ele sabe fazer: revelar o painel na chegada da dobra, e revelar os blocos do resultado quando ele aparece — para isso o script tira o .in de uma rodada anterior e chama LIB.scan(), que é idempotente.
A primeira pergunta acende quando a dobra chega, e não na carga. Se acendesse na carga, a animação palavra a palavra terminaria invisível numa peça que fica no meio da página, e a pessoa receberia a pergunta pronta. Quem resolve isso é LIB.onScroll — o laço único do core, que o §7.4 do contrato manda assinar em vez de registrar listener próprio —, com a assinatura se desfazendo assim que acende.
O índice da palavra vai no style de cada .perf-w, e isso não é valor cru no markup. É --perf-i:3, um número que o CSS multiplica pelo --perf-w-passo. Cravar o escalonamento em :nth-child amarraria o atraso à posição, e reescrever a pergunta (que é o que todo projeto faz) desalinharia a animação sozinho. Mesmo recurso do --frase-cor do conteudo/manifesto-com-imagens.
.perf [hidden]{display:none} no topo do arquivo não é redundância. O [hidden] da folha do navegador tem especificidade (0,1,0) e perde para qualquer classe que declare display — e o componente declara display:grid e display:flex em vários lugares. Sem essa regra, os passos escondidos reapareceriam empilhados. É a mesma pegadinha que o .hgal-nav[hidden] da midia/galeria-scroll-horizontal documenta.
min-inline-size:0 no fieldset é obrigatório. A folha do navegador dá min-inline-size:min-content ao fieldset, e uma resposta longa que não quebra estoura a coluna em tela estreita. Medido a 390px depois: overflowX: ok, nenhum elemento passando da borda.
Isto não é uma variação do conversao/simulador-carteira, apesar de os dois serem assistentes. Aquele é um painel ao lado de uma foto, na dobra de topo, com campos de tipos diferentes (radio em cartão, select, stepper, chips) e uma rosca de alocação no fim — a forma diz "ferramenta". Este é tipografia na largura da coluna, no meio da página, sem imagem, com um tipo de campo só e um perfil em texto — a forma diz "conversa". Os dois cabem na mesma página sem se repetir, e trocar a forma de um pelo outro apaga a razão de existirem dois.
A pontuação é uma soma linear, e isso é uma simplificação assumida. Cinco perguntas de peso 0 a 3, faixas em 0–4, 5–8, 9–11 e 12–15. Medido: todas as primeiras respostas dão 0 → Guardião; as segundas dão 7 → Construtor; uma combinação de terços dá 10 → Navegador; todas as últimas dão 15 → Legado. Uma soma linear trata as cinco perguntas como igualmente informativas, o que não é verdade — horizonte pesa mais que expectativa sobre o assessor. Quem for calibrar de verdade multiplica o peso por pergunta, e a conta continua cabendo no data-perf-peso.
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:
O enunciado consome a primitiva .display-xl do core pelos dois parafusos, em vez de recopiar as seis linhas de título display (§6 do contrato). A entrelinha sobe de .95 para 1.02 porque aqui há frase com descendente, e não título de uma palavra.
Respostas são <input type="radio"> reais dentro de <label>, sem for/id: o par for/id colidiria se o componente aparecesse duas vezes na página, e o contrato proíbe id no CSS e no JS.
Zero cor crua: filete, texto marcado, número e ressalva leem --rule, --accent, --fg, --fg-2 e --fg-muted do escopo da seção.
O respiro entre cabeçalho e painel sai de calc(var(--sec-gap) * .34) — a fonte única do ritmo vertical (§4), não um clamp solto.
Movimento: entrada do painel pelo data-rv do core; troca de passo por transição própria com filter:blur() indo a zero, como o §5 exige; e nada anda sozinho em prefers-reduced-motion — medido, transition-duration: 0s na palavra e no filete, troca em 33ms, avanço automático desligado.
A região aria-live consome a primitiva .visually-hidden do core em vez de recopiar as seis linhas de esconder-do-olho-mas-não-do-leitor (§6). É o primeiro consumidor dela na biblioteca.
Acessibilidade que o padrão original raramente traz: fieldset + legend por passo, foco levado ao enunciado novo com preventScroll, região aria-live com "Pergunta 3 de 5", contador com aria-hidden, passos fora de cena com hidden (nunca só opacity), e o fluxo inteiro percorrido só por teclado — 1 Tab do corpo até a primeira resposta, setas para escolher, Enter ou Tab+Enter no Continuar para avançar, 2 Tab do resultado até o "Refazer".