<!-- ==========================================================================
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.
========================================================================== -->
<sectionclass="sec-dark sec-pad duo-cmp"data-duo-cmp>
<divclass="wrap">
<divclass="sec-head">
<pclass="eyebrow">Como cobramos</p>
<h2class="sec-title ttl">O mesmo patrimônio, <em>dois</em> modelos de custo</h2>
<pclass="sec-lead"data-rvdata-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>
<divclass="duo-cmp-panel"data-rvdata-d="1">
<!-- ============================================================ controles --><!-- disabled de propósito: quem tira é o script. Ver o cabeçalho. -->
<fieldsetclass="duo-cmp-fields"data-duo-cmp-fieldsdisabled>
<legendclass="duo-cmp-fields-title">Ajuste o cenário</legend>
<divclass="duo-cmp-field">
<divclass="duo-cmp-field-top">
<labelclass="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. -->
<outputclass="duo-cmp-out"for="duo-cmp-patrimonio"aria-hidden="true"data-duo-cmp-out="wealth">R$ 5 mi</output>
</div>
<inputclass="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">
<divclass="duo-cmp-ends"aria-hidden="true">
<spanclass="duo-cmp-end">R$ 1 mi</span>
<spanclass="duo-cmp-end">R$ 50 mi</span>
</div>
</div>
<divclass="duo-cmp-field">
<divclass="duo-cmp-field-top">
<labelclass="duo-cmp-label"for="duo-cmp-anos">Horizonte</label>
<outputclass="duo-cmp-out"for="duo-cmp-anos"aria-hidden="true"data-duo-cmp-out="years">20 anos</output>
</div>
<inputclass="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">
<divclass="duo-cmp-ends"aria-hidden="true">
<spanclass="duo-cmp-end">1 ano</span>
<spanclass="duo-cmp-end">20 anos</span>
</div>
</div>
</fieldset>
<pclass="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 -->
<divclass="duo-cmp-grid">
<divclass="duo-cmp-chart">
<divclass="duo-cmp-chart-head">
<ulclass="duo-cmp-keys">
<liclass="duo-cmp-key">
<spanclass="duo-cmp-swatch duo-cmp-swatch-fee"aria-hidden="true"></span>
Taxa previamente acordada
</li>
<liclass="duo-cmp-key">
<spanclass="duo-cmp-swatch duo-cmp-swatch-com"aria-hidden="true"></span>
Comissão embutida no produto
</li>
</ul>
<pclass="duo-cmp-scale">
Topo da escala
<spanclass="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. -->
<divclass="duo-cmp-plot"data-duo-cmp-plotdata-rvaria-hidden="true">
<divclass="duo-cmp-group"data-duo-cmp-group>
<divclass="duo-cmp-bars">
<spanclass="duo-cmp-bar duo-cmp-bar-fee"data-duo-cmp-bar="fee"style="--duo-cmp-h:.026;--duo-cmp-d:0s"></span>
<spanclass="duo-cmp-bar duo-cmp-bar-com"data-duo-cmp-bar="com"style="--duo-cmp-h:.05;--duo-cmp-d:.04s"></span>
</div>
<spanclass="duo-cmp-mark"data-duo-cmp-mark>ano 1</span>
</div>
<divclass="duo-cmp-group"data-duo-cmp-group>
<divclass="duo-cmp-bars">
<spanclass="duo-cmp-bar duo-cmp-bar-fee"data-duo-cmp-bar="fee"style="--duo-cmp-h:.129;--duo-cmp-d:.08s"></span>
<spanclass="duo-cmp-bar duo-cmp-bar-com"data-duo-cmp-bar="com"style="--duo-cmp-h:.25;--duo-cmp-d:.12s"></span>
</div>
<spanclass="duo-cmp-mark"data-duo-cmp-mark>ano 5</span>
</div>
<divclass="duo-cmp-group"data-duo-cmp-group>
<divclass="duo-cmp-bars">
<spanclass="duo-cmp-bar duo-cmp-bar-fee"data-duo-cmp-bar="fee"style="--duo-cmp-h:.257;--duo-cmp-d:.16s"></span>
<spanclass="duo-cmp-bar duo-cmp-bar-com"data-duo-cmp-bar="com"style="--duo-cmp-h:.5;--duo-cmp-d:.2s"></span>
</div>
<spanclass="duo-cmp-mark"data-duo-cmp-mark>ano 10</span>
</div>
<divclass="duo-cmp-group"data-duo-cmp-group>
<divclass="duo-cmp-bars">
<spanclass="duo-cmp-bar duo-cmp-bar-fee"data-duo-cmp-bar="fee"style="--duo-cmp-h:.514;--duo-cmp-d:.24s"></span>
<spanclass="duo-cmp-bar duo-cmp-bar-com"data-duo-cmp-bar="com"style="--duo-cmp-h:1;--duo-cmp-d:.28s"></span>
</div>
<spanclass="duo-cmp-mark"data-duo-cmp-mark>ano 20</span>
</div>
</div>
</div>
<!-- ============================================================ leitura -->
<asideclass="duo-cmp-read">
<pclass="duo-cmp-read-head">
Em <spanclass="duo-cmp-read-em"data-duo-cmp-horizon>20 anos</span>,
sobre <spanclass="duo-cmp-read-em"data-duo-cmp-wealth>R$ 5 mi</span>
</p>
<!-- CONTEÚDO ILUSTRATIVO -->
<divclass="duo-cmp-stat">
<spanclass="duo-cmp-stat-k">Custo com taxa acordada</span>
<spanclass="duo-cmp-stat-v duo-cmp-stat-fee"data-duo-cmp-num="fee">R$ 1,08 mi</span>
</div>
<divclass="duo-cmp-stat">
<spanclass="duo-cmp-stat-k">Custo com comissão embutida</span>
<spanclass="duo-cmp-stat-v duo-cmp-stat-com"data-duo-cmp-num="com">R$ 2,10 mi</span>
</div>
<divclass="duo-cmp-stat duo-cmp-stat-gap">
<spanclass="duo-cmp-stat-k">Diferença de custo</span>
<spanclass="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 -->
<pclass="duo-cmp-premissas">
<spanclass="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,
<spanclass="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>
<aclass="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. -->
<divclass="duo-cmp-scroll">
<tableclass="duo-cmp-table">
<captionclass="duo-cmp-cap">
Custo acumulado em cada modelo, por marco de tempo. Valores ilustrativos.
</caption>
<theadclass="duo-cmp-thead">
<trclass="duo-cmp-tr">
<thclass="duo-cmp-th"scope="col">Marco</th>
<thclass="duo-cmp-th"scope="col">Taxa acordada</th>
<thclass="duo-cmp-th"scope="col">Comissão embutida</th>
<thclass="duo-cmp-th"scope="col">Diferença</th>
</tr>
</thead>
<tbodyclass="duo-cmp-tbody">
<trclass="duo-cmp-tr"data-duo-cmp-row>
<thclass="duo-cmp-rh"scope="row"data-duo-cmp-cell="year">ano 1</th>
<tdclass="duo-cmp-td"data-duo-cmp-cell="fee">R$ 54 mil</td>
<tdclass="duo-cmp-td"data-duo-cmp-cell="com">R$ 105 mil</td>
<tdclass="duo-cmp-td duo-cmp-td-gap"data-duo-cmp-cell="gap">R$ 51 mil</td>
</tr>
<trclass="duo-cmp-tr"data-duo-cmp-row>
<thclass="duo-cmp-rh"scope="row"data-duo-cmp-cell="year">ano 5</th>
<tdclass="duo-cmp-td"data-duo-cmp-cell="fee">R$ 270 mil</td>
<tdclass="duo-cmp-td"data-duo-cmp-cell="com">R$ 525 mil</td>
<tdclass="duo-cmp-td duo-cmp-td-gap"data-duo-cmp-cell="gap">R$ 255 mil</td>
</tr>
<trclass="duo-cmp-tr"data-duo-cmp-row>
<thclass="duo-cmp-rh"scope="row"data-duo-cmp-cell="year">ano 10</th>
<tdclass="duo-cmp-td"data-duo-cmp-cell="fee">R$ 540 mil</td>
<tdclass="duo-cmp-td"data-duo-cmp-cell="com">R$ 1,05 mi</td>
<tdclass="duo-cmp-td duo-cmp-td-gap"data-duo-cmp-cell="gap">R$ 510 mil</td>
</tr>
<trclass="duo-cmp-tr"data-duo-cmp-row>
<thclass="duo-cmp-rh"scope="row"data-duo-cmp-cell="year">ano 20</th>
<tdclass="duo-cmp-td"data-duo-cmp-cell="fee">R$ 1,08 mi</td>
<tdclass="duo-cmp-td"data-duo-cmp-cell="com">R$ 2,10 mi</td>
<tdclass="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. -->
<pclass="duo-cmp-sr"data-duo-cmp-livearia-live="polite"></p>
<pclass="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) 0calc(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 .2svar(--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:8px22px;
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:00 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:00,050%;
pointer-events:none;
}
.duo-cmp-group{
flex:110;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:0018px;
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:12px0;
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:18px00;
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:10px14px10px0;
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:12px14px12px0;
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:12px14px12px0;
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) 00;
--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:0clamp(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:10px0}
}
/* 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);
elseif (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
Na página de "como cobramos" ou de honorários, logo depois do texto que explica o modelo — a peça é a prova do parágrafo anterior.
Quando a assessoria tem a taxa e a política de remuneração fechadas e pode publicá-las. Sem números que o compliance aprove, a peça não existe.
Quando o público tem patrimônio o bastante para que a diferença seja visível: abaixo de R$ 1 mi a distância entre os dois modelos fica pequena na tela e o argumento enfraquece.
Quando não usar
Quando o cliente não é fee-based, ou é misto. A peça é uma acusação a um modelo de remuneração; usá-la sem estar do lado certo dela é tiro no pé.
Para mostrar número já consolidado, sem interação — patrimônio sob gestão, anos de casa: prova/stats-contador-regua, que conta e não pergunta nada.
Para coletar dado da pessoa. Esta peça não coleta nada e não grava nada; a captura é do conversao/cta-formulario-lead, para onde o botão do fim aponta.
Quando a página já tem outra ferramenta interativa na mesma rolagem. Duas peças que pedem para serem mexidas competem, e nenhuma das duas é usada.
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.
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.
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.
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ê.
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.
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.
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
com repasse de rebate — PREMISSAS.repasse:0.35e a mesma informação na linha visível.
taxa linear em vez de regressiva — uma faixa só. A conta fica mais fácil de explicar e o controle de patrimônio deixa de mudar a forma do gráfico.
sem a tabela — só com o aria-hidden do quadro removido e o valor de cada barra visível: a tabela é a leitura acessível.
horizonte de 30 anos — max="30" e ESCADA:[1,5,10,20,30]. Os marcos passam a 5/10/20/30.
em dobra clara — .sec-light na seção; confira só o contraste da série de comissão.
Gotchas
As premissas moram em dois lugares e têm de ser trocadas nos dois.PREMISSAS, no topo do script.js, faz a conta; a linha .duo-cmp-premissas, no markup, diz à pessoa qual conta está sendo feita. Trocar uma sem a outra produz uma peça que mente com números certos — e ninguém percebe, porque as duas metades continuam plausíveis sozinhas. Se um dia isso puder ser uma fonte só, será o script escrevendo a linha; hoje ele escreve apenas a taxa efetiva ([data-duo-cmp-rate]), que é a única parte que muda com o controle.
A escala é relativa, e é por isso que o controle de patrimônio move só metade das barras. O topo do quadro é o custo do modelo de comissão no último marco, então a barra cinza do fim está sempre cheia. Medido a 1440: mover o horizonte de 20 para 12 anos muda 6 das 8 alturas (6,7 → 11,1 px; 13 → 21,7; 33,4 → 55,7; 65 → 108,3; 66,9 → 111,4; 130 → 216,7; as duas do último marco ficam em 133,7 e 260 por construção); mover o patrimônio de R$ 5 mi para R$ 30 mi muda as 4 barras de taxa (6,7 → 4,5; 33,4 → 22,6; 66,9 → 45,2; 133,7 → 90,4) e nenhuma das de comissão. Não é defeito: custo de comissão é linear no patrimônio, então ele não pode se mover contra si mesmo. O que a pessoa vê é a barra de taxa afundando conforme o patrimônio sobe, que é exatamente o argumento — e o número do topo da escala (R$ 2,10 mi → R$ 12,60 mi) é o que impede a leitura de "não mudou nada".
toFixed(2) arredonda, e às vezes o topo da escala mente por R$ 5 mil. Com 19 anos e R$ 5 mi o teto é R$ 1.995.000 e sai escrito "R$ 2,00 mi". É arredondamento honesto de duas casas em milhão, e a tabela logo abaixo traz o valor de cada marco com a mesma regra — mas quem for comparar com uma planilha vai encontrar essa diferença antes de encontrar qualquer outra.
A barra tem piso de altura, e sem ele "quase zero" leria como "zero".PISO = 0.012 — 3,1px num quadro de 260. Com a escada e o horizonte de fábrica ele nunca chega a valer: a menor barra possível é 0,0161 (taxa acordada no ano 1 de um horizonte de 20, com R$ 50 mi — medido em 4,2px), e o cenário padrão fica em 6,7px. Ele existe para a variação de 30 anos, onde a mesma barra cai para 0,0108 e passaria a sumir. Quem tirar o piso ganha exatidão de pixel e perde a informação de que existe custo ali.
display escrito pelo componente vence o [hidden] da folha do navegador..duo-cmp-group tem display:flex, que é regra de autor e passa por cima do [hidden]{display:none} da folha do agente, que é de origem mais fraca. Sem a linha .duo-cmp-group[hidden]{display:none}, esconder um marco pelo atributo não faria absolutamente nada — e o horizonte de 5 anos mostraria quatro marcos, dois deles do cenário anterior. A tabela não precisa da mesma linha porque o CSS nunca escreve display em .duo-cmp-tr.
O <fieldset> nasce disabled de propósito, e quem o libera é a última linha do init. Sem script, a peça não é casca: os controles ficam visivelmente inertes (opacidade .42, cursor not-allowed), o aviso .duo-cmp-fallback aparece pelo seletor :disabled + …, e o cenário padrão continua desenhado — as oito alturas estão no style de cada barra e os quinze números estão no HTML. Medido removendo o <script src="script.js"> de uma cópia: barras em 6,8 / 13 / 33,5 / 65 / 66,8 / 130 / 133,6 / 260 px (contra 6,7 / 13 / 33,4 / 65 / 66,9 / 130 / 133,7 / 260 com script — a diferença é o arredondamento de três casas do markup), tabela e resumo idênticos, página 38px mais alta por causa do aviso. A alternativa — deixar a faixa habilitada — daria controles que se movem sem efeito, que é mentir.
O atraso de entrada das barras tem de morrer no primeiro toque.--duo-cmp-d escalona a entrada em até .28s, o que é bonito uma vez e insuportável a partir da segunda: sem a classe .is-live, cada movimento da faixa responderia com até 280ms de sono. .is-live zera o atraso e encurta a duração de .7s para .45s — a entrada é espetáculo, a resposta ao controle é ferramenta.
A regra de movimento reduzido precisa da mesma especificidade das outras duas..duo-cmp-plot.in .duo-cmp-bar e .duo-cmp-plot.is-live .duo-cmp-bar são (0,2,0). Um @media(prefers-reduced-motion) escrito como .duo-cmp-bar{transition:none} — (0,1,0) — seria vencido pelo transition-duration do .is-live, e a barra voltaria a animar depois do primeiro toque, só para quem pediu que ela não animasse. Por isso a regra lá está escrita .duo-cmp-plot .duo-cmp-bar, e por isso ela vem depois das duas. Medido com --reduce: barra no valor final já no quadro 0, e o número do resumo salta direto para R$ 11,22 mi em vez de contar (contra R$ 4,15 mi no quadro 2 e R$ 10,44 mi aos 120ms sem a preferência).
Abaixo de 560px o quadro passa a três marcos, e quem decide é o script. Quatro grupos de duas barras em 308px de quadro deixariam cada barra com ~12px. Com três, elas ficam em 27,3px — medido a 390. Quem sai é o marco mais antigo (o "ano 1"), porque a escada guarda os últimos que couberem, e a tabela acompanha: são três linhas, não quatro. Se isso for perda inaceitável para um cliente, o caminho é subir o limiar da media query e o do matchMedia do script — os dois leem 560, e separá-los faz o quadro e a tabela discordarem. Entre 560 e ~640 as barras ficam no piso de 18px do clamp — legíveis, mas no limite; é a faixa mais apertada da peça. A tabela, essa, nunca espreme: as células são nowrap e ela rola dentro do .duo-cmp-scroll — a 320px ela mede 270 numa caixa de 223 e rola 47px, em vez de passar por cima da borda do painel.
O <output> é aria-hidden e quem fala é o aria-valuetext. Um range anuncia "5", sem unidade; o aria-valuetext o faz anunciar "5 milhões de reais" e "20 anos". Sem esconder o <output>, o leitor de tela diria o valor duas vezes a cada seta. E o resultado não fica numa região viva direta: durante o arrasto ele muda dezenas de vezes por segundo, então o anúncio espera 700ms de silêncio e sai de uma vez, numa frase só, do [data-duo-cmp-live].
Nada é gravado, e isso é decisão, não esquecimento. Sem localStorage, sem cookie, sem envio: são dados financeiros de quem está lendo, e a captura é trabalho do conversao/cta-formulario-lead. Quem "melhorar" a peça guardando o último cenário passa a manter dado financeiro no navegador de terceiro sem consentimento nenhum.
A tabela não é redundância. O quadro leva aria-hidden porque é a ilustração; a tabela é o dado, com cabeçalho de linha e de coluna. Quem apagar a tabela para "enxugar" tem de tirar o aria-hidden e pôr o valor de cada barra como texto visível — senão quem usa leitor de tela fica com dois controles e nenhum resultado.
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:
Gráfico em DOM, não em SVG. O §3 do brief de ferramentas pede SVG contra canvas, e o que se evitava era o pixel do canvas — que aqui não entra. São oito elementos, cada um precisando de transição própria em transform e de uma variável escrita no style; em SVG isso seria <rect> com a mesma quantidade de código e sem ganho nenhum. O que a regra realmente exige — forma que escala, cor de token, dado inspecionável e leitura em texto — está tudo de pé.
Escala relativa com o topo escrito, em vez de eixo absoluto. Um eixo fixo na amplitude inteira dos controles (R$ 50 mi × 2,1% × 20 anos = R$ 21 mi) deixaria o cenário padrão com a barra mais alta em 10% do quadro.
Patrimônio constante no tempo, sem capitalização — ver Compliance.
transform:scaleY() com transform-origin:bottom, como o §3 manda, e o desfoque de entrada vindo do data-rv do core no quadro inteiro, em vez de um filter por barra: oito filtros animando juntos custam caro e o efeito visual é o mesmo.
Movimento por tempo, com teto de 50ms de dt, e um requestAnimationFrame por instância que dorme quando os três números chegam ao alvo.
Nenhum !important, nenhum seletor de elemento, nenhuma cor crua. Os pseudo-elementos de faixa (::-webkit-slider-thumb, ::-moz-range-track) são a única coisa que o contrato não previa, e ficam presos à classe .duo-cmp-range.
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.