← Biblioteca componentes/conversao/simulador-carteira/ficha.md CódigoTela cheia
conversao/simulador-carteira

Simulador de carteira

🟢 estavel complexidade alta prefixo sim- simulador assistente multi-etapas formulario campos radio-em-cartao chips select seletor-de-faixa rosca donut grafico-svg alocacao foto-sangrada painel-de-vidro compliance financeiro

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

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

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);
})();

Uma dobra de tela quase cheia partida em dois. À esquerda, uma foto sangrada com o título display por cima, ancorado no rodapé da imagem. À direita, sobreposto à foto e recuado até a coluna da página, um painel alto que é um assistente de quatro passos: objetivo, composição do patrimônio, horizonte e conhecimento. No quarto passo o painel devolve uma rosca de alocação desenhada em SVG, a mesma distribuição escrita em texto, a ressalva de compliance e um botão que leva à conversa.

Resolve a distância entre "quero entender o que vocês fazem" e "quero falar com alguém". A página de assessoria costuma pedir os dois extremos — ou um texto institucional que não compromete ninguém, ou um formulário de contato que pede telefone antes de dar qualquer coisa. Aqui a pessoa usa a peça primeiro: responde quatro perguntas que um assessor faria na primeira reunião e recebe uma leitura de volta. O contato vem depois, e por escolha dela. Por isso o simulador não coleta nome, e-mail nem telefone — quem faz isso é o conversao/cta-formulario-lead, para onde o botão do último passo aponta.

Ele é também um catálogo de campos: cada passo usa um tipo diferente de propósito (cartão de rádio com ícone, <select> nativo estilizado, seletor de faixa com − e +, chips de escolha única), todos com foco visível, rótulo real e teclado funcionando. Um consumidor que precise só do chip pode levar o chip.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Título sobre a foto Uma frase, duas linhas na caixa de ~580. Fala do horizonte da pessoa, não do produto. Sem <br>: a quebra automática está certa aqui.
Apoio Duas linhas. Diz quantas perguntas são e o que volta. Prometer "sua carteira ideal" é promessa; "como um patrimônio como o seu costuma ser distribuído" é leitura.
Pergunta do passo Uma linha, no máximo duas. Sempre pergunta de verdade, na segunda pessoa, do jeito que um assessor perguntaria. É ela que recebe o foco ao avançar — texto longo aqui vira leitura longa em leitor de tela.
Cartões do passo 1 Exatamente quatro, de três a cinco palavras, na mesma forma verbal (infinitivo). O ícone é desenhado à mão em stroke, nunca ícone de terceiro.
Opções do <select> Quatro a seis, com a primeira sendo "Selecione" e value vazio — é ela que mantém o passo por responder.
Faixas de patrimônio Três a cinco, do menor para o maior, em data-sim-bands separadas por \|. A última é aberta ("Acima de"). Sem centavos, sem "R$ 1.000.000,00".
Chips Três a quatro por fileira, de uma a três palavras. Duas fileiras é o teto do passo 3.
Classes de ativo Quatro, na mesma ordem no markup, no CSS e na tabela do script. Nome curto e uma linha de apoio que explica o papel da classe, não o retorno dela.
Nome do perfil Um substantivo, sem adjetivo de marketing. "Equilíbrio", não "Perfil equilibrado premium".
Frase do perfil Uma a duas linhas sobre a lógica da distribuição. Nunca sobre resultado, ganho ou proteção garantida.
Ressalva O texto do modelo, integral. Não é slot de criação.

Especificação dos assets

Uma foto em paisagem, 1600×900 ou maior, exportada em WebP. Ela é recortada em 64% da largura da dobra e na altura inteira da seção — ou seja, um retrato alto é cortado no meio. O assunto tem de estar na metade esquerda do quadro: a borda direita se dissolve por máscara (a partir de 68% da largura da foto) e é ali que o painel pousa.

Sem foto do cliente, a variação "sem foto" resolve. O que não entra: foto com texto embutido, marca de terceiro, gráfico dentro da imagem.


Compliance

Esta peça devolve número num site de assessoria, e é o item da biblioteca que mais pede revisão antes de ir ao ar. O consumidor tem de:

  1. Trocar as proporções da tabela PERFIS no script.js pelas faixas que a área técnica do cliente aprovar, e junto com elas o perfil padrão em três lugares (ver gotcha 4). As que estão aqui são exemplo de layout.
  2. Revisar a pontuação (PESO e CORTES) com quem responde pela política de investimentos. A regra é uma soma de três termos justamente para caber numa reunião de compliance. Medido nas 48 combinações possíveis: Preservação 8, Renda 20, Equilíbrio 14, Crescimento 6 — a régua é conservadora de propósito, e o perfil mais exposto só sai com objetivo de crescimento e prazo médio ou longo. Mexer nos pesos muda essa distribuição, e é isso que a área de compliance precisa ver.
  3. Manter a ressalva .sim-nota no passo do resultado, com o texto integral e no corpo em que está (13,5px, maior que a linha de apoio das classes de ativo). Mover para o rodapé da página descaracteriza o aviso.
  4. Levar a peça à área de compliance do cliente antes de publicar, com a tabela e o texto finais.

O que a peça já garante, e não deve ser desfeito: nada aqui fala em rentabilidade, ganho ou retorno — a rosca mostra alocação; o resultado é apresentado como distribuição de referência, não como recomendação; não há nome de instituição, marca de terceiro nem número apurado; e o bloco dos percentuais carrega <!-- CONTEÚDO ILUSTRATIVO --> no markup.

Dado pessoal: as respostas vivem em memória e morrem no recarregamento. Nada de localStorage, nada de envio, nenhum campo de contato.


Como usar

<!-- markup.html -->
<link rel="stylesheet" href="estilo.css">
<script src="script.js"></script>

Cole o bloco da seção, a folha e o script. Requer core/tokens.css, base.css, motion.css e motion.js já carregados. A seção não usa .sec-pad: ela é uma dobra de altura própria (--sim-min-h), e a foto sangra até as bordas.

Ganchos

Gancho O quê
[data-sim] a seção; o script parte dela e nunca do document
[data-sim-step] / [data-sim-q] cada passo e o título dele, que recebe o foco ao avançar
[data-sim-in="chave"] todo campo que responde; o valor do atributo é a chave da resposta
[data-sim-bands] as faixas de patrimônio, separadas por \| — conteúdo mora no markup
[data-sim-arc] / [data-sim-pct] as fatias da rosca e os percentuais em texto, na mesma ordem
[data-sim-rv] marca quem entra com reveal; o script traduz para [data-rv] (gotcha 1)
--sim-media-w --sim-panel-w --sim-min-h --sim-body-h as quatro medidas do desenho
--sim-donut --sim-arc-w --sim-arc-gap --sim-tint-1..4 a rosca: lado, espessura, vão e a rampa de cor
.is-live .is-on .is-blank assistente rodando · segmento percorrido · rosca vazia

Variações

Cinco, no componente.json: três passos (apague um <fieldset> e um segmento da régua), sem seletor de faixa, cinco perfis (uma linha em PERFIS, um corte em CORTES), em dobra clara e sem foto.


Gotchas

  1. Nenhum data-rv no markup, e é de propósito. [data-rv] nasce em opacity:0 e só o motion.js o acende — numa página sem JavaScript a ferramenta inteira sumiria. O markup marca [data-sim-rv], o script troca por [data-rv] e chama LIB.scan. Sem LIB, nada é trocado e tudo fica visível. Medido: opacity 1 e classe in no painel e no título, sem quadro intermediário — os dois <script> são clássicos e vizinhos no fim do <body>.

  2. .sim [hidden]{display:none} não é redundante. Regra do agente do usuário perde para regra de autor, sempre: sem essa linha, o display:grid de .sim-step faria o passo escondido continuar visível — e tabulável, que é a armadilha clássica de assistente.

  3. Uma <legend> não é item de grade do <fieldset>. Medido: com display:grid e gap:22px no passo, o vão entre a pergunta e o primeiro campo saía 0 — o navegador coloca a legenda fora do fluxo da grade —, e o anel de foco passava por cima do rótulo de baixo. O .sim-q repõe o vão com margin-bottom, lido da mesma variável do gap; no passo do resultado, onde o título é <h3> e portanto item de grade de verdade, a margem sai (senão o vão sairia dobrado: 24 + 24). A mesma armadilha vale para a legenda dos subgrupos do passo 3.

  4. Trocar o perfil padrão é mexer em três lugares. Os percentuais da lista no markup.html, a geometria de .sim-arc-1..4 no estilo.css (giro de partida e --sim-arc-off) e a linha da tabela no script.js. É o preço de a rosca desenhar certo sem JavaScript; se o estado sem script não importar no seu projeto, apague os quatro pares de variáveis do CSS e sobra um lugar só.

  5. A divisória entre passos é do estado sem script. hidden não remove o elemento da conta de irmãos, então .sim-step + .sim-step acertaria o passo 2 mesmo com o 1 escondido. O escopo é .sim-panel:not(.is-live), e .is-live entra na inicialização.

  6. disabled tira o botão da tabulação; aria-disabled não. Medido: com o "Continuar" travado, o Tab pula direto para fora do painel. Por isso os botões − e + da faixa usam aria-disabled — quem chegasse de teclado ao fim da fila perderia o foco para o <body>. O .btn-disc do core desenha os dois estados igual.

  7. O anel de foco aparece para quem usa teclado e não para quem usa mouse. Medido no Chrome: clique de mouse num cartão → :focus-visible falso; Tab → anel de 2px com recuo de 3px. O título do passo usa recuo 2px porque encosta no topo do painel.

  8. A rosca guarda dois truques. O primeiro: .is-blank (o estado vazio) declara transition:none, e o navegador lê a transição do estado de chegada — entrar em vazio é instantâneo, sair anima; sem isso o script teria de apagar e repor transition na mão. O segundo: --sim-arc-gap (4 unidades de caminho sobre 2πr = 289,03) é descontado do comprimento de cada arco, mas o giro de partida continua acumulando a fração cheia — medido, as quatro frações somam 100,0 nos quatro perfis e a rosca fecha a volta.

  9. A dobra cresce no resultado. Medido a 1440: seção de 810 para 1007 (+197); a 390, de 907 para 1365 (+458). É deliberado — o resultado é mais alto que as perguntas, e --sim-body-h (360px: o mais alto dos três passos de pergunta em qualquer largura medida, 352 a 1920, 345 a 1280, 336 a 390, mais folga) segura só os três primeiros. O preço é o vazio no pé dos passos 2 e 3; a alternativa seria o botão "Continuar" pular de lugar a cada avanço, que é pior. Quem precisar de altura fixa sobe --sim-body-h até a altura do resultado.

  10. A sobreposição do painel depende de --sim-media-w. Com 60% ela sumia a 1280 (o painel ficava 7px ao lado da foto, não sobre ela). Com 64% a borda esquerda do painel cai dentro da dissolução da foto em todas as larguras medidas: 44px de sobreposição a 1280, 69 a 1085, 127 a 1440, 194 a 1920.

  11. A lista aberta do <select> é do sistema operacional. É o único lugar do componente que não lê token: color-scheme:dark diz ao navegador para desenhá-la escura. Painel claro pede trocar por light, ou a lista abre branca sobre branco.

  12. Falta --light-rgb no core. Um véu claro sobre foto (a variação "em dobra clara") não tem token: --dark-rgb existe, o par claro não. Resolvido aqui por color-mix, mas a variação clara vai precisar da conta na mão — ver Proposta para o core, no relatório da leva.


Histórico

Projeto Arquivo Data Âncora
autoral construído do zero na biblioteca 2026-09-03 —

Origem. Padrão recorrente em site de instituição financeira — os simuladores de perfil de investidor de corretora e banco digital, os assistentes de onboarding de gestora de patrimônio, os calculadores de aposentadoria de seguradora. Não há referência única e não há uma linha copiada: a forma é conhecida, o arquivo é daqui.

Decisões da tradução. O gráfico é SVG inline sem lib — quatro <circle> girados sobre 2πr, com stroke-dashoffset acumulado —, e o dado verdadeiro é a lista em texto ao lado, porque o <svg> é aria-hidden inteiro. A paleta é uma rampa de color-mix entre --accent e --accent-2, medida em 3,8:1 na fatia mais escura contra o corpo do painel. O assistente é <fieldset>/<legend> com <input type="radio"> de verdade, e o foco vai para o título do passo novo. O reveal do core não fica no markup (gotcha 1). As respostas vivem em memória. E a ressalva de compliance é conteúdo do passo do resultado, num corpo maior que a linha de apoio das classes de ativo — letra miúda com outro nome continua sendo letra miúda.