← Biblioteca componentes/dados/comparador-cenarios/ficha.md CódigoTela cheia
dados/comparador-cenarios

Comparador de cenários de custo

🟢 estavel complexidade média prefixo duo-cmp- grafico barras comparador simulador calculadora custo fee-based range controle-deslizante tabela contador compliance sem-lib

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

<link rel="stylesheet" href="componentes/dados/comparador-cenarios/estilo.css">
<script src="componentes/dados/comparador-cenarios/script.js"></script>

markup.html · 267 linhas · 13,5 KB

<!-- ==========================================================================
     COMPARADOR DE CENÁRIOS — dois modelos de custo, lado a lado
     Prefixo: duo-cmp-   (duo- sozinho já é do conteudo/imagem-lista-numerada)
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css e script.js
     Sem lib. O gráfico são oito <span> com transform:scaleY().

     A DOBRA. Cabeçalho centrado; abaixo, um painel com dois controles de
     faixa (patrimônio e horizonte), um gráfico de barras em quatro marcos de
     tempo, a leitura em números do horizonte escolhido e a tabela com o
     custo acumulado em cada marco.

     O QUE SE COMPARA É CUSTO, NUNCA RESULTADO. Esta é a decisão que dá forma
     à peça inteira, e não é preferência de desenho: custo é aritmética
     verificável a partir de premissas declaradas; resultado é promessa. Por
     isso o patrimônio fica CONSTANTE ao longo do período — projetar o
     crescimento dele seria projetar rentabilidade, que é exatamente o que
     uma peça de assessoria não pode fazer. A conta é
     `patrimônio × percentual × anos`, e qualquer pessoa a refaz de cabeça.

     A ESCALA É RELATIVA, E O TOPO DELA ESTÁ ESCRITO. A barra mais alta do
     quadro é sempre a do modelo de comissão no último marco — é ela que
     define o 100%. Sem o número do topo à vista, uma barra "cheia" não
     significaria nada; com ele, o quadro é um gráfico de razão com o eixo
     declarado. É também por isso que mover o patrimônio faz as barras de
     taxa AFUNDAREM em relação às de comissão: a taxa acordada é regressiva
     por faixa e a comissão não é. O afundamento é o argumento.

     AS PREMISSAS ESTÃO NA PEÇA, não só no script. Um comparador que esconde
     a premissa é propaganda, não ferramenta. A linha .duo-cmp-premissas diz
     os três percentuais em uso e a taxa efetiva do patrimônio corrente.

     O GRÁFICO É DECORATIVO; O DADO É TEXTO. O bloco .duo-cmp-plot leva
     aria-hidden="true" e a tabela logo abaixo carrega os mesmos doze
     números, com cabeçalho de linha e de coluna. Quem ouve a página recebe
     os valores, não "gráfico de barras".

     SEM JAVASCRIPT. O <fieldset> nasce disabled e o script o libera. Assim a
     peça sem script não é uma casca com controles que se mexem à toa: são
     controles visivelmente inertes, um aviso curto, e o cenário padrão
     declarado — as alturas das barras vêm escritas em style="--duo-cmp-h:…"
     e todos os números vêm escritos no HTML.
     ========================================================================== -->

<section class="sec-dark sec-pad duo-cmp" data-duo-cmp>
  <div class="wrap">

    <div class="sec-head">
      <p class="eyebrow">Como cobramos</p>
      <h2 class="sec-title ttl">O mesmo patrimônio, <em>dois</em> modelos de custo</h2>
      <p class="sec-lead" data-rv data-d="1">
        Um modelo cobra pelo trabalho e está no contrato. O outro cobra pelo produto
        vendido e está no preço. Abaixo, o que cada um custaria ao longo do tempo.
      </p>
    </div>

    <div class="duo-cmp-panel" data-rv data-d="1">

      <!-- ============================================================ controles -->
      <!-- disabled de propósito: quem tira é o script. Ver o cabeçalho. -->
      <fieldset class="duo-cmp-fields" data-duo-cmp-fields disabled>
        <legend class="duo-cmp-fields-title">Ajuste o cenário</legend>

        <div class="duo-cmp-field">
          <div class="duo-cmp-field-top">
            <label class="duo-cmp-label" for="duo-cmp-patrimonio">Patrimônio sob assessoria</label>
            <!-- aria-hidden: o valor já é anunciado pelo aria-valuetext do
                 próprio range. Sem isso o leitor de tela diria duas vezes. -->
            <output class="duo-cmp-out" for="duo-cmp-patrimonio" aria-hidden="true"
                    data-duo-cmp-out="wealth">R$ 5 mi</output>
          </div>
          <input class="duo-cmp-range" type="range" id="duo-cmp-patrimonio"
                 min="1" max="50" step="1" value="5"
                 aria-valuetext="5 milhões de reais"
                 style="--duo-cmp-p:.082" data-duo-cmp-range="wealth">
          <div class="duo-cmp-ends" aria-hidden="true">
            <span class="duo-cmp-end">R$ 1 mi</span>
            <span class="duo-cmp-end">R$ 50 mi</span>
          </div>
        </div>

        <div class="duo-cmp-field">
          <div class="duo-cmp-field-top">
            <label class="duo-cmp-label" for="duo-cmp-anos">Horizonte</label>
            <output class="duo-cmp-out" for="duo-cmp-anos" aria-hidden="true"
                    data-duo-cmp-out="years">20 anos</output>
          </div>
          <input class="duo-cmp-range" type="range" id="duo-cmp-anos"
                 min="1" max="20" step="1" value="20"
                 aria-valuetext="20 anos"
                 style="--duo-cmp-p:1" data-duo-cmp-range="years">
          <div class="duo-cmp-ends" aria-hidden="true">
            <span class="duo-cmp-end">1 ano</span>
            <span class="duo-cmp-end">20 anos</span>
          </div>
        </div>
      </fieldset>

      <p class="duo-cmp-fallback">
        Os controles precisam de JavaScript. O cenário mostrado é o padrão declarado
        nas premissas: R$ 5 mi ao longo de 20 anos.
      </p>

      <!-- ============================================================ gráfico -->
      <div class="duo-cmp-grid">

        <div class="duo-cmp-chart">
          <div class="duo-cmp-chart-head">
            <ul class="duo-cmp-keys">
              <li class="duo-cmp-key">
                <span class="duo-cmp-swatch duo-cmp-swatch-fee" aria-hidden="true"></span>
                Taxa previamente acordada
              </li>
              <li class="duo-cmp-key">
                <span class="duo-cmp-swatch duo-cmp-swatch-com" aria-hidden="true"></span>
                Comissão embutida no produto
              </li>
            </ul>
            <p class="duo-cmp-scale">
              Topo da escala
              <span class="duo-cmp-scale-v" data-duo-cmp-scale>R$ 2,10 mi</span>
            </p>
          </div>

          <!-- O quadro é a RENDERIZAÇÃO do dado, e o dado está na tabela logo
               abaixo — daí o aria-hidden. As alturas são fração do quadro, de
               0 a 1, e vêm escritas aqui para a peça ficar de pé sem script.
               --duo-cmp-d é só o atraso de entrada, e o script o zera no
               primeiro toque nos controles. -->
          <div class="duo-cmp-plot" data-duo-cmp-plot data-rv aria-hidden="true">
            <div class="duo-cmp-group" data-duo-cmp-group>
              <div class="duo-cmp-bars">
                <span class="duo-cmp-bar duo-cmp-bar-fee" data-duo-cmp-bar="fee"
                      style="--duo-cmp-h:.026;--duo-cmp-d:0s"></span>
                <span class="duo-cmp-bar duo-cmp-bar-com" data-duo-cmp-bar="com"
                      style="--duo-cmp-h:.05;--duo-cmp-d:.04s"></span>
              </div>
              <span class="duo-cmp-mark" data-duo-cmp-mark>ano 1</span>
            </div>

            <div class="duo-cmp-group" data-duo-cmp-group>
              <div class="duo-cmp-bars">
                <span class="duo-cmp-bar duo-cmp-bar-fee" data-duo-cmp-bar="fee"
                      style="--duo-cmp-h:.129;--duo-cmp-d:.08s"></span>
                <span class="duo-cmp-bar duo-cmp-bar-com" data-duo-cmp-bar="com"
                      style="--duo-cmp-h:.25;--duo-cmp-d:.12s"></span>
              </div>
              <span class="duo-cmp-mark" data-duo-cmp-mark>ano 5</span>
            </div>

            <div class="duo-cmp-group" data-duo-cmp-group>
              <div class="duo-cmp-bars">
                <span class="duo-cmp-bar duo-cmp-bar-fee" data-duo-cmp-bar="fee"
                      style="--duo-cmp-h:.257;--duo-cmp-d:.16s"></span>
                <span class="duo-cmp-bar duo-cmp-bar-com" data-duo-cmp-bar="com"
                      style="--duo-cmp-h:.5;--duo-cmp-d:.2s"></span>
              </div>
              <span class="duo-cmp-mark" data-duo-cmp-mark>ano 10</span>
            </div>

            <div class="duo-cmp-group" data-duo-cmp-group>
              <div class="duo-cmp-bars">
                <span class="duo-cmp-bar duo-cmp-bar-fee" data-duo-cmp-bar="fee"
                      style="--duo-cmp-h:.514;--duo-cmp-d:.24s"></span>
                <span class="duo-cmp-bar duo-cmp-bar-com" data-duo-cmp-bar="com"
                      style="--duo-cmp-h:1;--duo-cmp-d:.28s"></span>
              </div>
              <span class="duo-cmp-mark" data-duo-cmp-mark>ano 20</span>
            </div>
          </div>
        </div>

        <!-- ============================================================ leitura -->
        <aside class="duo-cmp-read">
          <p class="duo-cmp-read-head">
            Em <span class="duo-cmp-read-em" data-duo-cmp-horizon>20 anos</span>,
            sobre <span class="duo-cmp-read-em" data-duo-cmp-wealth>R$ 5 mi</span>
          </p>

          <!-- CONTEÚDO ILUSTRATIVO -->
          <div class="duo-cmp-stat">
            <span class="duo-cmp-stat-k">Custo com taxa acordada</span>
            <span class="duo-cmp-stat-v duo-cmp-stat-fee" data-duo-cmp-num="fee">R$ 1,08 mi</span>
          </div>
          <div class="duo-cmp-stat">
            <span class="duo-cmp-stat-k">Custo com comissão embutida</span>
            <span class="duo-cmp-stat-v duo-cmp-stat-com" data-duo-cmp-num="com">R$ 2,10 mi</span>
          </div>
          <div class="duo-cmp-stat duo-cmp-stat-gap">
            <span class="duo-cmp-stat-k">Diferença de custo</span>
            <span class="duo-cmp-stat-v" data-duo-cmp-num="gap">R$ 1,02 mi</span>
          </div>

          <!-- CONTEÚDO ILUSTRATIVO — trocar pelas premissas do cliente e datá-las -->
          <p class="duo-cmp-premissas">
            <span class="duo-cmp-premissas-k">Premissas em uso</span>
            taxa acordada regressiva por faixa: 1,20% ao ano até R$ 3 mi, 0,90% de
            R$ 3 mi a R$ 10 mi e 0,60% acima disso — neste patrimônio,
            <span class="duo-cmp-premissas-v" data-duo-cmp-rate>1,08%</span> ao ano.
            Comissão embutida de 2,10% ao ano, sem repasse de rebate. Patrimônio
            constante no período: a peça compara custo e não projeta rendimento.
          </p>

          <a class="btn btn-ghost duo-cmp-cta" href="#contato">Falar sobre custos</a>
        </aside>
      </div>

      <!-- ============================================================ tabela -->
      <!-- A leitura em texto das oito barras. Não é redundância decorativa:
           é o conteúdo, e o quadro acima é que é a ilustração dele. -->
      <div class="duo-cmp-scroll">
        <table class="duo-cmp-table">
          <caption class="duo-cmp-cap">
            Custo acumulado em cada modelo, por marco de tempo. Valores ilustrativos.
          </caption>
          <thead class="duo-cmp-thead">
            <tr class="duo-cmp-tr">
              <th class="duo-cmp-th" scope="col">Marco</th>
              <th class="duo-cmp-th" scope="col">Taxa acordada</th>
              <th class="duo-cmp-th" scope="col">Comissão embutida</th>
              <th class="duo-cmp-th" scope="col">Diferença</th>
            </tr>
          </thead>
          <tbody class="duo-cmp-tbody">
            <tr class="duo-cmp-tr" data-duo-cmp-row>
              <th class="duo-cmp-rh" scope="row" data-duo-cmp-cell="year">ano 1</th>
              <td class="duo-cmp-td" data-duo-cmp-cell="fee">R$ 54 mil</td>
              <td class="duo-cmp-td" data-duo-cmp-cell="com">R$ 105 mil</td>
              <td class="duo-cmp-td duo-cmp-td-gap" data-duo-cmp-cell="gap">R$ 51 mil</td>
            </tr>
            <tr class="duo-cmp-tr" data-duo-cmp-row>
              <th class="duo-cmp-rh" scope="row" data-duo-cmp-cell="year">ano 5</th>
              <td class="duo-cmp-td" data-duo-cmp-cell="fee">R$ 270 mil</td>
              <td class="duo-cmp-td" data-duo-cmp-cell="com">R$ 525 mil</td>
              <td class="duo-cmp-td duo-cmp-td-gap" data-duo-cmp-cell="gap">R$ 255 mil</td>
            </tr>
            <tr class="duo-cmp-tr" data-duo-cmp-row>
              <th class="duo-cmp-rh" scope="row" data-duo-cmp-cell="year">ano 10</th>
              <td class="duo-cmp-td" data-duo-cmp-cell="fee">R$ 540 mil</td>
              <td class="duo-cmp-td" data-duo-cmp-cell="com">R$ 1,05 mi</td>
              <td class="duo-cmp-td duo-cmp-td-gap" data-duo-cmp-cell="gap">R$ 510 mil</td>
            </tr>
            <tr class="duo-cmp-tr" data-duo-cmp-row>
              <th class="duo-cmp-rh" scope="row" data-duo-cmp-cell="year">ano 20</th>
              <td class="duo-cmp-td" data-duo-cmp-cell="fee">R$ 1,08 mi</td>
              <td class="duo-cmp-td" data-duo-cmp-cell="com">R$ 2,10 mi</td>
              <td class="duo-cmp-td duo-cmp-td-gap" data-duo-cmp-cell="gap">R$ 1,02 mi</td>
            </tr>
          </tbody>
        </table>
      </div>

      <!-- Anúncio do resultado para leitor de tela, com atraso: durante o
           arrasto o valor muda dezenas de vezes por segundo, e uma região
           viva escrita a cada quadro seria inutilizável. Nasce vazia para não
           anunciar nada no carregamento. -->
      <p class="duo-cmp-sr" data-duo-cmp-live aria-live="polite"></p>

      <p class="nota duo-cmp-nota">
        Simulação ilustrativa, para orientar a conversa. Compara custo entre dois
        modelos de remuneração — não projeta rentabilidade, não considera imposto e
        não é recomendação de investimento. Cenários passados não garantem resultado
        futuro.
      </p>

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

estilo.css · 432 linhas · 14,9 KB

/* ==========================================================================
   COMPARADOR DE CENÁRIOS — prefixo duo-cmp-
   Dois controles de faixa, oito barras verticais em quatro marcos de tempo,
   a leitura em números do horizonte escolhido e a tabela com o custo
   acumulado. Compara CUSTO entre dois modelos de remuneração — nunca
   resultado (ver o cabeçalho do markup.html).

   O prefixo é duo-cmp- e não duo-: duo- já é do conteudo/imagem-lista-numerada,
   e duas classes .duo-item na mesma página colidiriam em silêncio.

   Tokens usados:
     --max --pad          só via .wrap, que o markup usa — nada aqui mexe na coluna
     --sec-gap            respiro da dobra, via .sec-pad do core
     --display --sans     tipografia
     --ease               curva única do projeto
     --radius-media       raio do painel
     --radius-pill        trilho e botão da faixa
     --accent             série da taxa acordada, trilho preenchido, foco
     --bg --fg --fg-2 --fg-muted --rule --rule-2   do escopo .sec-dark da seção

   AS DUAS CORES DE SÉRIE SÃO VARIÁVEIS DO PREFIXO e leem token: a taxa
   acordada é a cor de ação do tema, a comissão é o cinza de apoio. Elas
   separam por LUMINÂNCIA, não só por matiz (sage claro contra cinza médio),
   e é isso que mantém as duas séries distinguíveis em tela monocromática —
   o rótulo em texto e a tabela fecham o resto.
   ========================================================================== */

.duo-cmp{
  /* ---- Séries. Trocar aqui troca barra, quadradinho da legenda e número. */
  --duo-cmp-fee:var(--accent);
  --duo-cmp-com:color-mix(in srgb,var(--fg-muted) 88%,transparent);

  /* ---- Painel ---- */
  --duo-cmp-pad:clamp(20px,2.4vw,34px);       /* respiro interno do painel */
  --duo-cmp-radius:var(--radius-media);
  --duo-cmp-aside:clamp(240px,24vw,320px);    /* coluna da leitura, à direita */
  --duo-cmp-col-gap:clamp(24px,3vw,44px);

  /* ---- Controles ---- */
  --duo-cmp-track:6px;                        /* espessura do trilho */
  --duo-cmp-thumb:20px;                       /* diâmetro do botão da faixa */

  /* ---- Quadro ----
     A altura do quadro é só das barras: o rótulo do marco fica abaixo dela,
     e a linha de base é desenhada exatamente nesta medida (ver ::before). */
  --duo-cmp-plot-h:clamp(180px,19vw,260px);
  --duo-cmp-bar-w:clamp(18px,3vw,44px);       /* largura de cada barra */
  --duo-cmp-bar-gap:clamp(4px,.6vw,9px);      /* vão entre as duas do par */
  --duo-cmp-group-gap:clamp(10px,1.6vw,26px); /* vão entre marcos */
  --duo-cmp-grow:.7s;                         /* crescimento na entrada */
  --duo-cmp-move:.45s;                        /* mudança depois do primeiro toque */
}

/* ============================================================ painel */
.duo-cmp-panel{
  margin-top:clamp(30px,3.6vw,52px);
  padding:var(--duo-cmp-pad);
  border:1px solid var(--rule);
  border-radius:var(--duo-cmp-radius);
  /* 4% do texto sobre o fundo da própria seção: em .sec-dark levanta o
     painel do preto, em .sec-light o abaixa do creme. Uma regra, dois tons. */
  background:color-mix(in srgb,var(--fg) 4%,transparent);
}

/* ============================================================ controles */
/* O <fieldset> nasce disabled no markup e o script o libera — sem script,
   os controles ficam visivelmente inertes em vez de se mexerem à toa. */
.duo-cmp-fields{
  display:grid;
  grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(18px,2.6vw,40px);
  margin:0;padding:0;border:0;
}

.duo-cmp-fields-title{
  padding:0;
  margin-bottom:clamp(14px,1.4vw,20px);
  font-family:var(--display);
  font-size:13px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--fg-muted);
}

.duo-cmp-field{min-width:0}

.duo-cmp-field-top{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin-bottom:10px;
}

.duo-cmp-label{font-size:15px;color:var(--fg-2)}

.duo-cmp-out{
  font-family:var(--display);
  font-size:clamp(17px,1.5vw,20px);
  color:var(--fg);
  font-variant-numeric:tabular-nums;
  /* "R$ 5 mi" quebrando entre o 5 e o mi lê como dois valores. */
  white-space:nowrap;
}

/* ---- a faixa ----
   O trilho é pintado com um degradê de duas paradas na mesma posição:
   preenchido até --duo-cmp-p, vazio depois. A fração vem do style inline
   (escrita no markup para o estado padrão, atualizada pelo script). */
.duo-cmp-range{
  -webkit-appearance:none;appearance:none;
  display:block;width:100%;height:var(--duo-cmp-thumb);
  margin:0;padding:0;background:transparent;cursor:pointer;
}

.duo-cmp-range::-webkit-slider-runnable-track{
  height:var(--duo-cmp-track);border-radius:var(--radius-pill);
  background:linear-gradient(to right,
    var(--accent) 0 calc(var(--duo-cmp-p,0) * 100%),
    var(--rule-2) calc(var(--duo-cmp-p,0) * 100%) 100%);
}
.duo-cmp-range::-webkit-slider-thumb{
  -webkit-appearance:none;appearance:none;
  width:var(--duo-cmp-thumb);height:var(--duo-cmp-thumb);
  border:0;border-radius:var(--radius-pill);
  background:var(--accent);
  /* centra o botão no trilho, seja qual for a espessura dos dois */
  margin-top:calc((var(--duo-cmp-track) - var(--duo-cmp-thumb)) / 2);
  transition:transform .2s var(--ease);
}
.duo-cmp-range:hover::-webkit-slider-thumb{transform:scale(1.12)}

/* O Firefox tem trilho e progresso separados — não aceita o degradê acima. */
.duo-cmp-range::-moz-range-track{
  height:var(--duo-cmp-track);border-radius:var(--radius-pill);
  background:var(--rule-2);
}
.duo-cmp-range::-moz-range-progress{
  height:var(--duo-cmp-track);border-radius:var(--radius-pill);
  background:var(--accent);
}
.duo-cmp-range::-moz-range-thumb{
  width:var(--duo-cmp-thumb);height:var(--duo-cmp-thumb);
  border:0;border-radius:var(--radius-pill);
  background:var(--accent);
}

/* Estado sem script: inerte à vista, e o cursor não promete interação. */
.duo-cmp-range:disabled{opacity:.42;cursor:not-allowed}

@media(prefers-reduced-motion:reduce){
  .duo-cmp-range::-webkit-slider-thumb{transition:none}
  .duo-cmp-range:hover::-webkit-slider-thumb{transform:none}
}

.duo-cmp-ends{
  display:flex;justify-content:space-between;gap:12px;
  margin-top:8px;
  font-size:12px;letter-spacing:.06em;color:var(--fg-muted);
}

.duo-cmp-end{white-space:nowrap}

/* Só aparece quando o fieldset continua disabled — isto é, quando o script
   não rodou. Irmão imediato: a ordem no markup é fieldset e depois o aviso. */
.duo-cmp-fallback{display:none}
.duo-cmp-fields:disabled + .duo-cmp-fallback{
  display:block;
  margin-top:16px;font-size:14px;color:var(--fg-muted);
}

/* ============================================================ grade do miolo */
.duo-cmp-grid{
  display:grid;
  grid-template-columns:minmax(0,1fr) var(--duo-cmp-aside);
  gap:var(--duo-cmp-col-gap);
  margin-top:clamp(26px,3vw,44px);
}

.duo-cmp-chart{min-width:0}

.duo-cmp-chart-head{
  display:flex;align-items:baseline;justify-content:space-between;
  gap:16px;flex-wrap:wrap;
  margin-bottom:clamp(16px,1.8vw,24px);
}

.duo-cmp-keys{
  display:flex;flex-wrap:wrap;gap:8px 22px;
  margin:0;padding:0;list-style:none;
}
.duo-cmp-key{
  display:flex;align-items:center;gap:9px;
  font-size:14px;color:var(--fg-2);
}
.duo-cmp-swatch{
  width:12px;height:12px;flex:0 0 auto;
  border-radius:calc(var(--radius-media) / 2);
}
.duo-cmp-swatch-fee{background:var(--duo-cmp-fee)}
.duo-cmp-swatch-com{background:var(--duo-cmp-com)}

.duo-cmp-scale{
  margin:0;font-size:13px;color:var(--fg-muted);white-space:nowrap;
}
.duo-cmp-scale-v{
  color:var(--fg-2);font-variant-numeric:tabular-nums;
}

/* ---- o quadro ----
   Os grupos têm todos a mesma altura (barras + rótulo), então a linha de
   base e as duas linhas de grade podem ser uma camada só, presa ao topo com
   a altura das barras. Desenhar a base como border-bottom de cada grupo
   daria quatro segmentos com buracos entre eles. */
.duo-cmp-plot{
  position:relative;
  display:flex;align-items:flex-end;
  gap:var(--duo-cmp-group-gap);
}
.duo-cmp-plot::before{
  content:"";position:absolute;left:0;right:0;top:0;
  height:var(--duo-cmp-plot-h);
  border-bottom:1px solid var(--rule-2);
  background-image:linear-gradient(var(--rule),var(--rule)),
                   linear-gradient(var(--rule),var(--rule));
  background-repeat:no-repeat;
  background-size:100% 1px,100% 1px;
  background-position:0 0,0 50%;
  pointer-events:none;
}

.duo-cmp-group{
  flex:1 1 0;min-width:0;
  display:flex;flex-direction:column;
}
/* O display acima é regra de autor e venceria o [hidden] da folha do
   navegador, que é de origem mais fraca. Sem esta linha, esconder um marco
   pelo atributo não faria nada. */
.duo-cmp-group[hidden]{display:none}

.duo-cmp-bars{
  height:var(--duo-cmp-plot-h);
  display:flex;align-items:flex-end;justify-content:center;
  gap:var(--duo-cmp-bar-gap);
}

/* A barra ocupa o quadro inteiro e é ESCALADA a partir da base: assim a
   altura é uma fração pura de 0 a 1, sem conta de pixel em lugar nenhum, e
   o crescimento anima em composição (sem reflow). O atraso da entrada vem do
   style inline; o script o zera no primeiro toque, senão o gráfico
   responderia com meio segundo de sono a cada movimento da faixa. */
.duo-cmp-bar{
  display:block;
  width:var(--duo-cmp-bar-w);height:100%;
  transform:scaleY(0);transform-origin:bottom;
  transition:transform var(--duo-cmp-grow) var(--ease);
  transition-delay:var(--duo-cmp-d,0s);
}
.duo-cmp-bar-fee{background:var(--duo-cmp-fee)}
.duo-cmp-bar-com{background:var(--duo-cmp-com)}

/* .in é do core (o data-rv do quadro). .is-live é do script, no primeiro
   toque nos controles — as duas soltam a barra, para que tabular até o
   controle antes de o quadro entrar em cena não deixe o gráfico zerado. */
.duo-cmp-plot.in .duo-cmp-bar,
.duo-cmp-plot.is-live .duo-cmp-bar{transform:scaleY(var(--duo-cmp-h,0))}
.duo-cmp-plot.is-live .duo-cmp-bar{
  transition-delay:0s;
  transition-duration:var(--duo-cmp-move);
}

/* Especificidade igual à das duas regras acima, e depois delas: sem isso o
   .is-live devolveria uma duração ao que aqui se quer parado. */
@media(prefers-reduced-motion:reduce){
  .duo-cmp-plot .duo-cmp-bar{
    transform:scaleY(var(--duo-cmp-h,0));
    transition:none;
  }
}

.duo-cmp-mark{
  padding-top:10px;
  text-align:center;
  font-size:13px;letter-spacing:.04em;color:var(--fg-muted);
  white-space:nowrap;
}

/* ============================================================ leitura */
.duo-cmp-read{
  min-width:0;
  display:flex;flex-direction:column;
  align-self:start;
}

.duo-cmp-read-head{
  margin:0 0 18px;
  font-size:14px;color:var(--fg-muted);
}
.duo-cmp-read-em{color:var(--fg-2)}

.duo-cmp-stat{
  display:flex;flex-direction:column;gap:2px;
  padding:12px 0;
  border-top:1px solid var(--rule);
}
.duo-cmp-stat-k{font-size:13px;color:var(--fg-muted)}
.duo-cmp-stat-v{
  font-family:var(--display);
  font-size:clamp(22px,2.2vw,30px);line-height:1.1;
  /* Sem tabular-nums o número dança de largura enquanto conta. */
  font-variant-numeric:tabular-nums;
}
.duo-cmp-stat-fee{color:var(--duo-cmp-fee)}
.duo-cmp-stat-com{color:var(--duo-cmp-com)}

/* A diferença é o argumento: linha mais forte e número em cor de texto. */
.duo-cmp-stat-gap{border-top-color:var(--rule-2)}

.duo-cmp-premissas{
  margin:18px 0 0;
  font-size:12px;line-height:1.55;color:var(--fg-muted);
}
.duo-cmp-premissas-k{
  display:block;
  margin-bottom:4px;
  font-family:var(--display);
  font-size:11px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--fg-2);
}
.duo-cmp-premissas-v{color:var(--fg-2);font-variant-numeric:tabular-nums}

.duo-cmp-cta{margin-top:20px;align-self:start;justify-self:start}

/* ============================================================ tabela */
/* Quatro colunas de número não encolhem abaixo do conteúdo. Abaixo de ~340px
   a tabela passaria por cima da borda do painel; aqui ela rola dentro da
   própria caixa, que é o comportamento que o contrato pede para conteúdo
   largo. */
.duo-cmp-scroll{
  margin-top:clamp(26px,3vw,44px);
  overflow-x:auto;
}
.duo-cmp-table{
  width:100%;
  border-collapse:collapse;
  font-variant-numeric:tabular-nums;
}
.duo-cmp-cap{
  margin-bottom:12px;
  text-align:left;
  font-size:12px;letter-spacing:.06em;color:var(--fg-muted);
}
.duo-cmp-th{
  padding:10px 14px 10px 0;
  text-align:right;vertical-align:bottom;
  font-family:var(--display);font-weight:400;
  font-size:12px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--fg-muted);
  border-bottom:1px solid var(--rule-2);
}
.duo-cmp-th:first-child{text-align:left}
.duo-cmp-rh{
  padding:12px 14px 12px 0;
  text-align:left;font-weight:400;
  font-size:14px;color:var(--fg-2);
  border-bottom:1px solid var(--rule);
  /* "ano 20" quebrando em duas linhas desalinha a fila inteira em tela
     estreita — e é justamente onde a tabela é a leitura principal. */
  white-space:nowrap;
}
.duo-cmp-td{
  padding:12px 14px 12px 0;
  text-align:right;
  font-size:15px;color:var(--fg-2);
  border-bottom:1px solid var(--rule);
  /* Valor não quebra: em vez de espremer "R$ 270 mil" em duas linhas, a
     tabela cresce e rola dentro do .duo-cmp-scroll. */
  white-space:nowrap;
}
.duo-cmp-td:last-child{padding-right:0}
.duo-cmp-td-gap{color:var(--fg)}

/* ============================================================ ressalva */
/* Tipografia pela primitiva .nota do core; margem e medida ficam aqui — 78ch
   acompanha a largura da tabela de leitura logo acima. */
.duo-cmp-nota{
  margin:clamp(20px,2.2vw,30px) 0 0;
  --nota-max:78ch;
}

/* Só para leitor de tela: o anúncio do resultado depois que o arrasto para.
   clip-path e não clip: clip está obsoleto e clip-path não cria caixa de
   rolagem no pai. */
.duo-cmp-sr{
  position:absolute;
  width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;white-space:nowrap;border:0;
  clip-path:inset(50%);
}

/* ==========================================================================
   MOBILE — no fim do arquivo, como manda o §6.5
   ========================================================================== */

@media(max-width:900px){
  .duo-cmp-grid{grid-template-columns:minmax(0,1fr)}
  /* A leitura desce para debaixo do quadro e vira uma fila de três blocos,
     que é o formato que cabe numa coluna larga sem virar lista solta. */
  .duo-cmp-read{
    display:grid;
    grid-template-columns:repeat(3,minmax(0,1fr));
    gap:0 clamp(16px,2.4vw,30px);
    align-items:start;
  }
  .duo-cmp-read-head{grid-column:1 / -1}
  .duo-cmp-premissas{grid-column:1 / -1}
  .duo-cmp-cta{grid-column:1 / -1}
}

@media(max-width:640px){
  .duo-cmp-fields{grid-template-columns:minmax(0,1fr)}
  .duo-cmp-read{grid-template-columns:minmax(0,1fr)}
  .duo-cmp-stat{padding:10px 0}
}

/* Abaixo de 560 o quadro passa a três marcos — quem decide é o script, que
   observa a mesma largura. Aqui só encolhem o quadro e a tabela. */
@media(max-width:560px){
  .duo-cmp{
    --duo-cmp-plot-h:170px;
    --duo-cmp-bar-w:clamp(22px,7vw,40px);
    --duo-cmp-group-gap:8px;
  }
  .duo-cmp-th{font-size:11px;letter-spacing:.06em;padding-right:8px}
  .duo-cmp-td,.duo-cmp-rh{font-size:13px;padding-right:8px}
  .duo-cmp-nota{--nota-size:12px}
}

script.js · 283 linhas · 12,3 KB

/* ==========================================================================
   COMPARADOR DE CENÁRIOS — a conta e o desenho dela

   O que o script faz: lê as duas faixas, aplica as premissas da tabela
   abaixo e escreve a altura de cada barra (--duo-cmp-h, fração de 0 a 1), a
   tabela, os três números do resumo e a taxa efetiva da linha de premissas.

   O QUE ELE NÃO FAZ, e é o mais importante: não projeta rentabilidade. O
   patrimônio entra constante e sai constante — o que cresce é o CUSTO
   acumulado, que é `patrimônio × percentual × anos`. Fazer o patrimônio
   crescer transformaria uma comparação de custo numa promessa de retorno, e
   é exatamente isso que uma peça de assessoria não pode fazer. Quem for
   mexer aqui: essa fronteira é a peça inteira.

   Nada é gravado. Nenhum localStorage, nenhum cookie, nenhum dado enviado a
   lugar nenhum: os dois números vivem em memória e morrem no recarregamento.
   São dados financeiros de quem está lendo, e capturar dado é trabalho do
   conversao/cta-formulario-lead, para onde o botão do fim aponta.

   MOVIMENTO REDUZIDO: as barras nascem na altura final (o CSS resolve) e os
   números são escritos direto, sem contagem. O laço de rAF nem chega a
   existir.
   ========================================================================== */

(function () {
  var raizes = document.querySelectorAll('[data-duo-cmp]');
  if (!raizes.length) return;                       // guard

  /* ======================================================================
     PREMISSAS — CONTEÚDO ILUSTRATIVO

     Números hipotéticos, aqui para o preview ter o que desenhar. Quem
     consumir o componente TEM de substituí-los pelas premissas do cliente,
     datá-las, e refletir a troca na linha .duo-cmp-premissas do markup, que
     é o que a pessoa lê na tela. Um comparador que esconde a premissa é
     propaganda, não ferramenta.
     ====================================================================== */
  var PREMISSAS = {
    /* Taxa previamente acordada, ao ano, REGRESSIVA e marginal por faixa:
       [teto da faixa em reais, percentual ao ano]. Marginal quer dizer que
       cada faixa cobra só sobre a parte do patrimônio que cai dentro dela —
       como imposto de renda, e não "escolha a faixa e aplique a tudo". */
    faixas: [[3000000, 0.012], [10000000, 0.009], [Infinity, 0.006]],

    /* Percentual médio ao ano embutido no produto: spread de distribuição,
       taxa de administração acima da média, rebate de fundo. */
    comissao: 0.021,

    /* Fração da comissão devolvida ao cliente. 0 = nada volta. */
    repasse: 0
  };

  /* Marcos do eixo. Entram os que couberem antes do horizonte, e o horizonte
     é sempre o último — daí a barra cheia no fim do quadro. */
  var ESCADA = [1, 5, 10, 20];

  var PISO = 0.012;   /* altura mínima de barra, em fração do quadro: o custo
                         de um ano contra vinte daria 2px, e barra invisível
                         lê como "zero", que é falso. */
  var CONTA = 380;    /* ms — meia-vida da contagem dos três números */
  var FALA = 700;     /* ms de silêncio antes de anunciar o resultado */

  var estreito = matchMedia('(max-width:560px)');

  /* 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;
  }

  /* ---------------------------------------------------------------- a conta */

  /* Custo de UM ano no modelo de taxa acordada, faixa a faixa. */
  function taxaAno(p) {
    var resto = p, base = 0, total = 0, i, parte;
    for (i = 0; i < PREMISSAS.faixas.length && resto > 0; i++) {
      parte = Math.min(resto, PREMISSAS.faixas[i][0] - base);
      total += parte * PREMISSAS.faixas[i][1];
      resto -= parte;
      base = PREMISSAS.faixas[i][0];
    }
    return total;
  }

  /* Custo de UM ano no modelo de comissão embutida. */
  function comissaoAno(p) {
    return p * PREMISSAS.comissao * (1 - PREMISSAS.repasse);
  }

  /* Os marcos, do horizonte para trás. Quatro no desktop, três em tela
     estreita — quatro grupos de duas barras abaixo de 560px deixam cada
     barra com 12px e o rótulo do marco quebrando em duas linhas. */
  function marcos(h, teto) {
    var saida = [], i;
    for (i = 0; i < ESCADA.length; i++) if (ESCADA[i] < h) saida.push(ESCADA[i]);
    saida.push(h);
    while (saida.length > teto) saida.shift();
    return saida;
  }

  /* ------------------------------------------------------------- formatação */

  function moeda(v) {
    var a = Math.abs(v);
    if (a >= 1000000) return 'R$ ' + (v / 1000000).toFixed(2).replace('.', ',') + ' mi';
    if (a >= 1000) return 'R$ ' + Math.round(v / 1000) + ' mil';
    return 'R$ ' + Math.round(v);
  }

  function pct(x) { return (x * 100).toFixed(2).replace('.', ',') + '%'; }
  function anos(h) { return h === 1 ? '1 ano' : h + ' anos'; }
  function milhoes(m) { return 'R$ ' + m + ' mi'; }

  /* Fração percorrida da faixa, para o preenchimento do trilho. Lê min e max
     do próprio campo: mudar a amplitude no markup não pede mexer aqui. */
  function fracao(el) {
    var mn = +el.min, mx = +el.max;
    return mx > mn ? (+el.value - mn) / (mx - mn) : 0;
  }

  /* ---------------------------------------------------------------- montagem */

  function iniciar(raiz) {
    var faixaPat  = raiz.querySelector('[data-duo-cmp-range="wealth"]');
    var faixaAnos = raiz.querySelector('[data-duo-cmp-range="years"]');
    var quadro    = raiz.querySelector('[data-duo-cmp-plot]');
    if (!faixaPat || !faixaAnos || !quadro) return;   /* peça incompleta: sai calado */

    /* Tudo parte da raiz do próprio componente, nunca do documento: duas
       dobras destas na mesma página não podem escrever uma na outra. */
    var grupos = raiz.querySelectorAll('[data-duo-cmp-group]');
    var linhas = raiz.querySelectorAll('[data-duo-cmp-row]');
    var campos = raiz.querySelector('[data-duo-cmp-fields]');
    var reduz  = reduzido();

    var saiPat  = raiz.querySelector('[data-duo-cmp-out="wealth"]');
    var saiAnos = raiz.querySelector('[data-duo-cmp-out="years"]');
    var topo    = raiz.querySelector('[data-duo-cmp-scale]');
    var elHoriz = raiz.querySelector('[data-duo-cmp-horizon]');
    var elPat   = raiz.querySelector('[data-duo-cmp-wealth]');
    var elTaxa  = raiz.querySelector('[data-duo-cmp-rate]');
    var viva    = raiz.querySelector('[data-duo-cmp-live]');

    /* Os três números que contam. `atual` é o que está escrito na tela,
       `alvo` é para onde ele vai. */
    var nums = [], chaves = ['fee', 'com', 'gap'], i, el;
    for (i = 0; i < chaves.length; i++) {
      el = raiz.querySelector('[data-duo-cmp-num="' + chaves[i] + '"]');
      if (el) nums.push({ chave: chaves[i], el: el, atual: 0, alvo: 0 });
    }

    var rodando = false, ts = 0, aviso = 0, primeira = true;

    function laco() {
      if (rodando) return;
      rodando = true; ts = 0;
      requestAnimationFrame(function passo(t) {
        /* Por TEMPO, não por quadro, com teto de 50ms: uma aba que volta do
           segundo plano entrega um dt gigante e o número saltaria. */
        var dt = ts ? Math.min(50, t - ts) : 16;
        ts = t;
        var k = 1 - Math.pow(0.001, dt / CONTA);
        var vivo = false, j, n, d;
        for (j = 0; j < nums.length; j++) {
          n = nums[j];
          d = n.alvo - n.atual;
          if (Math.abs(d) < 1) n.atual = n.alvo; else { n.atual += d * k; vivo = true; }
          n.el.textContent = moeda(n.atual);
        }
        if (vivo) requestAnimationFrame(passo);
        else rodando = false;          /* dorme: sem alvo novo, sem quadro */
      });
    }

    function definir(chave, valor) {
      for (var j = 0; j < nums.length; j++) {
        if (nums[j].chave !== chave) continue;
        nums[j].alvo = valor;
        /* Movimento reduzido e primeira pintura escrevem o valor final: não
           há contagem, e o que está no HTML já é este número. */
        if (reduz || primeira) {
          nums[j].atual = valor;
          nums[j].el.textContent = moeda(valor);
        }
      }
    }

    function render() {
      var p = +faixaPat.value * 1000000;
      var h = +faixaAnos.value;
      var fa = taxaAno(p), ca = comissaoAno(p);
      var ms = marcos(h, estreito.matches ? 3 : 4);
      /* O topo do quadro é o custo do modelo de comissão no último marco —
         por isso a última barra cinza está sempre cheia. O valor fica
         escrito ao lado da legenda: sem ele, "cheia" não diria nada. */
      var teto = ca * ms[ms.length - 1];
      var j, m, g, barras, celulas, rotulo;

      for (j = 0; j < grupos.length; j++) {
        g = grupos[j];
        if (j >= ms.length) { g.hidden = true; continue; }
        g.hidden = false;
        m = ms[j];
        barras = g.querySelectorAll('[data-duo-cmp-bar]');
        if (barras[0]) barras[0].style.setProperty('--duo-cmp-h', Math.max(PISO, fa * m / teto) + '');
        if (barras[1]) barras[1].style.setProperty('--duo-cmp-h', Math.max(PISO, ca * m / teto) + '');
        rotulo = g.querySelector('[data-duo-cmp-mark]');
        if (rotulo) rotulo.textContent = 'ano ' + m;
      }

      for (j = 0; j < linhas.length; j++) {
        if (j >= ms.length) { linhas[j].hidden = true; continue; }
        linhas[j].hidden = false;
        m = ms[j];
        celulas = linhas[j].querySelectorAll('[data-duo-cmp-cell]');
        if (celulas.length < 4) continue;
        celulas[0].textContent = 'ano ' + m;
        celulas[1].textContent = moeda(fa * m);
        celulas[2].textContent = moeda(ca * m);
        celulas[3].textContent = moeda((ca - fa) * m);
      }

      definir('fee', fa * h);
      definir('com', ca * h);
      definir('gap', (ca - fa) * h);
      if (!reduz && !primeira) laco();

      faixaPat.style.setProperty('--duo-cmp-p', fracao(faixaPat) + '');
      faixaAnos.style.setProperty('--duo-cmp-p', fracao(faixaAnos) + '');
      /* O valor bruto de um range é anunciado como "5", sem unidade. O
         aria-valuetext é o que faz o leitor de tela dizer o que a pessoa vê
         — e é por isso que o <output> ao lado é aria-hidden. */
      faixaPat.setAttribute('aria-valuetext', faixaPat.value + ' milhões de reais');
      faixaAnos.setAttribute('aria-valuetext', anos(h));
      if (saiPat) saiPat.textContent = milhoes(faixaPat.value);
      if (saiAnos) saiAnos.textContent = anos(h);
      if (topo) topo.textContent = moeda(teto);
      if (elHoriz) elHoriz.textContent = anos(h);
      if (elPat) elPat.textContent = milhoes(faixaPat.value);
      if (elTaxa) elTaxa.textContent = pct(fa / p);

      /* O anúncio espera o arrasto parar. Uma região viva reescrita a cada
         evento de input diria trinta frases por segundo. */
      if (viva && !primeira) {
        clearTimeout(aviso);
        aviso = setTimeout(function () {
          viva.textContent = 'Em ' + anos(h) + ', sobre ' + milhoes(faixaPat.value) +
            ': custo com taxa acordada ' + moeda(fa * h) +
            '; custo com comissão embutida ' + moeda(ca * h) +
            '; diferença ' + moeda((ca - fa) * h) + '.';
        }, FALA);
      }
    }

    /* ------------------------------------------------------------- ligação */

    function aoMexer() {
      /* Solta as barras e apaga o atraso escalonado da entrada: sem isto o
         quadro responderia com até .28s de sono a cada movimento da faixa. */
      quadro.classList.add('is-live');
      render();
    }

    faixaPat.addEventListener('input', aoMexer);
    faixaAnos.addEventListener('input', aoMexer);

    /* Três marcos ou quatro é decisão de largura, e a largura muda sem
       ninguém tocar nas faixas. */
    if (estreito.addEventListener) estreito.addEventListener('change', render);
    else if (estreito.addListener) estreito.addListener(render);

    /* Primeira pintura: confere que o que está escrito no HTML bate com a
       conta, e só então libera os controles. A ordem importa — liberar antes
       abriria uma janela em que a faixa se move e o quadro ainda não sabe. */
    render();
    primeira = false;
    if (campos) campos.disabled = false;
  }

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

Dobra-ferramenta. Cabeçalho centrado e, abaixo, um painel com duas faixas de controle — patrimônio de R$ 1 mi a R$ 50 mi, horizonte de 1 a 20 anos —, um gráfico de oito barras verticais distribuídas em quatro marcos de tempo, a leitura em três números do horizonte escolhido, a linha de premissas em uso e a tabela com o custo acumulado de cada marco. Mover qualquer das duas faixas redesenha as barras e reescreve os quinze números da peça; os três do resumo contam até o valor novo.

Resolve, com forma, o argumento comercial que a biblioteca só sabia dizer em adjetivo: a diferença entre cobrar uma taxa previamente acordada e cobrar uma comissão embutida no produto. Escrito em texto, "somos fee-based" é uma afirmação sobre nós; desenhado, é uma conta que a pessoa refaz. O que se compara é custo — nunca resultado, nunca patrimônio final, nunca "quanto você teria a mais". Custo é aritmética verificável a partir de premissas declaradas; resultado seria promessa, e promessa não pode entrar em site de assessoria.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Olho Duas ou três palavras, no plural da casa ("Como cobramos"). Não é rótulo de produto.
Título Até 46 caracteres, com uma palavra em <em> — o .sec-title do core quebra em duas linhas a partir de ~25. A palavra em itálico é a que carrega o contraste ("dois modelos").
Apoio Duas linhas. Diz a diferença entre os dois modelos em uma frase cada, sem adjetivo. Não repete o título.
Rótulo da faixa Substantivo, até 26 caracteres. O valor fica à direita, no <output>, e não dentro do rótulo.
Extremos da faixa Os dois limites, escritos como a pessoa os lê ("R$ 1 mi", "20 anos"). São aria-hidden: a faixa já anuncia min e max.
Legenda das séries O nome de cada modelo, até 30 caracteres. É a única coisa que nomeia a cor — encurtar para "Taxa" e "Comissão" tira o sentido de quem chega pela legenda.
Rótulo do marco ano N, em caixa baixa. Escrito pelo script; não edite no markup.
Chaves do resumo Começam por "Custo com…" — a palavra custo aparece nas três, e é ela que impede a leitura de "rendimento".
Linha de premissas Os três percentuais, a taxa efetiva do patrimônio corrente e a frase do patrimônio constante. Obrigatória, e visível (ver Compliance).
Legenda da tabela Uma frase, terminando em "Valores ilustrativos."
Ressalva O modelo do §5 do contrato de ferramentas, em --fg-muted, no fim do painel. Nunca em letra miúda no rodapé da página.
Botão Verbo de conversa, não de cadastro ("Falar sobre custos"). Aponta para a âncora do formulário.

Especificação dos assets

Nenhum. Não há foto, ícone, logo nem SVG externo: o gráfico são oito <span> escalados por CSS, e todo o resto é texto. O que a peça pede no lugar de asset são números aprovados: os percentuais da política de remuneração do cliente, a data em que foram aprovados e a redação da ressalva revisada pelo compliance dele. Sem isso a peça não entra no ar.


Compliance

Esta é a seção que decide se a peça pode ser publicada. O consumidor tem de fazer as quatro coisas abaixo, e nenhuma delas é opcional.

  1. Trocar as premissas. A tabela PREMISSAS no topo do script.js (faixas da taxa acordada, percentual médio de comissão, repasse de rebate) é conteúdo ilustrativo. Substituir pelos números do cliente e datá-los na ficha do projeto. Reavaliar a cada revisão de tabela de honorários.
  2. Refletir a troca na linha visível. A .duo-cmp-premissas do markup repete os mesmos percentuais em texto. Trocar o script sem trocar a linha faz a peça mostrar uma conta que ela mesma desmente — é o pior defeito possível aqui, porque é invisível para quem revisa só o texto.
  3. Manter a fronteira custo × resultado. Nada na peça pode virar patrimônio final, rentabilidade, "você teria a mais" ou comparação de retorno. O patrimônio entra constante de propósito: fazê-lo crescer transformaria a comparação de custo em projeção de rentabilidade. Se alguém pedir "mostra também quanto rende", a resposta é não — e é a ficha que registra por quê.
  4. Passar a ressalva pelo compliance do cliente. O texto de .duo-cmp-nota diz simulação ilustrativa, sem projeção de rentabilidade, sem imposto e sem recomendação de investimento. Ele fica onde a pessoa lê o resultado, não no rodapé.

Além disso, e já resolvido no componente: nenhum nome de instituição real, nenhum superlativo de mercado, nenhum dado da empresa apresentado como auditado, nenhum dado pessoal coletado e nada gravado — sem localStorage, sem cookie, sem envio. Os dois números vivem em memória e morrem no recarregamento.


Como usar

<link rel="stylesheet" href="componentes/dados/comparador-cenarios/estilo.css">
<script src="componentes/dados/comparador-cenarios/script.js"></script>

A seção declara .sec-dark; o painel se resolve sozinho em qualquer escopo, porque o fundo dele é color-mix sobre --fg. Os dois id (duo-cmp-patrimonio e duo-cmp-anos) existem para os <label> e são únicos na página: duas dobras destas na mesma página pedem renumeração.

Ganchos

Gancho Papel
[data-duo-cmp] Raiz. O script parte daqui, nunca do documento.
[data-duo-cmp-fields] O <fieldset>. Nasce disabled no markup; o script o libera na última linha do init.
[data-duo-cmp-range] "wealth" e "years". O script lê min/max do próprio campo — mudar a amplitude no markup não pede mexer no script.
[data-duo-cmp-out] O <output> visível ao lado do rótulo. É aria-hidden: quem anuncia é o aria-valuetext da faixa.
[data-duo-cmp-plot] O quadro. Leva data-rv (entrada pelo core) e aria-hidden.
[data-duo-cmp-group] / [data-duo-cmp-bar] / [data-duo-cmp-mark] Um marco, suas duas barras e o rótulo. Marco que sobra recebe hidden.
[data-duo-cmp-scale] O valor do topo da escala. Sem ele, uma barra cheia não significa nada.
[data-duo-cmp-num] "fee", "com", "gap" — os três números que contam.
[data-duo-cmp-horizon] / [data-duo-cmp-wealth] / [data-duo-cmp-rate] O cenário escrito por extenso e a taxa efetiva do patrimônio corrente.
[data-duo-cmp-row] / [data-duo-cmp-cell] Linha da tabela e as quatro células (year, fee, com, gap). A tabela vive dentro de um .duo-cmp-scroll, que rola na horizontal quando ela não cabe.
[data-duo-cmp-live] Região aria-live só para leitor de tela, escrita 700ms depois que o arrasto para.
--duo-cmp-fee / --duo-cmp-com As duas cores de série. Leem --accent e --fg-muted; trocar aqui troca barra, quadradinho e número.
--duo-cmp-plot-h / --duo-cmp-bar-w / --duo-cmp-bar-gap / --duo-cmp-group-gap Geometria do quadro. 260px / 44px / 9px / 26px a 1440.
--duo-cmp-track / --duo-cmp-thumb Espessura do trilho (6px) e diâmetro do botão da faixa (20px).
--duo-cmp-grow / --duo-cmp-move .7s na entrada, .45s depois do primeiro toque.
--duo-cmp-h Altura da barra, em fração de 0 a 1. Escrita no style de cada barra — no markup para o estado padrão, pelo script depois.
--duo-cmp-d Atraso de entrada de cada barra (0 a .28s). O script o zera junto com .is-live.
--duo-cmp-p Fração percorrida da faixa, para o preenchimento do trilho.
.is-live No quadro, do primeiro toque nos controles em diante.

Variações


Gotchas


Histórico

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

Origem. Não há referência única. O padrão — dois ou três controles deslizantes que redesenham um gráfico simples e uma leitura em número — é recorrente em site de instituição financeira: calculadora de aposentadoria de gestora de fundos, comparador de taxa de corretora independente, simulador de portfólio de banco digital. Nenhuma linha veio de lugar nenhum: a forma foi decidida aqui, a partir do argumento comercial que a biblioteca precisava saber desenhar. Status externo — nunca rodou em produção. O primeiro site que usar promove, e a ficha registra qual foi. O registro consolidado vai em projetos/autorais.md.

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

Proposta para o core. A classe de texto só para leitor de tela (.duo-cmp-sr — position:absolute, 1×1px, clip-path:inset(50%), white-space:nowrap) já existe recopiada em midia/carrossel-anel-3d e em midia/galeria-pasta. Com esta, são três — que é exatamente o limiar do §6 do contrato para promover a primitiva. Sugestão: .sr no core/base.css, e os três componentes passam a lê-la.