← Biblioteca componentes/hero/duas-colunas-produto/ficha.md CódigoTela cheia
hero/duas-colunas-produto

Hero de duas colunas com palco de produto

🟢 estavel complexidade alta prefixo stage- hero above-the-fold duas-colunas carrossel produto saas cards-flutuantes movimento-continuo narrativa claro

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

<link rel="stylesheet" href="componentes/hero/duas-colunas-produto/estilo.css">
<script src="componentes/hero/duas-colunas-produto/script.js"></script>

markup.html · 199 linhas · 10,7 KB

<!-- ==========================================================================
     HERO DE DUAS COLUNAS COM PALCO DE PRODUTO
     Prefixo: stage-
     Requer: core/tokens.css, core/base.css, core/motion.css, core/motion.js
             + este estilo.css e este script.js
     Compõe (opcional): efeitos/cursor-anotacao — as duas bolhas .cur-note
             abaixo. Apague o bloco .cur-layer e o hero continua inteiro.

     Texto à esquerda, palco à direita. O palco é um trilho de slides que
     translada; cada slide é uma foto com cards de interface flutuando por
     cima, posicionados em porcentagem da própria foto.

     Os cards têm dois modos, e a diferença é o data-stage-step:
       SEM step  -> card permanente, sempre visível (o caso do slide 2)
       COM step  -> entra em sequência, um passo por vez, em laço (slide 1)
     Um slide pode misturar os dois. Os passos são numerados a partir de 1 e
     a ordem é a numérica, não a do markup.

     O carrossel é MANUAL, como a referência: só as setas viram o slide.
     Autoplay é variação — ver a ficha.
     ========================================================================== -->

<section class="stage sec-light" data-stage
         aria-roledescription="carrossel" aria-label="Como a assessoria funciona">

  <!-- ---------------------------------------------------------------------
       COMPOSIÇÃO — legendas de cursor (efeitos/cursor-anotacao)
       Não é deste componente. Vive aqui porque é assim que a referência
       monta a dobra, e porque a camada precisa de uma caixa relativa: a
       <section> serve, e o core já lhe dá position:relative.
       As âncoras estão em porcentagem DA SEÇÃO, não da coluna de texto.

       .cur-layer-desktop apaga a camada abaixo de 980px, que é exatamente
       onde esta grade colapsa para uma coluna. Sem isso a bolha de cima cai
       sobre a headline e a de baixo sobre a foto — medido no aparelho.
       -------------------------------------------------------------------- -->
  <div class="cur-layer cur-layer-desktop">
    <span class="cur-note cur-note-br cur-note-deep"
          data-cur-note data-cur-pos="6% 12%; 16% 12%" data-cur-hold="2600">
      <span class="cur-body" data-rv>
        <span class="cur-tip" aria-hidden="true">
          <svg viewBox="0 0 22 26" fill="none"><path d="M1 1L1 19L6.2 14.2L9.6 22.4L12.8 21L9.4 13L17.4 12.6Z" fill="currentColor"/></svg>
        </span>
        <span class="cur-bubble">Carteira desenhada para o seu horizonte, não para a meta do banco</span>
      </span>
    </span>

    <!-- Bolha estreita de propósito: na referência esta tem 112px contra os
         187px da outra, e é o que a faz caber entre o link secundário e a
         borda da coluna. --cur-max é o gancho do cursor-anotacao para isso. -->
    <span class="cur-note cur-note-tl"
          style="--cur-max:152px"
          data-cur-note data-cur-pos="40% 84%; 44% 84%" data-cur-hold="3100">
      <span class="cur-body" data-rv data-d="2">
        <span class="cur-tip" aria-hidden="true">
          <svg viewBox="0 0 22 26" fill="none"><path d="M1 1L1 19L6.2 14.2L9.6 22.4L12.8 21L9.4 13L17.4 12.6Z" fill="currentColor"/></svg>
        </span>
        <span class="cur-bubble">Relatório consolidado de todas as instituições</span>
      </span>
    </span>
  </div>

  <div class="wrap stage-grid">

    <!-- ---------------- coluna de texto ---------------- -->
    <div class="stage-copy">
      <!-- data-split e não .ttl: a headline é longa (dez palavras) e a
           revelação palavra a palavra sustenta. Headline curta de três
           palavras se desmonta assim — ali o certo é o reveal por linha do
           hero/full-bleed-media. -->
      <h1 class="stage-title" data-split>
        Seu patrimônio organizado com <em>uma assessoria que responde só a você.</em>
      </h1>

      <p class="stage-desc" data-rv data-d="1">
        Diagnóstico que vira estratégia. Estratégia que vira patrimônio. Estruturamos
        investimentos, sucessão e proteção com independência de instituição.
      </p>

      <div class="stage-cta" data-rv data-d="2">
        <a href="#diagnostico" class="btn btn-primary">
          Agende seu diagnóstico
          <svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 7H12M8 3L12 7L8 11"/></svg>
        </a>
        <a href="#solucoes" class="stage-link">Como atuamos</a>
      </div>
    </div>

    <!-- ---------------- o palco ---------------- -->
    <div class="stage-media" data-rv data-d="1">
      <div class="stage-view">
        <div class="stage-track" data-stage-track>

          <!-- SLIDE 1 — narrativa em três passos.
               Os três cards ocupam quase o mesmo lugar de propósito: é uma
               conversa, um passo substituindo o anterior. -->
          <div class="stage-slide" data-stage-slide>
            <img class="stage-shot" src="../../../showroom/demo/ph-wide-2.webp" alt="Consultor revisando a carteira com o cliente">
            <div class="stage-cards">
              <div class="stage-card stage-card-note" data-stage-step="1" style="--stage-cx:4%;--stage-cy:50%">
                <span class="stage-card-icon" aria-hidden="true">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
                </span>
                <span class="stage-card-line">Quero organizar a sucessão da holding da família</span>
              </div>

              <div class="stage-card stage-card-note" data-stage-step="2" style="--stage-cx:4%;--stage-cy:50%">
                <span class="stage-card-icon" aria-hidden="true">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.8 2.8M15.2 15.2L18 18M18 6l-2.8 2.8M8.8 15.2L6 18"/></svg>
                </span>
                <span class="stage-card-line">Montando seu diagnóstico&hellip;</span>
              </div>

              <div class="stage-card stage-card-note" data-stage-step="3" style="--stage-cx:4%;--stage-cy:50%">
                <span class="stage-card-icon" aria-hidden="true">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12.5L9.5 18L20 6.5"/></svg>
                </span>
                <span class="stage-card-line">Pronto: sua estrutura sucessória mapeada em 6 etapas</span>
              </div>
            </div>
          </div>

          <!-- SLIDE 2 — cards permanentes, sem data-stage-step.
               Três objetos em alturas diferentes: o olho circula em vez de
               varrer. Em linha reta viram uma legenda comprida. -->
          <div class="stage-slide" data-stage-slide>
            <img class="stage-shot" src="../../../showroom/demo/ph-wide-4.webp" alt="Cliente acompanhando a carteira consolidada">
            <div class="stage-cards">
              <div class="stage-card stage-card-item" style="--stage-cx:10%;--stage-cy:10%">
                <span class="stage-card-thumb" aria-hidden="true">
                  <img src="../../../showroom/demo/ph-8.webp" alt="">
                </span>
                <span class="stage-card-text">
                  <b class="stage-card-title">Fundo de crédito privado — CDI + 3,2%</b>
                  <span class="stage-card-value">R$ 1,2 mi</span>
                </span>
              </div>

              <div class="stage-card stage-card-stat" style="--stage-cx:36%;--stage-cy:54%">
                <span class="stage-card-cell">
                  <span class="stage-card-label">Patrimônio sob gestão</span>
                  <b class="stage-card-figure">R$ 18,4 mi</b>
                </span>
                <span class="stage-card-cell">
                  <span class="stage-card-label">Rentabilidade 12m</span>
                  <b class="stage-card-figure">14,6%</b>
                </span>
              </div>
            </div>
          </div>

          <!-- SLIDE 3 -->
          <div class="stage-slide" data-stage-slide>
            <img class="stage-shot" src="../../../showroom/demo/ph-wide-1.webp" alt="Comitê de investimentos em reunião">
            <div class="stage-cards">
              <div class="stage-card stage-card-stat" style="--stage-cx:8%;--stage-cy:16%">
                <span class="stage-card-cell">
                  <span class="stage-card-label">Isenção de IR</span>
                  <b class="stage-card-figure">R$ 340 mil</b>
                </span>
                <span class="stage-card-cell">
                  <span class="stage-card-label">Custo evitado</span>
                  <b class="stage-card-figure">R$ 1,1 mi</b>
                </span>
              </div>

              <div class="stage-card stage-card-note" data-stage-step="1" style="--stage-cx:22%;--stage-cy:62%">
                <span class="stage-card-icon" aria-hidden="true">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 18V9M10 18V4M16 18v-6M22 18H2"/></svg>
                </span>
                <span class="stage-card-line">Relatório consolidado fechado agora</span>
              </div>

              <div class="stage-card stage-card-note" data-stage-step="2" style="--stage-cx:22%;--stage-cy:62%">
                <span class="stage-card-icon" aria-hidden="true">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l2.6 5.6L21 9.4l-4.6 4.3L17.6 20 12 17l-5.6 3 1.2-6.3L3 9.4l6.4-.8z"/></svg>
                </span>
                <span class="stage-card-line">Rebalanceamento do trimestre aprovado</span>
              </div>
            </div>
          </div>

        </div>
      </div>

      <!-- As setas moram dentro do palco, sobre a foto, como na referência.
           São <button> de verdade: o carrossel tem de virar por teclado. -->
      <div class="stage-nav">
        <button type="button" class="stage-arrow" data-stage-prev aria-label="Slide anterior">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>
        </button>
        <button type="button" class="stage-arrow" data-stage-next aria-label="Próximo slide">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>
        </button>
      </div>
    </div>

  </div>
</section>

estilo.css · 309 linhas · 11,5 KB

/* ==========================================================================
   HERO DE DUAS COLUNAS COM PALCO DE PRODUTO
   Prefixo: stage-
   Tokens usados: --bg --fg --fg-2 --fg-muted --rule --accent --dark --dark-rgb
                  --light --display --sans --nav-h --pad --dur --ease
                  --radius-pill --radius-media
   ========================================================================== */

.stage{overflow:hidden}

/* ---- a grade ----
   A coluna de texto é mais larga que o palco, e o padding-top é maior que o
   necessário para limpar o menu. Os dois têm o mesmo motivo, explicado
   abaixo: é a legenda de cursor que precisa de lugar.

   O padding-top é o dobro do que a dobra precisaria para limpar o menu, e é
   deliberado: o vão entre o menu e a headline é onde a legenda de cursor
   pousa. Com o respiro mínimo a bolha cai sobre a primeira linha do título —
   medido, não suposto. Quem usar o hero sem a camada de anotação pode baixar
   o clamp; quem usar com ela, não. */
.stage-grid{
  display:grid;
  /* 1,25 / 1 e não 1 / 1. Medida na referência: coluna de texto 770 contra
     foto 608, ou seja o texto é ~27% mais largo. Não é gosto — é o que dá
     lugar, à direita dos botões, para a legenda de cursor pousar sem cobrir
     o link secundário. Em 1 / 1 a bolha não cabe ali. */
  grid-template-columns:1.25fr 1fr;
  align-items:center;
  gap:clamp(32px,4vw,64px);
  padding-top:calc(var(--nav-h) + clamp(56px,10vh,120px));
  padding-bottom:clamp(56px,9vh,110px);
}

/* ---- headline ----
   Linhas encostadas são a assinatura desta abertura: o bloco vira massa. A
   referência faz isso em caixa alta com entrelinha 1; aqui é caixa baixa,
   porque na biblioteca caixa alta é reservada a texto que é efeito. Em caixa
   baixa 1 não fecha — o "g" de uma linha encosta na seguinte — então a
   entrelinha subiu para 1.06, que é o mínimo que separa sem soltar o bloco. */
.stage-title{
  font-family:var(--display);
  font-weight:700;
  font-size:clamp(34px,4.6vw,68px);
  line-height:1.06;
  letter-spacing:-.022em;
  color:var(--fg);
}
.stage-title em{font-style:normal;font-weight:700;color:var(--accent)}

.stage-desc{
  font-family:var(--sans);
  font-size:16px;
  line-height:1.6;
  color:var(--fg-2);
  max-width:46ch;
  margin-top:clamp(20px,2.4vw,30px);
}

/* ---- ações ----
   O segundo caminho é link de texto, não segundo botão: dois botões lado a
   lado disputam o clique e nenhum dos dois ganha. */
.stage-cta{
  display:flex;align-items:center;
  gap:clamp(16px,2vw,30px);
  flex-wrap:wrap;
  margin-top:clamp(26px,3.2vw,40px);
}
.stage-link{
  font-family:var(--sans);font-weight:600;font-size:14px;
  color:var(--fg);
  border-bottom:1px solid transparent;
  padding-bottom:2px;
  transition:color .3s var(--ease),border-color .3s var(--ease);
}
.stage-link:hover{color:var(--accent);border-bottom-color:var(--accent)}

/* ==========================================================================
   O PALCO
   ========================================================================== */

.stage-media{
  --stage-ratio:1.09;      /* 608x560 da referência. 4/3 e 1/1 também servem */
  --stage-radius:24px;
  position:relative;
}

/* A janela recorta o trilho. O raio vive aqui, não na foto: assim o card que
   encosta na borda também é recortado, e não fica um canto vazando. */
.stage-view{
  position:relative;
  aspect-ratio:var(--stage-ratio);
  overflow:hidden;
  border-radius:var(--stage-radius);
  background:var(--dark-2);   /* o que se vê enquanto a foto não chega */
}

/* O trilho anda em múltiplos de -100%. Quem escreve --stage-i é o script;
   a conta é daqui, e é por isso que mudar a curva não exige tocar no JS. */
.stage-track{
  display:flex;
  height:100%;
  transform:translate3d(calc(var(--stage-i,0) * -100%),0,0);
  transition:transform .72s var(--ease);
  will-change:transform;
}
@media(prefers-reduced-motion:reduce){
  .stage-track{transition:none}
}

.stage-slide{position:relative;flex:0 0 100%;height:100%}
.stage-shot{width:100%;height:100%;object-fit:cover;object-position:50% 42%}

/* ==========================================================================
   OS CARDS FLUTUANTES
   ========================================================================== */

.stage-cards{position:absolute;inset:0;pointer-events:none}

/* Posição em porcentagem da foto, escrita inline no markup. Porcentagem e
   não pixel porque o palco encolhe com a coluna: em pixel o card sai da
   foto na primeira tela estreita.

   A boiada usa `translate`, não `transform`, e isso é o que permite os dois
   movimentos coexistirem: `translate` e `transform` são propriedades
   distintas e se compõem, então a entrada do passo (transform) não briga com
   a flutuação contínua (translate). Com os dois em `transform` um apagaria
   o outro, e a saída seria um invólucro a mais só para separá-los. */
.stage-card{
  --stage-card-radius:14px;
  --stage-bob:11px;         /* amplitude medida na referência */
  --stage-bob-dur:6s;
  --stage-bob-delay:0s;

  position:absolute;
  left:var(--stage-cx,6%);
  top:var(--stage-cy,50%);
  display:flex;align-items:center;gap:12px;
  max-width:64%;
  padding:12px;
  background:var(--light);
  border-radius:var(--stage-card-radius);
  box-shadow:0 16px 38px rgba(var(--dark-rgb),.17);
  animation:stageBob var(--stage-bob-dur) var(--ease) var(--stage-bob-delay) infinite alternate;
}

/* Amplitudes e tempos ligeiramente diferentes por posição: em fase igual os
   três cards sobem e descem juntos e a foto inteira parece respirar, o que
   entrega o truque. Dessincronizar é o que faz parecerem objetos soltos. */
.stage-card:nth-child(2){--stage-bob:8px;--stage-bob-dur:7.2s;--stage-bob-delay:-2.4s}
.stage-card:nth-child(3){--stage-bob:13px;--stage-bob-dur:5.4s;--stage-bob-delay:-1.1s}

@keyframes stageBob{
  from{translate:0 0}
  to{translate:0 calc(var(--stage-bob) * -1)}
}

/* ---- os passos ----
   Card com data-stage-step nasce fora e entra quando é a vez dele. A entrada
   carrega blur indo a zero, como o §5 manda para toda entrada — é o mesmo
   desenho do [data-rv] do core, só que disparado por classe em laço em vez
   de por IntersectionObserver uma única vez. */
.stage-card[data-stage-step]{
  opacity:0;
  filter:blur(10px);
  transform:translateY(16px);
  visibility:hidden;
  transition:opacity .6s var(--ease),filter .7s var(--ease),
             transform .6s var(--ease),visibility .6s;
}
.stage-card[data-stage-step].is-on{
  opacity:1;filter:blur(0);transform:none;visibility:visible;
}
@media(prefers-reduced-motion:reduce){
  .stage-card{animation:none}
  .stage-card[data-stage-step]{transition:opacity .3s linear,visibility .3s}
  .stage-card[data-stage-step].is-on{filter:none;transform:none}
}
/* SEM JAVASCRIPT, A NARRATIVA MOSTRA O DESFECHO. Os passos nascem fora de cena
   e quem os traz é o laço do script; sem ele, o palco ficava só com a foto e
   a conversa inteira sumia — "Pronto: sua estrutura sucessória mapeada" era
   justamente a frase que fecha o argumento. O último passo de cada slide é
   sempre o último filho da .stage-cards (os passos dividem a mesma posição e
   se substituem), então é ele que aparece. Achado pelo bin/varredura.js. */
@media (scripting: none){
  .stage-card[data-stage-step]:last-child{
    opacity:1;filter:none;transform:none;visibility:visible;
  }
}

/* ---- variante: nota de conversa ----
   Pílula com ícone em círculo. É o card da narrativa. */
.stage-card-note{border-radius:var(--radius-pill);padding:10px 20px 10px 10px}
.stage-card-icon{
  flex:0 0 auto;
  width:40px;height:40px;
  display:grid;place-items:center;
  border-radius:var(--radius-pill);
  background:color-mix(in srgb,var(--accent) 16%,var(--light));
  color:var(--accent);
}
.stage-card-icon svg{width:19px;height:19px;display:block}
.stage-card-line{
  font-family:var(--sans);font-weight:600;font-size:13px;line-height:1.35;
  color:var(--dark);
}

/* ---- variante: item de catálogo ---- */
.stage-card-thumb{
  flex:0 0 auto;
  width:46px;height:46px;
  overflow:hidden;
  border-radius:10px;
  background:color-mix(in srgb,var(--accent) 14%,var(--light));
}
.stage-card-thumb img{width:100%;height:100%;object-fit:cover}
.stage-card-text{display:flex;flex-direction:column;gap:3px;min-width:0}
.stage-card-title{
  font-family:var(--sans);font-weight:600;font-size:12.5px;line-height:1.3;
  color:var(--dark);
}
.stage-card-value{
  font-family:var(--display);font-weight:700;font-size:15px;letter-spacing:-.01em;
  color:var(--accent);
}

/* ---- variante: par de números ----
   O filete entre as células é o que separa duas grandezas sem virar tabela. */
.stage-card-stat{gap:0;padding:14px 4px}
.stage-card-cell{
  display:flex;flex-direction:column;gap:4px;
  padding-inline:16px;
}
.stage-card-cell + .stage-card-cell{border-left:1px solid var(--rule)}
.stage-card-label{
  font-family:var(--sans);font-weight:500;font-size:11px;line-height:1.3;
  color:var(--fg-muted);
}
.stage-card-figure{
  font-family:var(--display);font-weight:700;font-size:16px;letter-spacing:-.015em;
  white-space:nowrap;
  color:var(--accent);
}

/* ==========================================================================
   AS SETAS
   ========================================================================== */

.stage-nav{
  position:absolute;
  left:50%;bottom:clamp(14px,2.4vw,22px);
  transform:translateX(-50%);
  display:flex;gap:12px;
  z-index:2;
}
.stage-arrow{
  width:44px;height:44px;
  display:grid;place-items:center;
  border:1px solid color-mix(in srgb,var(--light) 45%,transparent);
  border-radius:var(--radius-pill);
  background:color-mix(in srgb,var(--light) 80%,transparent);
  color:var(--dark);
  padding:0;
  transition:background .3s var(--ease);
}
.stage-arrow svg{width:20px;height:20px;display:block}
/* Sem salto no hover (CLAUDE.md §6, 2026-09-29): o disco acende e não
   cresce, a mesma regra do .btn do core — botão não salta nem move. */
.stage-arrow:hover{background:var(--light)}
@supports (backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px)){
  .stage-arrow{
    backdrop-filter:blur(10px) saturate(140%);
    -webkit-backdrop-filter:blur(10px) saturate(140%);
  }
}

/* ==========================================================================
   MOBILE
   Uma coluna, palco embaixo. O palco vira 4/3 porque em largura cheia o
   1.09 fica alto demais e empurra o CTA para fora da primeira tela — e o
   CTA acima da dobra é o motivo da dobra existir.
   Abaixo de 560px os cards encolhem e o terceiro sai: três objetos numa
   foto de 320px de largura viram uma pilha ilegível.
   ========================================================================== */
@media(max-width:980px){
  .stage-grid{
    grid-template-columns:1fr;
    gap:clamp(30px,5vh,46px);
    padding-top:calc(var(--nav-h) + clamp(20px,4vh,44px));
  }
  .stage-media{--stage-ratio:4/3}
  .stage-desc{max-width:60ch}
}

@media(max-width:560px){
  .stage-media{--stage-ratio:1}
  .stage-card{max-width:80%;padding:9px}
  .stage-card-note{padding:8px 15px 8px 8px}
  .stage-card-icon{width:32px;height:32px}
  .stage-card-icon svg{width:15px;height:15px}
  .stage-card-line,.stage-card-title{font-size:11.5px}
  .stage-card-figure{font-size:14px}
  .stage-card-cell{padding-inline:11px}
  /* Só card PERMANENTE sai. Esconder um passo da narrativa o apagaria para
     sempre — o laço continuaria dando a vez a um card invisível, e o slide
     ficaria vazio um terço do tempo. */
  .stage-card:nth-child(3):not([data-stage-step]){display:none}
  .stage-arrow{width:38px;height:38px}
  .stage-arrow svg{width:17px;height:17px}
}

script.js · 154 linhas · 5,8 KB

/* ==========================================================================
   HERO DE DUAS COLUNAS COM PALCO DE PRODUTO

   Duas máquinas independentes, e a separação é de propósito:

     1. o CARROSSEL — manual, pelas setas ou pelas flechas do teclado.
        O script só escreve --stage-i no trilho; a conta de translação e a
        curva moram no estilo.css.

     2. a NARRATIVA — em cada slide, os cards com data-stage-step entram um
        por vez, em laço. Roda só no slide visível: os outros não gastam
        relógio, e ao virar o slide a sequência recomeça do passo 1, que é o
        que faz a conversa ler como conversa e não como "peguei no meio".

   Nenhum `style.transform` sai daqui. Card permanente (sem data-stage-step)
   o script não toca — ele é CSS puro do começo ao fim.
   ========================================================================== */

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

  var PASSO = 3400;   /* tempo de cada passo da narrativa, em ms */

  /* Flag do core (contrato §7.5). O plano B existe para o componente rodar
     colado num Elementor onde o motion.js não entrou. */
  function reduzido() {
    if (window.LIB && typeof LIB.reduce === 'boolean') return LIB.reduce;
    return matchMedia('(prefers-reduced-motion: reduce)').matches &&
           location.search.indexOf('motion=on') < 0;
  }

  function iniciar(raiz) {
    var trilho = raiz.querySelector('[data-stage-track]');
    var slides = raiz.querySelectorAll('[data-stage-slide]');
    if (!trilho || !slides.length) return;

    var btnPrev = raiz.querySelector('[data-stage-prev]');
    var btnNext = raiz.querySelector('[data-stage-next]');

    var atual = 0;
    var relogio = null;
    var visivel = true;

    /* ------------------------------------------------------- os passos
       Um array por slide, ordenado pelo NÚMERO do data-stage-step e não
       pela ordem do markup: assim dá para reordenar a conversa mexendo só
       no número, sem recortar e colar blocos de HTML. */
    var roteiros = [];
    for (var s = 0; s < slides.length; s++) {
      var passos = [];
      var cards = slides[s].querySelectorAll('[data-stage-step]');
      for (var c = 0; c < cards.length; c++) passos.push(cards[c]);
      passos.sort(function (a, b) {
        return (parseInt(a.getAttribute('data-stage-step'), 10) || 0) -
               (parseInt(b.getAttribute('data-stage-step'), 10) || 0);
      });
      roteiros.push(passos);
    }

    function apagarPassos(i) {
      var passos = roteiros[i];
      for (var p = 0; p < passos.length; p++) passos[p].classList.remove('is-on');
    }

    function acenderPasso(i, n) {
      var passos = roteiros[i];
      for (var p = 0; p < passos.length; p++) {
        if (p === n) passos[p].classList.add('is-on');
        else passos[p].classList.remove('is-on');
      }
    }

    function pararRoteiro() {
      if (relogio) { clearInterval(relogio); relogio = null; }
    }

    /* O primeiro passo acende na hora; os seguintes entram no compasso.
       Com movimento reduzido a narrativa congela no passo 1 — mostrar o
       conteúdo parado é melhor que esconder dois terços dele. */
    function rodarRoteiro() {
      pararRoteiro();
      var passos = roteiros[atual];
      if (!passos.length) return;

      var n = 0;
      acenderPasso(atual, 0);
      if (passos.length < 2 || reduzido() || !visivel) return;

      relogio = setInterval(function () {
        n = (n + 1) % passos.length;
        acenderPasso(atual, n);
      }, PASSO);
    }

    /* ----------------------------------------------------- o carrossel */
    function ir(i, mover) {
      /* Laço nos dois sentidos: da última volta para a primeira. Carrossel
         que trava na ponta faz o visitante clicar duas vezes para descobrir
         que acabou. */
      var total = slides.length;
      var alvo = ((i % total) + total) % total;

      if (alvo !== atual) {
        apagarPassos(atual);
        atual = alvo;
      }

      trilho.style.setProperty('--stage-i', atual);

      /* Slide fora de vista sai da árvore de acessibilidade: sem isto o
         leitor de tela e o Tab passeiam pelos três slides como se os três
         estivessem na tela. */
      for (var s = 0; s < total; s++) {
        if (s === atual) slides[s].removeAttribute('aria-hidden');
        else slides[s].setAttribute('aria-hidden', 'true');
      }

      rodarRoteiro();

      /* Devolve o foco ao botão que virou o slide. Sem `mover` a chamada é
         a inicialização, e aí não há foco a mexer. */
      if (mover && document.activeElement === document.body) {
        (mover === 'next' ? btnNext : btnPrev).focus();
      }
    }

    if (btnPrev) btnPrev.addEventListener('click', function () { ir(atual - 1, 'prev'); });
    if (btnNext) btnNext.addEventListener('click', function () { ir(atual + 1, 'next'); });

    /* Flechas do teclado, mas só quando o foco está dentro do palco: capturar
       ArrowLeft na página inteira roubaria a navegação de quem está lendo. */
    raiz.addEventListener('keydown', function (e) {
      if (e.key === 'ArrowLeft')  { e.preventDefault(); ir(atual - 1); }
      if (e.key === 'ArrowRight') { e.preventDefault(); ir(atual + 1); }
    });

    /* ------------------------------------------- economia fora da tela
       A dobra é a primeira da página: assim que o visitante rola, a
       narrativa para de trocar de card para ninguém. */
    if (window.IntersectionObserver) {
      var io = new IntersectionObserver(function (es) {
        visivel = es[0].isIntersecting;
        if (visivel) rodarRoteiro();
        else pararRoteiro();
      }, { threshold: 0 });
      io.observe(raiz);
    }

    ir(0);
  }

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

Texto à esquerda, palco à direita. O palco é um trilho de fotos que vira pelas setas, e sobre a foto flutuam cards de interface posicionados em porcentagem: um item de catálogo, um par de números, uma nota de conversa. Os cards boiam de leve, sem parar, e podem entrar em sequência — contando uma narrativa de três passos em laço.

O problema editorial é o de toda abertura de produto: mostrar o software sem dar print da tela. Print envelhece, não cabe em 600px e obriga o visitante a ler tipografia de 11px. Os cards flutuantes recortam só o que importa — um preço, uma conversão, uma frase de confirmação — e a foto de pessoa por baixo dá a escala humana que um print nunca dá. A headline em caixa alta com line-height:1 é o contrapeso: massa tipográfica de um lado, objetos leves do outro.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Headline 8 a 12 palavras, caixa alta. A última frase vai no <em> e é o que a marca vende, não um adjetivo
Apoio duas a três frases curtas. O padrão da referência é bom: duas afirmações em paralelo, depois uma de ação
Ação primária verbo no infinitivo ou imperativo, três palavras no máximo
Ação secundária link de texto, nunca segundo botão — dois botões lado a lado disputam o clique e nenhum ganha
Slide um momento do serviço por slide — o diagnóstico, o relatório consolidado, o comitê. Dois slides que mostram a mesma coisa são um slide
Card 2 ou 3 por slide. Quatro cobre a foto e o rosto que dava a escala humana desaparece
Nota de conversa uma frase, 40 a 50 caracteres. É fala, então aceita reticências e exclamação
Número grandeza com unidade. R$ 18,4 mi lê como relatório; 18400000 lê como placeholder. Ilustrativo, sempre — número em hero de assessoria não é dado apurado

Os cards de um slide vão em alturas diferentes, nunca alinhados. Três objetos em linha reta viram uma legenda comprida; em alturas distintas o olho circula pela foto. E as posições em % são de tuning fino: mova de dois em dois pontos, olhando o rosto da foto — o card não pode cobrir olhos nem boca.

Especificação dos assets


Como usar

<link rel="stylesheet" href="componentes/hero/duas-colunas-produto/estilo.css">
<script src="componentes/hero/duas-colunas-produto/script.js"></script>

O card é posicionado inline, porque a posição é conteúdo — muda a cada foto:

<div class="stage-card stage-card-stat" style="--stage-cx:36%;--stage-cy:54%"> … </div>

E a diferença entre card permanente e card de narrativa é um atributo:

<div class="stage-card stage-card-item" style="…">                       <!-- sempre visível -->
<div class="stage-card stage-card-note" data-stage-step="1" style="…">   <!-- entra na vez -->

Ganchos

Gancho Papel
[data-stage] a seção. Uma por página, mas o script aguenta várias
[data-stage-track] o trilho. Recebe --stage-i do script
[data-stage-slide] o slide. Ganha aria-hidden quando sai de vista
[data-stage-prev] [data-stage-next] as setas. <button> de verdade
data-stage-step="n" ordena a narrativa pelo número, não pela ordem do markup
--stage-cx --stage-cy posição do card em % da foto. Inline
--stage-ratio proporção do palco. Padrão 1.09; 4/3 em tablet, 1 em celular
--stage-radius raio da janela. Padrão 24px
--stage-bob --stage-bob-dur --stage-bob-delay amplitude, duração e fase da boiada
.stage-card-note -item -stat os três tipos de card: fala do cliente, posição em um ativo, par de grandezas

Variações


Gotchas


Histórico

Projeto Onde Data Diferenças
referência: Corvex https://usecorvex.com.br/ — hero 2026-08-31 referência visual, não origem de código

Normalizações aplicadas na tradução: a referência é Framer — classes hash, sem licença declarada, e nada foi copiado (§11). Medido nela e reproduzido: foto 608×560 com raio 24px (virou --stage-ratio:1.09 + --stage-radius), headline 72px/72px com tracking −0.02em em caixa alta e a frase final em cor de marca, corpo 16/25.6, boiada de ~11px de amplitude, narrativa de três passos em ciclo de ~10s, carrossel manual pelas setas ao pé da foto.

O que é nosso: Anek Latin e Instrument Sans saíram (Google Fonts, proibido em produção pelo §1) e viraram --display/--sans, com o tema decidindo; os hex #F5F6FC, #8758F0 e #33196F viraram .sec-light, --accent e color-mix; a headline entra por data-split do core, com blur, em vez do fade simples do original; o conteúdo dos cards deixou de ser UI da Corvex e virou um contrato de slot em três tipos (-note, -item, -stat); os cards ganharam fases de boiada dessincronizadas (na referência os três sobem quase juntos, e isso entrega o truque); as setas viraram <button> com rótulo, navegação por flechas do teclado e aria-hidden nos slides fora de vista — na referência são <div> clicáveis; e tanto o carrossel quanto a narrativa param quando a dobra sai da tela.

Divergências conhecidas vs. a referência: três slides aqui contra quatro lá — contagem é conteúdo, não desenho. O deslize do trilho é --ease em 0,72s, não a mola do Motion. E a referência troca o texto das legendas de cursor quando o slide vira; aqui as notas são estáticas em conteúdo, porque a camada é um componente separado que não sabe nada sobre o palco. Ligar as duas coisas exigiria um gancho entre componentes — dívida assumida e registrada, não esquecimento.