markup.html · 333 linhas · 17,3 KB
<!-- ==========================================================================
SIMULADOR DE CARTEIRA
Prefixo: sim-
Requer: core (tokens, base, motion.css, motion.js) + estilo.css e script.js
Sem lib.
A DOBRA. Tela quase cheia partida em dois: à esquerda a foto sangrada com
o título display por cima; à direita, sobreposto e recuado até a coluna,
um painel alto que é um ASSISTENTE DE QUATRO PASSOS. A pessoa responde
objetivo, composição, horizonte e conhecimento, e o quarto passo devolve
uma rosca de alocação com a leitura em texto.
O QUE A PEÇA ENTREGA NO FIM: uma CONVERSA, não um cadastro. Nenhum campo
pede nome, e-mail ou telefone, nada é gravado em localStorage e as
respostas morrem no recarregamento. Quem coleta dado é o
conversao/cta-formulario-lead, para onde o botão final aponta.
QUATRO TIPOS DE CAMPO DE PROPÓSITO — a dobra é também um catálogo de
campos para o consumidor reaproveitar:
1. cartões de escolha única com ícone -> <input type="radio">
2. <select> nativo estilizado + seletor de faixa com − e +
3. chips de escolha única, em duas fileiras
4. resultado: rosca SVG + leitura em texto + ressalva + saída
ESTADO SEM JAVASCRIPT. Os quatro passos nascem TODOS visíveis, um embaixo
do outro, como formulário comum — nenhum `hidden` no markup. Quem esconde
os passos 2 a 4 é o script, na inicialização. Pelo mesmo motivo a régua, o
"Voltar" e o "Continuar" nascem `hidden` e são revelados pelo script:
botão de avançar que não avança é pior que botão nenhum. O <noscript>
explica a situação em uma linha. Assim a peça nunca é casca vazia.
NENHUM data-rv NO MARKUP, e é de propósito. Quem entra marca data-sim-rv,
e o script troca por data-rv e chama LIB.scan. [data-rv] escrito aqui
nasceria em opacity:0 e só o motion.js o acende: numa página sem
JavaScript o título e o formulário inteiro sumiriam. Esta dobra é a que
menos pode se dar a esse luxo — ela é uma ferramenta, não decoração. O
data-d segue valendo: ele é lido pelo core depois da troca.
COMPLIANCE. As porcentagens são ilustrativas e a ressalva vive DENTRO do
passo do resultado, em corpo de leitura — não em letra miúda de rodapé.
Nada aqui fala em rentabilidade: a rosca mostra ALOCAÇÃO.
========================================================================== -->
<section class="sim sec-dark" data-sim>
<!-- ---------------------------------------------------------------------
A FOTO
Sangra pela esquerda e pelo topo/rodapé da dobra. O véu é gradiente de
três paradas subindo do rodapé, e o texto mora DENTRO da imagem,
ancorado embaixo — nunca vira parágrafo abaixo da foto (contrato §6).
A borda direita da foto se dissolve por máscara, para o painel pousar
sobre ela sem parecer adesivo colado.
-------------------------------------------------------------------- -->
<div class="sim-media">
<div class="sim-media-frame">
<img class="sim-media-img" src="../../../showroom/demo/ph-wide-1.webp"
alt="" loading="lazy" decoding="async" width="1600" height="900">
<span class="sim-media-veil" aria-hidden="true"></span>
<span class="grain" aria-hidden="true"></span>
</div>
<!-- Largura cheia por fora, coluna por dentro: é o .wrap do core que
alinha o título à coluna da página, e é por isso que este bloco não
mora dentro do recorte da foto. -->
<div class="wrap sim-hgroup">
<div class="sim-copy" data-sim-rv>
<p class="eyebrow">Simulador de carteira</p>
<h2 class="display-xl sim-title">Uma carteira pensada para o seu horizonte.</h2>
<p class="sim-lead">
Responda quatro perguntas e veja como um patrimônio como o seu costuma
ser distribuído.
</p>
</div>
</div>
</div>
<div class="wrap sim-form">
<!-- ---------------------------------------------------------------------
O PAINEL
.glass do core com os parafusos trocados: sobre foto, o vidro claro do
padrão deixaria os campos ilegíveis, então --glass-bg vira escuro quase
opaco. Nenhuma regra da primitiva é reescrita.
-------------------------------------------------------------------- -->
<div class="glass sim-panel" data-sim-panel data-sim-rv data-d="1">
<div class="sim-head">
<!-- A régua é DECORAÇÃO: quatro segmentos que preenchem conforme
avança. A informação verdadeira está no texto ao lado, que é uma
região aria-live e anuncia "Passo 2 de 4" na troca. -->
<div class="sim-rule" aria-hidden="true" data-sim-rule>
<span class="sim-rule-seg is-on"></span>
<span class="sim-rule-seg"></span>
<span class="sim-rule-seg"></span>
<span class="sim-rule-seg"></span>
</div>
<p class="sim-status" role="status" aria-live="polite" data-sim-status>Passo 1 de 4</p>
<!-- "Voltar" nunca é disabled: some no primeiro passo e volta a
existir do segundo em diante. -->
<button class="sim-back" type="button" data-sim-back hidden>
<span class="sim-back-ico" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14.2 5.4 7.6 12l6.6 6.6"/></svg>
</span>
Voltar
</button>
</div>
<noscript>
<p class="sim-nojs">
Sem JavaScript o assistente não calcula: as quatro perguntas ficam à
vista e a distribuição abaixo é o exemplo padrão.
</p>
</noscript>
<div class="sim-body">
<!-- ============================================================ 1/4 -->
<fieldset class="sim-step" data-sim-step>
<legend class="sim-q" tabindex="-1" data-sim-q>O que este patrimônio precisa fazer nos próximos anos?</legend>
<div class="sim-opts">
<label class="sim-opt">
<input class="sim-radio" type="radio" name="sim-goal" value="preservar" data-sim-in="objetivo">
<span class="sim-opt-box">
<span class="sim-opt-ico" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.2 4.6 6.1v5.5c0 4.3 3 7.5 7.4 9.2 4.4-1.7 7.4-4.9 7.4-9.2V6.1z"/><path d="M9.2 12.1 11.3 14.2 15 10.2"/></svg>
</span>
<span class="sim-opt-lab">Preservar o patrimônio</span>
</span>
</label>
<label class="sim-opt">
<input class="sim-radio" type="radio" name="sim-goal" value="renda" data-sim-in="objetivo">
<span class="sim-opt-box">
<span class="sim-opt-ico" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.6"/><path d="M14.7 9.1h-3.9a1.9 1.9 0 0 0 0 3.8h2.4a1.9 1.9 0 0 1 0 3.8H9.3M12 7.2v1.9M12 16.7v1.9"/></svg>
</span>
<span class="sim-opt-lab">Gerar renda</span>
</span>
</label>
<label class="sim-opt">
<input class="sim-radio" type="radio" name="sim-goal" value="crescer" data-sim-in="objetivo">
<span class="sim-opt-box">
<span class="sim-opt-ico" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.6 17.6 9.3 11.2l3.4 3.3 7-7.6"/><path d="M14.4 6.7h5.5v5.5"/></svg>
</span>
<span class="sim-opt-lab">Fazer o patrimônio crescer</span>
</span>
</label>
<label class="sim-opt">
<input class="sim-radio" type="radio" name="sim-goal" value="sucessao" data-sim-in="objetivo">
<span class="sim-opt-box">
<span class="sim-opt-ico" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5.1" r="2.1"/><circle cx="6.4" cy="18.4" r="2.1"/><circle cx="17.6" cy="18.4" r="2.1"/><path d="M12 7.2v4.4M6.4 16.3v-2.5h11.2v2.5M12 11.6v2.2"/></svg>
</span>
<span class="sim-opt-lab">Organizar a sucessão</span>
</span>
</label>
</div>
</fieldset>
<!-- ============================================================ 2/4 -->
<fieldset class="sim-step" data-sim-step>
<legend class="sim-q" tabindex="-1" data-sim-q>Como o patrimônio está composto hoje?</legend>
<!-- O <label> ENVOLVE o campo: o vínculo é implícito e não gasta id.
Componente que pode repetir na página não pode carregar id fixo —
dois iguais quebram os dois. -->
<label class="sim-field">
<span class="sim-lab">Onde está hoje a maior parte</span>
<span class="sim-select-box">
<select class="sim-select" data-sim-in="composicao">
<option value="">Selecione</option>
<option value="financeira">Aplicação financeira</option>
<option value="imoveis">Imóveis</option>
<option value="empresa">Participação em empresa</option>
<option value="agro">Terra e agronegócio</option>
</select>
<span class="sim-select-arrow" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5.4 9.2 12 15.8l6.6-6.6"/></svg>
</span>
</span>
</label>
<!-- Seletor de faixa. As quatro faixas são CONTEÚDO e por isso vivem
no markup, separadas por barra vertical: quem quiser três faixas
apaga uma e o script conta o que sobrou. O valor mora numa região
aria-live, e os dois botões carregam aria-label próprio — "−" e
"+" sozinhos não dizem nada em leitor de tela. -->
<div class="sim-field">
<span class="sim-lab">Faixa de patrimônio</span>
<div class="sim-range" role="group" aria-label="Faixa de patrimônio">
<button class="btn-disc sim-nudge" type="button" aria-label="Faixa anterior"
data-sim-nudge="-1" aria-disabled="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M6.4 12h11.2"/></svg>
</button>
<span class="sim-range-val" aria-live="polite" data-sim-range
data-sim-bands="R$ 1 mi a R$ 3 mi|R$ 3 mi a R$ 10 mi|R$ 10 mi a R$ 30 mi|Acima de R$ 30 mi">R$ 1 mi a R$ 3 mi</span>
<button class="btn-disc sim-nudge" type="button" aria-label="Próxima faixa"
data-sim-nudge="1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M12 6.4v11.2M6.4 12h11.2"/></svg>
</button>
</div>
</div>
</fieldset>
<!-- ============================================================ 3/4 -->
<fieldset class="sim-step" data-sim-step>
<legend class="sim-q" tabindex="-1" data-sim-q>Em quanto tempo o dinheiro pode ser necessário?</legend>
<fieldset class="sim-sub">
<legend class="sim-lab">Horizonte</legend>
<div class="sim-chips">
<label class="sim-chip">
<input class="sim-radio" type="radio" name="sim-horizon" value="curto" data-sim-in="horizonte">
<span class="sim-chip-box">Até 3 anos</span>
</label>
<label class="sim-chip">
<input class="sim-radio" type="radio" name="sim-horizon" value="medio" data-sim-in="horizonte">
<span class="sim-chip-box">3 a 10 anos</span>
</label>
<label class="sim-chip">
<input class="sim-radio" type="radio" name="sim-horizon" value="longo" data-sim-in="horizonte">
<span class="sim-chip-box">Mais de 10 anos</span>
</label>
</div>
</fieldset>
<fieldset class="sim-sub">
<legend class="sim-lab">O quanto você acompanha investimentos</legend>
<div class="sim-chips">
<label class="sim-chip">
<input class="sim-radio" type="radio" name="sim-know" value="nenhum" data-sim-in="conhecimento">
<span class="sim-chip-box">Nenhum</span>
</label>
<label class="sim-chip">
<input class="sim-radio" type="radio" name="sim-know" value="basico" data-sim-in="conhecimento">
<span class="sim-chip-box">Básico</span>
</label>
<label class="sim-chip">
<input class="sim-radio" type="radio" name="sim-know" value="intermediario" data-sim-in="conhecimento">
<span class="sim-chip-box">Intermediário</span>
</label>
<label class="sim-chip">
<input class="sim-radio" type="radio" name="sim-know" value="avancado" data-sim-in="conhecimento">
<span class="sim-chip-box">Avançado</span>
</label>
</div>
</fieldset>
</fieldset>
<!-- ============================================================ 4/4
O resultado. A rosca é aria-hidden inteira; quem usa leitor de tela
recebe a lista abaixo, que é o dado de verdade — classe, percentual
e uma linha de leitura. O markup já nasce no perfil do meio
(Equilíbrio), que é o que fica de pé sem script.
================================================================ -->
<div class="sim-step sim-result" data-sim-step role="group" aria-label="Distribuição de referência">
<h3 class="sim-q" tabindex="-1" data-sim-q>Uma distribuição de referência para o seu perfil</h3>
<div class="sim-out">
<!-- Quatro <circle> sobre o mesmo raio. O script escreve, em cada
um, o giro de partida e o dashoffset final; o desenho é do CSS.
r="46" é a única medida compartilhada com o script, que a lê do
atributo para calcular 2πr — mudar o raio aqui basta. -->
<svg class="sim-donut" viewBox="0 0 120 120" aria-hidden="true" data-sim-donut>
<circle class="sim-track" cx="60" cy="60" r="46"/>
<circle class="sim-arc sim-arc-1" cx="60" cy="60" r="46" data-sim-arc/>
<circle class="sim-arc sim-arc-2" cx="60" cy="60" r="46" data-sim-arc/>
<circle class="sim-arc sim-arc-3" cx="60" cy="60" r="46" data-sim-arc/>
<circle class="sim-arc sim-arc-4" cx="60" cy="60" r="46" data-sim-arc/>
</svg>
<p class="sim-verdict">
<b class="sim-verdict-name" data-sim-name>Equilíbrio</b>
<span class="sim-verdict-txt" data-sim-phrase>Metade sustenta o dia a dia; a outra metade acompanha o mercado dentro do prazo que você tem.</span>
</p>
</div>
<!-- CONTEÚDO ILUSTRATIVO -->
<ul class="sim-slices" role="list">
<li class="sim-slice sim-slice-1">
<span class="sim-slice-dot" aria-hidden="true"></span>
<span class="sim-slice-name">Renda fixa</span>
<b class="sim-slice-pct" data-sim-pct>38%</b>
<span class="sim-slice-note">Caixa e liquidez para o que vier antes do prazo.</span>
</li>
<li class="sim-slice sim-slice-2">
<span class="sim-slice-dot" aria-hidden="true"></span>
<span class="sim-slice-name">Crédito privado</span>
<b class="sim-slice-pct" data-sim-pct>22%</b>
<span class="sim-slice-note">Prazo mais longo, em emissor analisado um a um.</span>
</li>
<li class="sim-slice sim-slice-3">
<span class="sim-slice-dot" aria-hidden="true"></span>
<span class="sim-slice-name">Ações e fundos</span>
<b class="sim-slice-pct" data-sim-pct>24%</b>
<span class="sim-slice-note">A parte que acompanha empresas e mercado.</span>
</li>
<li class="sim-slice sim-slice-4">
<span class="sim-slice-dot" aria-hidden="true"></span>
<span class="sim-slice-name">Internacional</span>
<b class="sim-slice-pct" data-sim-pct>16%</b>
<span class="sim-slice-note">Moeda forte e economias fora do Brasil.</span>
</li>
</ul>
<p class="nota sim-nota">
Simulação ilustrativa, para orientar a conversa. Não é recomendação de
investimento nem promessa de rentabilidade. Cenários passados não
garantem resultado futuro.
</p>
<div class="sim-acoes">
<a class="btn btn-primary sim-cta" href="#diagnostico">Falar com um assessor</a>
<button class="sim-redo" type="button" data-sim-redo>Refazer</button>
</div>
</div>
</div>
<div class="sim-foot" data-sim-foot hidden>
<button class="btn btn-primary sim-next" type="button" data-sim-next disabled>Continuar</button>
</div>
</div><!-- /.sim-panel -->
</div><!-- /.sim-form -->
</section>
estilo.css · 636 linhas · 22,8 KB
/* ==========================================================================
SIMULADOR DE CARTEIRA — prefixo sim-
Dobra partida em dois: foto sangrada à esquerda com o título por cima,
painel-assistente de quatro passos sobreposto à direita.
Tokens usados:
--accent --accent-2 as duas pontas da rampa de cor da rosca
--bg --fg --fg-2 --fg-muted --rule --rule-2 tinta e linhas, do escopo
--dark --dark-rgb véu da foto e corpo do painel de vidro
--display --sans títulos/números e texto
--ease a curva, em toda transição daqui
--radius-media canto do painel e dos campos
--radius-pill chip, pílula e disco
A coluna não aparece nesta lista de propósito: --max e --pad são lidos
pelo .wrap do core, e nenhuma regra daqui os toca (contrato §4).
DUAS CAMADAS, UMA CÉLULA. A seção é uma grade de UMA célula com dois
filhos empilhados: .sim-media (a foto, recortada na fração da esquerda) e
.sim-form (o painel). Os dois são LARGURA CHEIA, e cada um alinha o próprio
conteúdo com o .wrap do core — é o que garante que o título e a borda
direita do painel caiam exatamente na coluna da página, sem conta de
inset e sem margem negativa (contrato §4). No celular a mesma grade vira
duas linhas: foto em faixa em cima, painel embaixo.
O QUE O SCRIPT NÃO FAZ. Ele não escreve aparência: troca `hidden`, troca
classe e escreve as DUAS variáveis de geometria de cada fatia da rosca
(giro de partida e dashoffset final). Cor, espessura, vão, curva e blur
estão todos aqui.
========================================================================== */
.sim{
/* Fração da dobra ocupada pela foto. O painel entra por cima dela, e o que
importa é a borda esquerda do painel cair DENTRO da dissolução da foto
(que começa em 68% da largura dela). Medido a 1085, 1280, 1440 e 1920:
com 64% a borda cai na dissolução nas quatro; com 60% ela escapava por
7px a 1280 e o painel ficava flutuando ao lado da foto, não sobre ela. */
--sim-media-w:64%;
/* Largura do painel. Encolhe com a tela até o ponto em que o título ao
lado ficaria estreito demais — daí o layout quebra em duas linhas. */
--sim-panel-w:clamp(400px,33vw,476px);
/* Altura mínima da dobra. Não é 100vh: em tela baixa o painel passaria do
rodapé e a pessoa rolaria dentro de um formulário. */
--sim-min-h:clamp(600px,90vh,900px);
/* Respiro interno do painel. */
--sim-pad:clamp(22px,2.1vw,32px);
/* Altura mínima do corpo do painel: o mais alto dos três passos de
pergunta, mais um dedo de folga. É o que impede o painel de encolher e
crescer a cada avanço. Medido com a fonte de recurso: 330 a 1920, 323 a
1280, 320 a 390. O passo do RESULTADO é mais alto que isto de propósito
— ali crescer é o efeito, e o painel ganha ~150px. */
--sim-body-h:360px;
/* A rosca. --sim-arc-w é espessura em unidades do viewBox (120), e
--sim-arc-gap é o vão entre fatias em unidades de caminho — o script lê
este segundo com parseFloat para descontar da conta de cada fatia. */
--sim-donut:164px;
--sim-arc-w:13;
--sim-arc-gap:4;
/* A paleta da rosca é uma RAMPA entre as duas cores da marca, e não quatro
cores cruas: da cor de ação até a secundária, e a quarta é a secundária
escurecida contra o fundo. Medido no tema Liberta, contra o corpo do
painel: 3,8:1 na fatia mais escura — acima do 3:1 que o WCAG pede para
objeto gráfico. Tema de baixo contraste entre --accent e --accent-2
precisa refazer esta conta antes de publicar. */
--sim-tint-1:var(--accent);
--sim-tint-2:color-mix(in srgb,var(--accent) 55%,var(--accent-2));
--sim-tint-3:var(--accent-2);
--sim-tint-4:color-mix(in srgb,var(--accent-2) 70%,var(--bg));
position:relative;
display:grid;
grid-template-columns:minmax(0,1fr);
min-height:var(--sim-min-h);
}
/* O `hidden` do passo, do rodapé e do "Voltar" tem de vencer o display que
este arquivo dá a cada um deles. Regra do agente do usuário perde para
regra de autor por especificidade nenhuma — perde sempre —, então a
reposição é nossa. Uma linha, escopada no componente, resolve os três.
Sem ela, campo invisível continua tabulável: a armadilha clássica. */
.sim [hidden]{display:none}
/* ==========================================================================
CAMADA 1 — A FOTO E O TÍTULO
========================================================================== */
.sim-media{
grid-area:1/1;
position:relative;
display:grid;
align-items:end;
min-width:0;
}
/* O recorte da foto. Ocupa a fração da esquerda e sangra em cima e embaixo.
A máscara dissolve a borda direita no fundo da seção: sem ela o painel
pousa sobre um corte reto e lê como adesivo colado na foto. */
.sim-media-frame{
position:absolute;inset:0;
width:var(--sim-media-w);
overflow:hidden;
-webkit-mask-image:linear-gradient(to right,#000 68%,transparent 100%);
mask-image:linear-gradient(to right,#000 68%,transparent 100%);
}
.sim-media-img{
position:absolute;inset:0;
width:100%;height:100%;
object-fit:cover;object-position:50% 42%;
}
/* Véu de três paradas subindo do rodapé: escuro onde o texto pousa, quase
limpo no topo. rgba(var(--dark-rgb),…) é a forma da casa para véu — é
token, não cor crua. */
.sim-media-veil{
position:absolute;inset:0;
background:linear-gradient(to top,
rgba(var(--dark-rgb),.92) 0%,
rgba(var(--dark-rgb),.62) 38%,
rgba(var(--dark-rgb),.14) 100%);
}
/* O título mora DENTRO da imagem, ancorado embaixo (contrato §6). O .wrap é
quem o alinha à coluna da página — este bloco é largura cheia justamente
para que o .wrap de dentro faça a conta certa. */
.sim-hgroup{
position:relative;z-index:1;
padding-block:clamp(40px,7vh,88px);
pointer-events:none;
}
/* A largura do título é a coluna MENOS o painel e o vão: assim ele nunca
corre por baixo do painel, em nenhuma largura de tela. */
.sim-copy{
max-width:calc(100% - var(--sim-panel-w) - clamp(24px,3vw,56px));
pointer-events:auto;
}
.sim-title{
--display-xl:clamp(32px,3.9vw,58px);
--display-xl-lh:1.06;
margin-top:18px;
}
.sim-lead{
margin-top:20px;
max-width:44ch;
color:var(--fg-2);
font-size:clamp(15px,1.15vw,17px);line-height:1.55;
}
/* ==========================================================================
CAMADA 2 — O PAINEL
Mesma célula da grade, por cima. É largura cheia e usa o .wrap para
encostar a borda direita do painel na coluna.
========================================================================== */
.sim-form{
grid-area:1/1;
position:relative;z-index:2;
display:grid;
align-items:center;
padding-block:clamp(48px,8vh,96px);
pointer-events:none;
}
.sim-panel{
/* O vidro do core, sobre foto. O padrão é claro e translúcido — aqui os
campos ficariam ilegíveis, então o corpo vira escuro quase opaco. Nenhuma
regra da primitiva é reescrita: só os parafusos dela. */
--glass-bg:color-mix(in srgb,var(--dark) 88%,transparent);
--glass-line:color-mix(in srgb,var(--fg) 15%,transparent);
--glass-blur:26px;
pointer-events:auto;
width:min(var(--sim-panel-w),100%);
margin-left:auto;
border-radius:var(--radius-media);
padding:var(--sim-pad);
display:flex;flex-direction:column;
gap:clamp(18px,1.8vw,24px);
}
/* ---- cabeça do painel: régua, contador e voltar ------------------------- */
.sim-head{display:grid;gap:14px}
.sim-rule{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
.sim-rule-seg{
height:2px;
background:var(--rule-2);
position:relative;overflow:hidden;
}
/* O segmento preenche da esquerda para a direita, e é só isto que a régua
faz — ela é aria-hidden. Quem anuncia é o .sim-status. */
.sim-rule-seg::after{
content:"";position:absolute;inset:0;
background:var(--accent);
transform:scaleX(0);transform-origin:left center;
transition:transform .5s var(--ease);
}
.sim-rule-seg.is-on::after{transform:scaleX(1)}
@media(prefers-reduced-motion:reduce){
.sim-rule-seg::after{transition:none}
}
.sim-status{
font-family:var(--display);
font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;
color:var(--fg-muted);
}
/* "Voltar" é botão de texto, não pílula: ele volta, não avança, e não pode
competir com o "Continuar". Nunca fica disabled — some no primeiro passo. */
.sim-back{
justify-self:start;
display:inline-flex;align-items:center;gap:7px;
padding:0;border:0;background:none;
font-family:var(--sans);font-size:13.5px;letter-spacing:.02em;
color:var(--fg-muted);
transition:color .3s var(--ease);
}
.sim-back:hover{color:var(--accent)}
.sim-back-ico{
display:block;width:15px;height:15px;
transition:transform .3s var(--ease);
}
.sim-back-ico svg{display:block;width:100%;height:100%}
/* O chevron recua no hover — o mesmo gesto do .btn do core, ao contrário. */
.sim-back:hover .sim-back-ico{transform:translateX(-2px)}
@media(prefers-reduced-motion:reduce){
.sim-back-ico{transition:none}
}
/* Só aparece com o script desligado: o <noscript> não gera caixa nenhuma
quando há JavaScript. Por isso esta regra NÃO declara display — declarar
faria a linha aparecer também com o script ligado. */
.sim-nojs{
color:var(--fg-muted);
font-size:13.5px;line-height:1.5;
padding-bottom:4px;
border-bottom:1px solid var(--rule);
}
/* ---- corpo: os passos --------------------------------------------------- */
.sim-body{min-height:var(--sim-body-h)}
.sim-step{
--sim-gap:clamp(16px,1.7vw,22px);
border:0;margin:0;padding:0;min-width:0;
display:grid;gap:var(--sim-gap);
/* A entrada do passo é ANIMAÇÃO, não transição: quem sai de display:none
não interpola. A animação reinicia sozinha toda vez que o passo volta a
ser exibido — o script só tira o `hidden`. */
animation:sim-in .5s var(--ease) both;
}
@media(prefers-reduced-motion:reduce){
.sim-step{animation:none}
}
@keyframes sim-in{
from{opacity:0;filter:blur(9px);transform:translateY(10px)}
to{opacity:1;filter:blur(0);transform:none}
}
/* Sem script os quatro passos ficam empilhados: a linha os separa para não
virarem um bloco só. Com o script, .is-live desliga a divisória — o passo
escondido continua sendo irmão do visível, e sem este escopo o passo 2
herdaria a linha de um passo 1 que ninguém está vendo. */
.sim-panel:not(.is-live) .sim-step + .sim-step{
margin-top:clamp(18px,2vw,26px);
padding-top:clamp(18px,2vw,26px);
border-top:1px solid var(--rule);
}
/* A pergunta do passo. É <legend> nos três primeiros e <h3> no resultado, e
nos quatro é o alvo do foco quando o passo entra (tabindex="-1"). */
/* MEDIDO: uma <legend> NÃO é item de grade do <fieldset> — o navegador a
coloca fora do fluxo da grade, e o `gap` do passo não vale para ela. O vão
entre a pergunta e o primeiro campo saía 0, e o anel de foco (2px com recuo
de 2) passava por cima do rótulo de baixo. A margem repõe o vão, lida da
mesma variável do gap para os dois nunca divergirem. No passo do resultado
o título é <h3>, item de grade de verdade: ali a margem sai, senão o vão
sairia dobrado. */
.sim-q{
font-family:var(--display);font-weight:700;
font-size:clamp(19px,1.6vw,23px);line-height:1.25;letter-spacing:-.015em;
color:var(--fg);
padding:0;
margin-bottom:var(--sim-gap);
}
.sim-result .sim-q{margin-bottom:0}
/* O core desenha o anel de foco 3px para fora; num título que já encosta no
topo do painel isso encostaria na borda. O deslocamento menor mantém o
anel inteiro dentro do respiro. */
.sim-q:focus-visible{outline-offset:2px}
.sim-lab{
display:block;padding:0;
font-size:12px;letter-spacing:.14em;text-transform:uppercase;
color:var(--fg-muted);
}
/* O input de verdade continua no fluxo do teclado: escondê-lo com display ou
visibility o tiraria da tabulação e das setas. Some da tela, não da
navegação — e o anel de foco é desenhado na caixa irmã. */
.sim-radio{
position:absolute;
width:1px;height:1px;margin:0;padding:0;border:0;
clip-path:inset(50%);overflow:hidden;white-space:nowrap;
}
/* ---- passo 1: cartões de objetivo --------------------------------------- */
.sim-opts{display:grid;gap:8px}
.sim-opt{position:relative;display:block;cursor:pointer}
.sim-opt-box{
display:flex;align-items:center;gap:14px;
padding:13px 16px;
border:1px solid var(--rule-2);
border-radius:var(--radius-media);
background:color-mix(in srgb,var(--fg) 3%,transparent);
transition:border-color .3s var(--ease),background .3s var(--ease),color .3s var(--ease);
}
.sim-opt:hover .sim-opt-box{border-color:color-mix(in srgb,var(--accent) 55%,transparent)}
.sim-opt-ico{
flex:0 0 34px;width:34px;height:34px;
display:grid;place-items:center;
border:1px solid var(--rule);
border-radius:var(--radius-pill);
color:var(--fg-2);
transition:color .3s var(--ease),border-color .3s var(--ease);
}
.sim-opt-ico svg{width:18px;height:18px;display:block}
.sim-opt-lab{
font-size:15px;line-height:1.3;letter-spacing:0;
color:var(--fg-2);
}
.sim-radio:checked + .sim-opt-box{
border-color:var(--accent);
background:color-mix(in srgb,var(--accent) 9%,transparent);
}
.sim-radio:checked + .sim-opt-box .sim-opt-ico{color:var(--accent);border-color:var(--accent)}
.sim-radio:checked + .sim-opt-box .sim-opt-lab{color:var(--fg)}
.sim-radio:focus-visible + .sim-opt-box{outline:2px solid var(--accent);outline-offset:3px}
/* ---- passo 2: select e seletor de faixa --------------------------------- */
.sim-field{display:grid;gap:9px}
.sim-select-box{position:relative;display:block}
/* Select NATIVO, estilizado — não uma lista falsa. A lista aberta é do
sistema, e por isso ela é o único lugar do componente que não lê token:
color-scheme diz ao navegador para desenhá-la escura, para casar com o
painel. Painel claro (ver variação "em dobra clara") pede trocar por light. */
.sim-select{
appearance:none;-webkit-appearance:none;
width:100%;
padding:14px 44px 14px 15px;
font-family:var(--sans);font-size:15px;letter-spacing:0;
color:var(--fg);
background:color-mix(in srgb,var(--fg) 5%,var(--bg));
border:1px solid var(--rule-2);
border-radius:var(--radius-media);
color-scheme:dark;
transition:border-color .3s var(--ease);
}
.sim-select:hover{border-color:color-mix(in srgb,var(--accent) 55%,transparent)}
.sim-select-arrow{
position:absolute;top:50%;right:15px;
width:17px;height:17px;margin-top:-8px;
color:var(--fg-muted);pointer-events:none;
}
.sim-select-arrow svg{display:block;width:100%;height:100%}
.sim-range{
display:flex;align-items:center;justify-content:space-between;gap:12px;
padding:6px;
border:1px solid var(--rule-2);
border-radius:var(--radius-pill);
}
/* O valor é o conteúdo da região aria-live: muda no clique e é anunciado. */
.sim-range-val{
flex:1 1 auto;text-align:center;
font-family:var(--display);font-weight:700;
font-size:clamp(14px,1.15vw,16px);letter-spacing:-.01em;
color:var(--fg);
}
/* O disco do core, um tamanho abaixo. Nas pontas da fila ele apaga por
aria-disabled e NÃO por disabled: quem chegou de teclado ao "+" e bateu no
fim continua com o foco no botão, em vez de ser jogado para o <body>. */
.sim-nudge{--disc-size:36px}
/* ---- passo 3: chips ----------------------------------------------------- */
/* Mesma armadilha da <legend> aqui dentro: o rótulo do subgrupo é uma
<legend> e não pega o gap. Nos outros lugares .sim-lab é <span> dentro de
uma grade e já pega — por isso a margem é escopada ao subgrupo. */
.sim-sub{border:0;margin:0;padding:0;min-width:0;display:grid;gap:9px}
.sim-sub .sim-lab{margin-bottom:9px}
.sim-chips{display:flex;flex-wrap:wrap;gap:7px}
.sim-chip{position:relative;display:block;cursor:pointer}
.sim-chip-box{
display:block;
padding:9px 15px;
border:1px solid var(--rule-2);
border-radius:var(--radius-pill);
font-size:13.5px;letter-spacing:.01em;line-height:1.2;
color:var(--fg-2);
transition:border-color .3s var(--ease),background .3s var(--ease),color .3s var(--ease);
}
.sim-chip:hover .sim-chip-box{border-color:color-mix(in srgb,var(--accent) 55%,transparent)}
.sim-radio:checked + .sim-chip-box{
border-color:var(--accent);
background:var(--accent);
color:var(--dark);
}
.sim-radio:focus-visible + .sim-chip-box{outline:2px solid var(--accent);outline-offset:3px}
/* ---- passo 4: o resultado ----------------------------------------------- */
.sim-result{--sim-gap:clamp(18px,1.9vw,24px)}
.sim-out{display:flex;align-items:center;gap:clamp(16px,1.8vw,24px)}
.sim-donut{
flex:0 0 auto;
width:var(--sim-donut);height:var(--sim-donut);
}
.sim-track{fill:none;stroke:var(--rule);stroke-width:var(--sim-arc-w)}
/* CADA FATIA É UM <circle> INTEIRO, girado para o seu ponto de partida e
cortado por dasharray. O tracejado tem UM traço do tamanho da volta
(--sim-len, que é 2πr e o script calcula a partir do r do markup); o
dashoffset é o que sobra dela — quanto maior, menor a fatia. O giro de
−90° põe o zero no topo; o giro de partida de cada fatia é acumulado.
Os valores abaixo são a geometria do perfil PADRÃO (Equilíbrio, 38/22/24/16
— os mesmos números que o markup traz na lista e a tabela do script traz
em `equilibrio`). Ficam aqui para a rosca desenhar certo mesmo sem script;
com script, o inline style ganha e escreve o perfil calculado. Trocar o
perfil padrão é mexer nestes três lugares. */
.sim-arc{
fill:none;
stroke-width:var(--sim-arc-w);
stroke-linecap:butt;
stroke-dasharray:var(--sim-len,289.03);
stroke-dashoffset:var(--sim-arc-off,0);
transform-box:view-box;transform-origin:50% 50%;
transform:rotate(calc(-90deg + var(--sim-arc-start,0deg)));
filter:blur(0);
transition:stroke-dashoffset .85s var(--ease),filter .85s var(--ease);
}
.sim-arc-1{stroke:var(--sim-tint-1);--sim-arc-start:0deg;--sim-arc-off:183.2}
.sim-arc-2{stroke:var(--sim-tint-2);--sim-arc-start:136.8deg;--sim-arc-off:229.44;transition-delay:.08s}
.sim-arc-3{stroke:var(--sim-tint-3);--sim-arc-start:216deg;--sim-arc-off:223.66;transition-delay:.16s}
.sim-arc-4{stroke:var(--sim-tint-4);--sim-arc-start:302.4deg;--sim-arc-off:246.79;transition-delay:.24s}
/* A rosca VAZIA. O script põe esta classe, escreve a geometria nova e tira a
classe no quadro seguinte — e a fatia cresce até o valor. A transição é
declarada aqui como `none` de propósito: o navegador lê a transição do
estado de CHEGADA, então entrar em vazio é instantâneo e sair de vazio
anima. É o que evita o script ter de apagar e repor transition na mão. */
.sim-donut.is-blank .sim-arc{
stroke-dashoffset:var(--sim-len,289.03);
filter:blur(7px);
transition:none;
}
@media(prefers-reduced-motion:reduce){
.sim-arc{transition:none}
}
.sim-verdict{font-size:14.5px;line-height:1.5;color:var(--fg-2)}
.sim-verdict-name{
display:block;
font-family:var(--display);font-weight:700;
font-size:clamp(19px,1.6vw,23px);letter-spacing:-.015em;
color:var(--fg);
margin-bottom:6px;
}
/* A LEITURA EM TEXTO — é isto que o leitor de tela recebe, porque o <svg> é
aria-hidden inteiro. Três colunas: bolinha, nome e percentual; a linha de
apoio ocupa a segunda linha, alinhada com o nome. */
.sim-slices{display:grid;gap:11px}
.sim-slice{
display:grid;
grid-template-columns:auto 1fr auto;
align-items:baseline;
gap:2px 10px;
padding-bottom:11px;
border-bottom:1px solid var(--rule);
}
.sim-slice-dot{
width:9px;height:9px;border-radius:var(--radius-pill);
align-self:center;
}
.sim-slice-1 .sim-slice-dot{background:var(--sim-tint-1)}
.sim-slice-2 .sim-slice-dot{background:var(--sim-tint-2)}
.sim-slice-3 .sim-slice-dot{background:var(--sim-tint-3)}
.sim-slice-4 .sim-slice-dot{background:var(--sim-tint-4)}
.sim-slice-name{font-size:14.5px;color:var(--fg)}
.sim-slice-pct{
font-family:var(--display);font-weight:700;
font-size:15px;letter-spacing:-.01em;
color:var(--fg);
font-variant-numeric:tabular-nums;
}
.sim-slice-note{
grid-column:2/4;
font-size:12.5px;line-height:1.45;
color:var(--fg-muted);
}
/* A RESSALVA. Fica no passo do resultado, em corpo de leitura — não em letra
miúda de rodapé. Compliance é conteúdo, não nota de rodapé. */
/* 13,5px e não 12,5: a ressalva é MAIOR que a linha de apoio de cada classe
de ativo. Compliance que vira o menor texto da tela é letra miúda com outro
nome, e o §5 do brief é explícito. */
/* Entrelinha e tinta vêm da primitiva .nota do core; o corpo é o parafuso,
pela razão do parágrafo acima. Filete e recuo são posição e ficam aqui. */
.sim-nota{
--nota-size:13.5px;
border-left:1px solid var(--rule-2);
padding-left:13px;
}
.sim-acoes{display:flex;flex-wrap:wrap;align-items:center;gap:16px}
.sim-cta{flex:0 0 auto}
/* "Refazer" é discreto de propósito: existe para quem errou, não para
competir com a saída para a conversa. */
.sim-redo{
padding:0;border:0;background:none;
font-family:var(--sans);font-size:13.5px;letter-spacing:.02em;
color:var(--fg-muted);
text-decoration:underline;text-underline-offset:4px;
text-decoration-color:var(--rule-2);
transition:color .3s var(--ease),text-decoration-color .3s var(--ease);
}
.sim-redo:hover{color:var(--accent);text-decoration-color:var(--accent)}
/* ---- pé: o botão de avançar --------------------------------------------- */
.sim-foot{display:flex}
.sim-next{width:100%}
/* O botão nasce disabled e só libera quando o passo está respondido. Apaga
sem sumir: um botão que desaparece move o painel inteiro. */
.sim-next:disabled{
opacity:.34;cursor:default;
}
.sim-next:disabled:hover{
background:var(--accent);color:var(--dark);backdrop-filter:none;-webkit-backdrop-filter:none;
}
/* ==========================================================================
TELAS ESTREITAS
A mesma grade vira duas linhas: a foto vira faixa em cima, com o título
dentro dela, e o painel desce para baixo. Nenhuma regra de coluna muda —
quem alinha continua sendo o .wrap.
========================================================================== */
@media(max-width:1080px){
.sim{
grid-template-rows:auto auto;
min-height:0;
}
.sim-media{
grid-area:1/1;
min-height:clamp(300px,54vw,420px);
}
.sim-media-frame{
width:100%;
-webkit-mask-image:none;
mask-image:none;
}
.sim-media-veil{
background:linear-gradient(to top,
rgba(var(--dark-rgb),.94) 0%,
rgba(var(--dark-rgb),.66) 46%,
rgba(var(--dark-rgb),.22) 100%);
}
.sim-hgroup{padding-block:clamp(64px,16vw,120px) clamp(28px,6vw,44px)}
.sim-copy{max-width:none}
.sim-title{--display-xl:clamp(30px,6.4vw,46px)}
.sim-form{
grid-area:2/1;
align-items:start;
padding-block:clamp(28px,6vw,44px) clamp(56px,12vw,96px);
}
.sim-panel{width:100%;margin-left:0}
}
@media(max-width:560px){
.sim{--sim-donut:132px}
.sim-out{flex-direction:column;align-items:flex-start;gap:18px}
.sim-acoes{gap:14px}
.sim-cta{width:100%}
}
script.js · 344 linhas · 14,9 KB
/* ==========================================================================
SIMULADOR DE CARTEIRA — o assistente de quatro passos.
O que o script faz, e só isto:
1. esconde os passos 2 a 4 (o markup nasce com os quatro à vista, para o
estado sem JavaScript ser um formulário honesto e não uma casca);
2. revela a régua, o "Voltar" e o "Continuar", que nascem `hidden`;
3. troca de passo, trava o "Continuar" enquanto o passo não está
respondido e leva o foco para o título do passo novo;
4. no último passo, escolhe o perfil pela tabela abaixo e escreve, em
cada fatia da rosca, DUAS variáveis de geometria.
O que ele NÃO faz: aparência. Cor, espessura, vão, curva, blur e a entrada
de cada passo estão no estilo.css. Aqui só se troca `hidden`, classe e as
duas variáveis da rosca.
AS RESPOSTAS VIVEM EM MEMÓRIA. Nada vai para localStorage, nada é enviado,
nada persiste no recarregamento. São dados financeiros de alguém, e a
captura de contato é trabalho do conversao/cta-formulario-lead, para onde
o botão do último passo aponta.
MOVIMENTO REDUZIDO. Nada anda sozinho em nenhum momento: não há laço, não
há temporizador e não há listener de scroll. O que o movimento reduzido
desliga é o desenho da rosca — ela nasce pronta, no valor final.
========================================================================== */
(function () {
var raizes = document.querySelectorAll('[data-sim]');
if (!raizes.length) return; // guard
/* ======================================================================
A TABELA DE PERFIS — O ÚNICO LUGAR QUE O CONSUMIDOR PRECISA EDITAR.
Cada perfil traz as quatro proporções, NA ORDEM EM QUE AS FATIAS
APARECEM NO MARKUP (renda fixa, crédito privado, ações e fundos,
internacional), e uma frase de leitura. As quatro proporções de cada
linha têm de somar 100 — a rosca normaliza pelo total, então uma soma
diferente desenha certo e MENTE no percentual escrito ao lado.
Os perfis ficam em ordem, do mais conservador ao mais exposto: é essa
ordem que os cortes de pontuação percorrem.
CONTEÚDO ILUSTRATIVO. Estas proporções são exemplo de layout, não a
política de investimentos de ninguém. Antes de publicar, troque pelas
faixas que a área técnica do cliente aprovar — e leia a seção Compliance
da ficha.
TROCAR O PERFIL PADRÃO (o que aparece sem JavaScript) é mexer em três
lugares que têm de continuar batendo: os percentuais da lista no
markup.html, a geometria de .sim-arc-1..4 no estilo.css e a linha
correspondente aqui. Hoje o padrão é o "Equilíbrio".
====================================================================== */
var PERFIS = [
{ nome: 'Preservação', fatias: [62, 20, 8, 10],
frase: 'A maior parte fica em caixa e prazo curto: o patrimônio não depende de janela de mercado para estar disponível.' },
{ nome: 'Renda', fatias: [46, 30, 10, 14],
frase: 'O peso vai para ativos que pagam ao longo do caminho, sem depender de venda para gerar caixa.' },
{ nome: 'Equilíbrio', fatias: [38, 22, 24, 16],
frase: 'Metade sustenta o dia a dia; a outra metade acompanha o mercado dentro do prazo que você tem.' },
{ nome: 'Crescimento', fatias: [24, 18, 38, 20],
frase: 'O prazo longo abre espaço para empresas e moeda forte, com caixa suficiente para não vender na hora errada.' }
];
/* A pontuação. Uma soma, e é de propósito que ela caiba num olhar: quem
for auditar a peça com a área de compliance do cliente precisa entender
a regra sem ler código. O objetivo pesa mais (0 a 8), o horizonte vem
depois (0 a 4) e o conhecimento é o desempate (0 a 3).
A composição e a faixa de patrimônio do passo 2 NÃO pontuam: elas
qualificam a conversa com o assessor, não a alocação de referência.
Para incluí-las, acrescente um grupo aqui com as chaves do value de cada
<option> e some mais um termo em `perfilDe`. */
var PESO = {
objetivo: { preservar: 0, sucessao: 2, renda: 4, crescer: 8 },
horizonte: { curto: 0, medio: 2, longo: 4 },
conhecimento: { nenhum: 0, basico: 1, intermediario: 2, avancado: 3 }
};
/* Os cortes entre um perfil e o seguinte, na mesma ordem de PERFIS.
Três cortes = quatro perfis. Um quinto perfil pede um quarto corte. */
var CORTES = [4, 8, 12];
function perfilDe(resp) {
var pontos = (PESO.objetivo[resp.objetivo] || 0)
+ (PESO.horizonte[resp.horizonte] || 0)
+ (PESO.conhecimento[resp.conhecimento] || 0);
var i = 0;
while (i < CORTES.length && pontos >= CORTES[i]) i++;
return PERFIS[Math.min(i, PERFIS.length - 1)];
}
/* ==================================================================== */
/* A flag do core é a fonte. O fallback existe para o componente colado
sozinho, sem motion.js — e repete a regra do ?motion=on. */
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 iniciar(raiz, indice) {
var painel = raiz.querySelector('[data-sim-panel]');
var passos = [].slice.call(raiz.querySelectorAll('[data-sim-step]'));
if (!painel || passos.length < 2) return;
var reduz = reduzido();
var regua = raiz.querySelector('[data-sim-rule]');
var segs = regua ? [].slice.call(regua.children) : [];
var status = raiz.querySelector('[data-sim-status]');
var voltar = raiz.querySelector('[data-sim-back]');
var pe = raiz.querySelector('[data-sim-foot]');
var proximo = raiz.querySelector('[data-sim-next]');
var refazer = raiz.querySelector('[data-sim-redo]');
var donut = raiz.querySelector('[data-sim-donut]');
var arcos = [].slice.call(raiz.querySelectorAll('[data-sim-arc]'));
var pcts = [].slice.call(raiz.querySelectorAll('[data-sim-pct]'));
var elNome = raiz.querySelector('[data-sim-name]');
var elFrase = raiz.querySelector('[data-sim-phrase]');
var campos = [].slice.call(raiz.querySelectorAll('[data-sim-in]'));
var elFaixa = raiz.querySelector('[data-sim-range]');
var nudges = [].slice.call(raiz.querySelectorAll('[data-sim-nudge]'));
var ultimo = passos.length - 1;
var atual = 0;
var resp = {}; /* as respostas: memória e só */
var faixa = 0;
/* As faixas são conteúdo e vêm do markup, separadas por barra vertical.
Quem quiser três faixas apaga uma lá — aqui só se conta o que veio. */
var bandas = [];
if (elFaixa && elFaixa.getAttribute('data-sim-bands')) {
bandas = elFaixa.getAttribute('data-sim-bands').split('|');
}
/* Dois simuladores na mesma página compartilhariam os grupos de radio —
marcar num apagaria a resposta do outro. O sufixo por instância
separa os grupos sem obrigar o markup a saber quantos são. */
var i;
for (i = 0; i < campos.length; i++) {
if (campos[i].type === 'radio') campos[i].name = campos[i].name + '-' + (indice + 1);
}
/* ---------------------------------------------------------- estado */
/* Um passo está respondido quando TODO campo que ele declara tem
resposta. Nada aqui sabe que os passos são quatro nem o que eles
perguntam: apagar um passo do markup continua funcionando. */
function completo(n) {
var lista = passos[n].querySelectorAll('[data-sim-in]');
var visto = {};
for (var k = 0; k < lista.length; k++) {
var chave = lista[k].getAttribute('data-sim-in');
if (visto[chave]) continue;
visto[chave] = true;
if (!resp[chave]) return false;
}
return true;
}
function travar() {
if (proximo) proximo.disabled = !completo(atual);
}
/* Passo fora de cena sai do fluxo com `hidden`, e não com opacity: campo
invisível e tabulável é a armadilha clássica de assistente. */
function ir(n, mover) {
atual = n;
for (var p = 0; p < passos.length; p++) passos[p].hidden = (p !== n);
for (var s = 0; s < segs.length; s++) {
if (s <= n) segs[s].classList.add('is-on');
else segs[s].classList.remove('is-on');
}
if (status) status.textContent = 'Passo ' + (n + 1) + ' de ' + passos.length;
if (voltar) voltar.hidden = (n === 0);
if (pe) pe.hidden = (n === ultimo);
travar();
if (n === ultimo) calcular();
/* O foco vai para o título do passo novo. Sem isto ele ficaria no
botão que acabou de sumir e cairia no <body> — quem navega por
teclado perderia o lugar na página. preventScroll porque a dobra já
está na tela: rolar aqui é sempre solavanco. */
if (mover) {
var titulo = passos[n].querySelector('[data-sim-q]');
if (titulo) {
try { titulo.focus({ preventScroll: true }); }
catch (e) { titulo.focus(); }
}
}
}
function avancar() {
if (atual >= ultimo || !completo(atual)) return;
ir(atual + 1, true);
}
/* ---------------------------------------------------------- a faixa */
function mostrarFaixa() {
if (!elFaixa || !bandas.length) return;
elFaixa.textContent = bandas[faixa];
for (var k = 0; k < nudges.length; k++) {
var passo = parseInt(nudges[k].getAttribute('data-sim-nudge'), 10) || 0;
var fim = (passo < 0 && faixa === 0) || (passo > 0 && faixa === bandas.length - 1);
/* aria-disabled e não disabled: quem chegou de teclado ao "+" e bateu
no fim da fila continua com o foco no botão em vez de ser jogado
para o <body>. O core desenha os dois estados igual. */
nudges[k].setAttribute('aria-disabled', fim ? 'true' : 'false');
}
}
/* ---------------------------------------------------------- a rosca */
/* Cada fatia é um <circle> inteiro: o dasharray tem UM traço do tamanho
da volta, o dashoffset come o que sobra e o giro põe o começo no lugar.
O raio vem do markup — mudar r="46" lá reajusta tudo aqui. */
function desenhar(fatias) {
if (!donut || !arcos.length) return;
var r = parseFloat(arcos[0].getAttribute('r')) || 46;
var volta = 2 * Math.PI * r;
var vao = parseFloat(getComputedStyle(raiz).getPropertyValue('--sim-arc-gap'));
if (!(vao >= 0)) vao = 0;
var soma = 0, k;
for (k = 0; k < fatias.length; k++) soma += fatias[k];
if (!soma) return;
donut.style.setProperty('--sim-len', volta.toFixed(2));
var giro = 0;
for (k = 0; k < arcos.length; k++) {
var fr = (fatias[k] || 0) / soma;
var comp = Math.max(volta * fr - vao, 0);
arcos[k].style.setProperty('--sim-arc-start', (giro * 360).toFixed(2) + 'deg');
arcos[k].style.setProperty('--sim-arc-off', (volta - comp).toFixed(2));
giro += fr;
}
/* Com movimento reduzido a rosca nasce pronta: a classe do estado
vazio nunca entra. */
if (reduz) { donut.classList.remove('is-blank'); return; }
/* Entrar em vazio é instantâneo porque a própria regra .is-blank
declara transition:none — o navegador lê a transição do estado de
chegada. Tirar a classe no quadro seguinte é o que faz cada fatia
crescer até o valor, com o blur indo a zero. */
donut.classList.add('is-blank');
void donut.offsetHeight;
requestAnimationFrame(function () {
requestAnimationFrame(function () { donut.classList.remove('is-blank'); });
});
}
function calcular() {
var perfil = perfilDe(resp);
if (elNome) elNome.textContent = perfil.nome;
if (elFrase) elFrase.textContent = perfil.frase;
for (var k = 0; k < pcts.length; k++) {
pcts[k].textContent = (perfil.fatias[k] || 0) + '%';
}
desenhar(perfil.fatias);
}
function reiniciar() {
for (var k = 0; k < campos.length; k++) {
if (campos[k].type === 'radio') campos[k].checked = false;
else if (campos[k].tagName === 'SELECT') campos[k].selectedIndex = 0;
}
resp = {};
faixa = 0;
mostrarFaixa();
if (donut) donut.classList.remove('is-blank');
ir(0, true);
}
/* ---------------------------------------------------------- ligação */
for (i = 0; i < campos.length; i++) {
campos[i].addEventListener('change', function () {
resp[this.getAttribute('data-sim-in')] = this.value;
travar();
});
}
for (i = 0; i < nudges.length; i++) {
nudges[i].addEventListener('click', function () {
if (this.getAttribute('aria-disabled') === 'true') return;
var passo = parseInt(this.getAttribute('data-sim-nudge'), 10) || 0;
faixa = Math.min(Math.max(faixa + passo, 0), bandas.length - 1);
mostrarFaixa();
});
}
if (proximo) proximo.addEventListener('click', avancar);
if (voltar) voltar.addEventListener('click', function () {
if (atual > 0) ir(atual - 1, true);
});
if (refazer) refazer.addEventListener('click', reiniciar);
/* Enter avança de qualquer campo. Botão e link ficam de fora: os dois já
têm ação própria no Enter, e disparar as duas faria pular um passo. */
painel.addEventListener('keydown', function (e) {
if (e.key !== 'Enter') return;
var alvo = e.target;
if (!alvo || alvo.tagName === 'BUTTON' || alvo.tagName === 'A') return;
e.preventDefault();
avancar();
});
/* ------------------------------------------------------------ início */
/* A ENTRADA É LIGADA AQUI, e não no markup: [data-rv] nasce em opacity:0
e quem o acende é o motion.js. Escrito no markup, o título e o
formulário inteiro sumiriam numa página sem JavaScript — e esta dobra é
uma ferramenta, não decoração. Então o markup marca [data-sim-rv], que
não faz nada sozinho, e a tradução acontece aqui. LIB.scan é
idempotente e existe para exatamente isto.
Os dois <script> são clássicos e vizinhos no fim do <body>: o motion.js
roda, este roda em seguida e a página só pinta depois dos dois — não há
quadro em que o elemento apareça antes de ser marcado. */
if (window.LIB && typeof window.LIB.scan === 'function') {
var entram = raiz.querySelectorAll('[data-sim-rv]');
for (i = 0; i < entram.length; i++) entram[i].setAttribute('data-rv', '');
window.LIB.scan(raiz);
}
/* .is-live diz ao estilo que o assistente está rodando — é ela que
desliga as divisórias entre passos empilhados do estado sem script. */
painel.classList.add('is-live');
if (pe) pe.hidden = false;
mostrarFaixa();
ir(0, false);
}
for (var n = 0; n < raizes.length; n++) iniciar(raizes[n], n);
})();