<!-- ==========================================================================
DIAGRAMA HUB — dois territórios ligados a um núcleo em órbita
Prefixo: hub-
Requer: core (tokens, base, motion) + estilo.css e script.js
Os traços que ligam as pontas ao núcleo NÃO são coordenadas fixas: o
script lê a posição real dos elementos no DOM e escreve o "d" dos
paths. Por isso o diagrama acompanha qualquer largura, tradução ou
mudança de conteúdo sem ninguém redesenhar SVG.
Os traços nascem apagados (stroke-dashoffset) e são desenhados quando a
seção entra na tela.
========================================================================== -->
<sectionclass="sec-dark sec-pad"id="planejamento">
<divclass="wrap">
<divclass="sec-head">
<spanclass="eyebrow"data-rv>Wealth planning</span>
<h2class="sec-title ttl">Sua <em>família</em> no centro da estratégia</h2>
<pclass="sec-lead"data-rvdata-d="1">Conectamos alocação no Brasil e no exterior em uma arquitetura patrimonial única — pensada para o presente da sua empresa e o futuro da sua família.</p>
</div>
<divclass="hub"data-hub>
<svgclass="hub-links"data-hub-linksaria-hidden="true">
<pathclass="hub-link"data-hub-link="left"d=""></path>
<pathclass="hub-link"data-hub-link="right"d=""></path>
<pathclass="hub-link hub-link-down"data-hub-link="down"d=""></path>
</svg>
<divclass="hub-grid">
<divclass="hub-side"data-rv>
<divclass="hub-fig"data-hub-node="left">
<imgclass="hub-ic"src="../../../showroom/demo/icon-territorio.svg"alt=""aria-hidden="true">
</div>
<divclass="hub-cap"><b>Brasil</b><span>onshore</span></div>
<ulclass="hub-list">
<li>Multicustódia, fundo exclusivo e carteira administrada</li>
<li>Relatório de patrimônio consolidado</li>
<li>Alocação orientada pelo passivo</li>
<li>Holding familiar e estruturação tributária</li>
</ul>
</div>
<divclass="hub-core"data-hub-coredata-rvdata-d="1">
<svgclass="hub-orbit"viewBox="0 0 300 300"aria-hidden="true">
<circleclass="hub-ring"cx="150"cy="150"r="140"></circle>
<gclass="hub-spin">
<circleclass="hub-dot"cx="150"cy="10"r="4.5"></circle>
<circleclass="hub-dot is-sm"cx="150"cy="290"r="3"></circle>
</g>
</svg>
<divclass="hub-core-inner">
<strong>Sua família<br>no centro</strong>
</div>
</div>
<divclass="hub-side is-right"data-rvdata-d="2">
<divclass="hub-fig"data-hub-node="right">
<imgclass="hub-ic"src="../../../showroom/demo/icon-globo.svg"alt=""aria-hidden="true">
</div>
<divclass="hub-cap"><b>Exterior</b><span>offshore</span></div>
<ulclass="hub-list">
<li>Conta de pessoa física no exterior</li>
<li>Personal Investment Company</li>
<li>Fundo exclusivo offshore</li>
<li>Trust e seguro de vida internacional</li>
</ul>
</div>
</div>
<divclass="hub-foot"data-hub-footdata-rvdata-d="3">
<spanclass="hub-foot-mark"aria-hidden="true"></span>
<b>Terceira ramificação</b>
<p>Soluções para sócios e empresas: gestão de caixa, sucessão empresarial, seguros, câmbio, consultoria estratégica e M&A.</p>
</div>
</div>
</div>
</section>
estilo.css · 128 linhas · 5,6 KB
/* ==========================================================================
DIAGRAMA HUB — dois territórios ligados a um núcleo em órbita
Prefixo: hub-
Tokens usados: --fg --fg-2 --fg-muted --rule --rule-2 --accent --accent-2
--display --sans --ease
========================================================================== */.hub-head .eyebrow{margin-inline:auto}
/* nowrap: o título é curto e centralizado; quebrar em duas linhas aqui
desequilibra a composição simétrica do diagrama logo abaixo. */.hub-head .sec-title{max-width:none;margin-inline:auto;white-space:nowrap}
.hub-head .sec-lead{max-width:63ch;margin-inline:auto}
.hub{position:relative;margin-top:clamp(50px,8vh,86px)}
/* ---- os traços ----
O "d" de cada path é escrito pelo script, a partir da posição real dos
elementos. pathLength="1" (também posto pelo script) normaliza o
comprimento, então dasharray/dashoffset:1 vale para qualquer forma. */.hub-links{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;z-index:0}
.hub-link{fill:none;stroke:var(--rule-2);stroke-width:1;stroke-dasharray:1;stroke-dashoffset:1}
.hub.is-drawn .hub-link{transition:stroke-dashoffset 1.4svar(--ease);stroke-dashoffset:0}
.hub.is-drawn .hub-link-down{transition-delay:.25s}
.hub-grid{
display:grid;
grid-template-columns:minmax(0,1fr) clamp(210px,24vw,290px) minmax(0,1fr);
gap:clamp(20px,3vw,48px);align-items:center;position:relative;z-index:1;
}
/* ---- figuras ---- */.hub-fig{width:clamp(78px,9vw,112px);margin-bottom:22px}
.is-right .hub-fig{margin-left:auto}
/* Os ícones chegam como arquivo, então entram por fade. O "desenho" por
stroke-dashoffset só funcionaria com o SVG inline. */.hub-ic{
width:100%;height:auto;display:block;
opacity:0;transform:translateY(8px);
transition:opacity 1svar(--ease) .15s,transform 1svar(--ease) .15s;
}
.hub.is-drawn .hub-ic{opacity:1;transform:none}
@media(prefers-reduced-motion:reduce){.hub-ic{opacity:1;transform:none;transition:none}}
/* ---- núcleo ----
Sem borda nem fundo próprio: o único círculo da composição é o anel
externo. Um segundo círculo em volta do texto vira alvo, não centro. */.hub-core{position:relative;aspect-ratio:1;display:grid;place-items:center}
.hub-orbit{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.hub-ring{fill:none;stroke:var(--rule-2);stroke-width:1}
.hub-spin{transform-origin:150px150px;animation:hubOrbit 26s linear infinite}
.hub-dot{fill:var(--accent)}
.hub-dot.is-sm{fill:var(--accent-2)}
@keyframes hubOrbit{from{transform:rotate(0)}to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){.hub-spin{animation:none}}
.hub-core-inner{
position:relative;z-index:2;text-align:center;padding:16px;
width:62%;aspect-ratio:1;display:grid;place-content:center;
}
.hub-core-inner strong{
display:block;font-family:var(--sans);font-style:italic;font-weight:300;
font-size:clamp(15px,1.7vw,19px);line-height:1.3;color:var(--fg);
}
/* ---- listas laterais ---- */.hub-side{min-width:0}
.hub-cap{display:flex;align-items:baseline;gap:10px;padding-bottom:14px;border-bottom:1px solid var(--rule)}
.hub-cap b{
font-family:var(--display);font-weight:700;font-size:12.5px;
letter-spacing:.1em;text-transform:uppercase;color:var(--accent);
}
.hub-cap span{font-size:11px;color:var(--fg-muted);letter-spacing:.06em}
.hub-list li{
font-size:15px;line-height:1.6;color:var(--fg-2);
padding:13px0;border-bottom:1px solid var(--rule);
display:flex;gap:11px;align-items:flex-start;
transition:color .3s,border-color .3s;
}
.hub-list li:hover{color:var(--fg);border-bottom-color:var(--rule-2)}
.hub-list li::before{
content:"";flex:00 auto;width:5px;height:5px;margin-top:8px;border-radius:50%;
background:var(--accent-2);transition:background .3s,transform .3s;
}
.hub-list li:hover::before{background:var(--accent);transform:scale(1.3)}
/* A coluna da direita é espelhada: texto, marcador e legenda invertem. */.hub-side.is-right{text-align:right}
.hub-side.is-right .hub-cap{justify-content:flex-end}
.hub-side.is-right .hub-list li{flex-direction:row-reverse}
/* ---- terceira ramificação ----
Não é card avulso: pendura no mesmo traço que liga as pontas ao núcleo
(o hub-link-down, calculado a partir da base do núcleo). */.hub-foot{
margin-top:clamp(46px,7vh,72px);padding-top:clamp(22px,3vh,32px);
display:flex;flex-direction:column;align-items:center;text-align:center;
position:relative;z-index:1;
}
.hub-foot-mark{
position:absolute;top:0;left:50%;width:6px;height:6px;background:var(--accent-2);
transform:translate(-50%,-50%) rotate(45deg);
}
.hub-foot b{
font-family:var(--display);font-weight:700;font-size:12px;
letter-spacing:.1em;text-transform:uppercase;color:var(--accent);white-space:nowrap;
}
.hub-foot p{font-size:15px;line-height:1.6;color:var(--fg-muted);max-width:50ch;margin-top:10px}
/* ---- mobile ----
Uma coluna, tudo centralizado, sem traços: em pilha vertical eles ligariam
coisas que já estão obviamente ligadas. */@media(max-width:900px){
.hub-grid{grid-template-columns:1fr;gap:38px}
.hub-links{display:none}
.hub-core{width:100%;max-width:224px;margin-inline:auto}
.hub-core-inner{width:72%;padding:14px;place-items:center}
.hub-fig,.is-right .hub-fig{margin-inline:auto}
.hub-side,.hub-side.is-right{text-align:center}
.hub-cap,.hub-side.is-right .hub-cap{justify-content:center}
.hub-list li,.hub-side.is-right .hub-list li{display:block;text-align:center}
.hub-list li::before{display:none}
.hub-cap b{font-size:14px}
.hub-foot b{font-size:14px}
}
@media(max-width:560px){.hub-head .sec-title{white-space:normal}}
script.js · 83 linhas · 3,4 KB
/* ==========================================================================
DIAGRAMA HUB — traços calculados sobre a posição real do DOM
A ideia toda: em vez de desenhar um SVG com coordenadas fixas, medimos
onde os elementos realmente caíram e escrevemos o "d" dos paths. O
diagrama passa a acompanhar qualquer largura, qualquer tradução e
qualquer mudança de conteúdo sem ninguém abrir o Illustrator.
========================================================================== */
(function () {
document.querySelectorAll('[data-hub]').forEach(function (hub) {
var svg = hub.querySelector('[data-hub-links]');
var core = hub.querySelector('[data-hub-core]');
if (!svg || !core) return;
var linkL = hub.querySelector('[data-hub-link="left"]');
var linkR = hub.querySelector('[data-hub-link="right"]');
var linkD = hub.querySelector('[data-hub-link="down"]');
var noL = hub.querySelector('[data-hub-node="left"]');
var noR = hub.querySelector('[data-hub-node="right"]');
var pe = hub.querySelector('[data-hub-foot]');
/* pathLength="1" normaliza o comprimento de qualquer traço, então o
dasharray/dashoffset:1 do CSS vale para todos. Escrever o offset
inline aqui NÃO funcionaria: estilo inline vence a regra .is-drawn e
o traço nunca chegaria a zero. */function normalizar(el) { el.setAttribute('pathLength', '1'); }
function ponto(el, lado) {
var r = el.getBoundingClientRect();
var h = hub.getBoundingClientRect();
var x = lado === 'left' ? r.left : lado === 'right' ? r.right : r.left + r.width / 2;
var y = lado === 'top' ? r.top : lado === 'bottom' ? r.bottom : r.top + r.height / 2;
return { x: x - h.left, y: y - h.top };
}
function reta(a, b) { return'M' + a.x + ',' + a.y + ' L' + b.x + ',' + b.y; }
function medir() {
if (innerWidth <= 900) return; // no mobile não há traçosvar h = hub.getBoundingClientRect();
svg.setAttribute('viewBox', '0 0 ' + h.width + ' ' + h.height);
var esq = ponto(noL, 'right');
var dir = ponto(noR, 'left');
/* Linha reta e nivelada com o ícone, mas partindo da BORDA da div do
núcleo (não do anel visível). A div é maior que o anel, então isso
deixa a mesma folga entre traço e anel nos três links — inclusive
no de baixo, que sai da base da mesma div. */var coreE = { x: ponto(core, 'left').x, y: esq.y };
var coreD = { x: ponto(core, 'right').x, y: dir.y };
linkL.setAttribute('d', reta(esq, coreE));
linkR.setAttribute('d', reta(dir, coreD));
normalizar(linkL); normalizar(linkR);
if (pe && linkD) {
linkD.setAttribute('d', reta(ponto(core, 'bottom'), ponto(pe, 'top')));
normalizar(linkD);
}
}
hub.querySelectorAll('.hub-link').forEach(normalizar);
medir();
/* Fontes carregam depois do primeiro layout e mudam a altura das listas,
o que move os nós. Medir de novo no load resolve. */
addEventListener('load', medir);
var t;
addEventListener('resize', function () {
clearTimeout(t);
t = setTimeout(medir, 150);
}, { passive: true });
new IntersectionObserver(function (es, obs) {
if (!es[0].isIntersecting) return;
hub.classList.add('is-drawn');
obs.disconnect();
}, { threshold: .2 }).observe(hub);
});
})();
Composição simétrica: dois territórios com listas de itens nas laterais, um núcleo circular no meio com um ponto em órbita, e uma terceira ramificação pendurada embaixo. Os traços que ligam tudo são SVG desenhados em runtime, a partir da posição real que os elementos ocuparam na tela — e entram sendo traçados quando a seção aparece.
Nenhuma coordenada é fixa. É o que faz o diagrama sobreviver a qualquer largura, tradução ou mudança de texto sem ninguém reabrir o Illustrator.
Quando usar
Explicar uma arquitetura: duas frentes que convergem para um centro. Onshore/offshore, produto/serviço, pessoa física/jurídica, aquisição/retenção.
Quando o argumento é "isso tudo é uma coisa só", e uma lista de bullets não diz isso.
Seção de meio de página, depois de o visitante já saber o que a empresa faz.
Quando não usar
Fluxo com etapas em sequência. Isso é listas/baralho-sticky, não hub.
Mais de duas laterais. A simetria é o que sustenta a leitura; com três pontas vira organograma.
Quando os dois lados não são equivalentes em peso. Um lado com 4 itens e outro com 1 quebra a composição.
Padrão editorial
Slot
Regra
Título
1 linha curta, uma palavra em <em>. Ele é nowrap no desktop — se não couber em uma linha, encurte o texto, não mexa no CSS.
4 itens de cada lado, mesma contagem. 5 a 9 palavras cada.
.hub-core-inner
3 a 5 palavras, com <br> manual. É a tese da seção: "Sua família no centro".
.hub-foot
Rótulo curto em caixa alta e 1 frase de até 50ch.
Contagem igual dos dois lados é estrutural. O align-items:center do grid centraliza cada coluna; com listas de alturas muito diferentes, os traços saem em alturas que não conversam.
Sem terceira ramificação — apague .hub-foot e o path down. O script já testa se existe.
Núcleo sem órbita — apague o .hub-spin; sobra o anel parado.
Gotchas
pathLength="1" é o truque todo. Ele normaliza o comprimento de qualquer traço, então stroke-dasharray:1; stroke-dashoffset:1 no CSS vale para todos, independente do tamanho real.
Não escreva o stroke-dashoffset inline no script. Estilo inline vence a regra .is-drawn, e o traço nunca chegaria a zero — fica invisível para sempre. Esse bug custa meia hora para achar.
Os traços saem da borda da div do núcleo, não do anel visível. A div é maior que o anel; usar a borda dela deixa a mesma folga entre traço e anel nos três links, inclusive no de baixo. Mirar no anel deixaria o de baixo colado e os laterais soltos.
medir() roda de novo no load. As fontes carregam depois do primeiro layout e mudam a altura das listas, o que move os nós — sem o segundo cálculo os traços ficam fora de lugar em recarga fria.
O núcleo não tem borda nem fundo próprio. O único círculo da composição é o anel externo. Um segundo círculo em volta do texto vira alvo, não centro.
overflow:visible no SVG — sem isso o traço é cortado quando encosta na borda da caixa.
No mobile os traços somem. Em pilha vertical eles ligariam coisas que já estão obviamente ligadas, e cruzariam o texto.
Histórico
Projeto
Onde
Data
Diferenças
Liberta Wealth
index.html#planejamento + main.js
2026-08
Origem. Ids #hub, #hubLinks, #linkL/R/D, #hubCore, #figBR, #figEX — o script conhecia cada elemento pelo nome. Classes .cap, .o-ring, .o-dot, .o-spin, .hub-foot-dm fora do prefixo, .drawn como estado, --sage/--tan cruas.
Normalizações aplicadas na extração: ids → [data-hub-*], o que permite mais de um diagrama na página e desacopla o script dos nomes do conteúdo (figBR/figEX amarravam o código a "Brasil" e "Exterior"); .cap → .hub-cap, .o-* → .hub-ring/.hub-dot/.hub-spin, .hub-foot-dm → .hub-foot-mark; .drawn → .is-drawn; --sage/--tan → --accent/--accent-2.
Código morto removido: o CSS de origem ainda trazia .geo-line, .geo-wire, .geo-arc e .geo-pin — classes de uma versão anterior em que os mapas eram SVG inline "desenhados" por stroke-dashoffset. Na versão que rodou, os mapas viraram <img> e essas regras nunca mais foram usadas. Não vieram para a biblioteca.