← Biblioteca componentes/conteudo/vitrine-destaque/ficha.md CódigoTela cheia
conteudo/vitrine-destaque

Vitrine com destaque comutável

🟢 estavel complexidade média prefixo vit- abas tabs vitrine destaque frentes servicos texto-na-foto big-number compliance acessibilidade apg sem-js teclado selo

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

<link rel="stylesheet" href="componentes/conteudo/vitrine-destaque/estilo.css">
<script src="componentes/conteudo/vitrine-destaque/script.js"></script>

markup.html · 452 linhas · 25,1 KB

<!-- ==========================================================================
     VITRINE COM DESTAQUE COMUTÁVEL — um card grande em cima, a escolha embaixo
     Prefixo: vit-
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css + script.js
     Compõe: efeitos/selo-circular (o selo do canto — o CSS dele vem de lá,
             declarado em preview-css.txt; nada aqui redesenha o selo)
     Sem lib. A troca são ~150 linhas de vanilla, e a maior parte delas é o
     teclado que o padrão de abas exige.

     A DOBRA. Duas camadas empilhadas:

       em cima  o DESTAQUE — um card largo com foto sangrada à direita, a
                categoria e o título escritos DENTRO da imagem, e à esquerda
                um número grande com o rótulo do que ele mede, a ressalva, uma
                frase e três atributos com ícone de traço;
       embaixo  a FILEIRA — quatro escolhas (número, categoria, nome e frase
                curta). Clicar numa delas troca o destaque.

     POR QUE ESTA FORMA. Quatro frentes de assessoria não cabem lado a lado
     sem virar quatro cartõezinhos iguais, em que nenhuma delas tem foto,
     número nem espaço para uma frase inteira. A vitrine dá a UMA delas o
     tamanho de uma dobra e deixa as outras três à vista como escolha. O
     irmão em colunas é o conversao/planos-honorarios, e a ficha diz quando
     cada um ganha: coluna serve para COMPARAR (as três de uma vez, na
     horizontal); vitrine serve para APROFUNDAR uma de cada vez.

     ============================ AS DUAS ROTAS =============================

     COM JAVASCRIPT — abas de verdade, no padrão APG. A fileira é o
     role="tablist", cada escolha é um role="tab" com aria-selected, cada
     destaque é um role="tabpanel". Setas ←/→ andam e trocam, Home/End vão às
     pontas, e o tabindex é roving: só a aba acesa fica na ordem de tabulação,
     as outras saem dela (é isso que faz um Tab atravessar a fileira inteira
     em vez de parar quatro vezes). O conteúdo troca INSTANTÂNEO; só a foto
     funde, em 280ms.

     SEM JAVASCRIPT — a peça continua inteira. Os quatro destaques estão no
     documento, empilhados e legíveis, cada um com o seu título dentro da
     foto; a fileira é feita de <a href="#id"> de verdade, que levam a cada
     um. Nada aqui é revelado por script: o script ESCONDE o que sobra, nunca
     o contrário (mesmo critério do conversao/planos-honorarios).

     O PREÇO DA ROTA SEM SCRIPT, declarado: os papéis de aba estão no markup,
     e sem script eles descrevem a INTENÇÃO, não o estado — a fileira se
     anuncia como abas enquanto os quatro painéis estão todos visíveis. A
     alternativa era escrever role/aria por script e deixar o markup mudo
     sobre a própria estrutura. O que NÃO está no markup, de propósito, é o
     `tabindex`: roving tabindex escrito à mão aqui tiraria três das quatro
     escolhas da ordem de tabulação para quem está sem script, e aí a rota
     sem JavaScript quebraria de verdade — não em teoria. Quem põe (e tira) o
     tabindex é o script, junto com o tabindex="0" do painel, que o APG pede
     para painel sem nada focável dentro.

     ============================ O NÚMERO =================================

     O NÚMERO GRANDE É O PONTO DE COMPLIANCE DESTA PEÇA, e a regra é dura:
     ele NÃO pode ser rentabilidade, retorno, "+52%", "1,8x o CDI" nem nada
     que insinue desempenho — com número ou com palavra. O que ele mede é
     ESTRUTURA DO TRABALHO: prazo, frequência de revisão, número de comitês,
     gerações cobertas pelo desenho, jurisdições no escopo, anos de casa,
     famílias atendidas. Os quatro exemplos aqui são inofensivos por
     construção: 90 dias, 3 gerações, 4 comitês por ano, 2 jurisdições.
     Nenhum deles vira promessa nem se lê como resultado.

     E cada um carrega a ressalva presa a ele: <p class="nota vit-nota">, a
     primitiva do core, logo abaixo do rótulo e separada por um filete. A
     ligação é de ORDEM DE LEITURA e de desenho, e não de aria-describedby:
     descrição em elemento que não é focável nem tem nome acessível é
     anunciada de forma inconsistente entre leitores de tela, enquanto a
     ordem do documento é lida por todos.

     ======================== TEXTO SOBRE IMAGEM ============================

     Categoria e título vivem DENTRO da foto (contrato §6), sobre a pilha da
     casa: img -> scrim em degradê -> véu desfocado COM máscara em gradiente.
     Sem a máscara o backdrop-filter vira tarja de borda dura.

     Eles são ancorados EM CIMA, e não embaixo como no midia/grade-cases-foto.
     Não é capricho: aqui a metade de baixo da foto é onde o selo mora e para
     onde a vista desce rumo à fileira de escolhas, e o card não é um card de
     galeria — é uma dobra com duas colunas, em que o texto grande já está à
     esquerda. Título no alto da foto e número no alto da coluna de texto
     deixam as duas colunas começando na mesma linha. O véu acompanhou: a
     máscara vai do topo para baixo, o contrário da receita padrão.

     ========================== O SELO, COMPOSTO ============================

     O selo do canto é o efeitos/selo-circular COMPOSTO, não recopiado: o
     desenho e a volta vivem no estilo.css dele. O id do arco é vit-selo-arco
     e não o selo-arco de origem, porque dois selos na mesma página com o
     mesmo id fazem os dois textos seguirem o PRIMEIRO arco. O data-rv fica no
     invólucro .vit-palco e nunca no .selo, porque o .selo gerencia o próprio
     transform (o giro) e [data-rv].in{transform:none} vale (0,2,0) — venceria
     por especificidade e congelaria a volta depois da entrada (contrato §5).

     A seta do miolo aponta para baixo de propósito: a fileira de escolhas
     está logo abaixo. Ela é decoração e está marcada como tal.

     ======================= OS ÍCONES, DESENHADOS AQUI =====================

     Os três atributos de cada destaque levam ícone de TRAÇO desenhado à mão
     em SVG inline: traço simples, currentColor, aria-hidden. Nenhum ícone de
     terceiro atravessa a biblioteca (contrato §11) — a referência desta dobra
     usa uma família proprietária, e ela ficou lá.

     ================== O PAINEL QUE NÃO ESTÁ ESCOLHIDO =====================

     Ele sai por visibility:hidden, e não por [hidden]. A razão é medida e
     está inteira no estilo.css: com display:none a célula da grade tem a
     altura do painel VISÍVEL, os quatro não têm a mesma altura natural
     (537px contra 517px a 1440), e a fileira de escolhas subia e descia
     debaixo do cursor de quem acabou de clicar. visibility tira o painel da
     árvore de acessibilidade E da ordem de foco — o que opacity:0 não faz.

     ==================== AS QUATRO FOTOS EM eager ==========================

     Nenhuma das quatro leva loading="lazy", e isso não é descuido: imagem
     lazy dentro de conteúdo escondido não é buscada — ela só começaria a
     baixar no instante da troca, e a fusão atravessaria para um retângulo
     vazio. Medido aqui antes de virar isto: com lazy, três das quatro fotos
     apareciam em "imgsSemPintar" no primeiro carregamento. O preço é
     honesto — quatro webp de uma dobra do meio da página, e é o mesmo que o
     hero/slideshow-cinematico pagou pela mesma razão. Com a troca para
     visibility o problema deixaria de existir sozinho; elas ficam eager
     assim mesmo, para a peça continuar de pé se alguém voltar a esconder
     painel por display.

     ======================== NADA ANDA SOZINHO =============================

     A vitrine NÃO gira sozinha, e isso é decisão e não falta: aba que troca
     no relógio rouba a leitura de quem está no meio de um parágrafo, e ainda
     obrigaria a peça a carregar o botão de pausa da WCAG 2.2.2. Quem troca é
     sempre a pessoa.
     ========================================================================== -->

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

    <span class="eyebrow" data-rv>Frentes de trabalho</span>
    <h2 class="sec-title ttl">Quatro frentes, e o <em>escopo</em> de cada uma</h2>
    <p class="sec-lead" data-rv data-d="1">Escolha uma frente na fileira de baixo. O que muda de uma para outra não é o acesso a um produto melhor — é o tipo de decisão que cada uma organiza, e o que a casa se compromete a entregar dentro dela.</p>

    <!-- Região viva: quem não vê a troca precisa ouvir que ela aconteceu. Fica
         FORA do palco, porque o palco é reescrito a cada troca. -->
    <p class="vit-anuncio visually-hidden" role="status" aria-live="polite"></p>

    <!-- O data-rv mora AQUI, no invólucro, e não nos painéis: painel que nasce
         [hidden] nunca intersecta, e ficaria em opacity 0 para sempre quando a
         aba o revelasse. Mesma armadilha medida no conversao/planos-honorarios. -->
    <div class="vit-palco" data-vit-palco data-rv data-d="2">

      <!-- ===================================================== destaque 01 -->
      <article class="vit-painel" id="vit-painel-1" data-vit-painel
               role="tabpanel" aria-labelledby="vit-aba-1">

        <div class="vit-info">
          <div class="vit-cifra">
            <span class="display-xl vit-num">90<span class="vit-num-un">dias</span></span>
            <span class="vit-num-rot">até a política de investimentos assinada, contados do primeiro diagnóstico</span>
            <p class="nota vit-nota">Prazo de referência do processo desta casa, não compromisso contratual. Não diz nada sobre resultado: é o tempo do trabalho, não do dinheiro.</p>
          </div>

          <p class="vit-frase">Antes de mexer em qualquer aplicação, a casa levanta o que já existe — contas, imóveis, participações, dívidas e o que entra todo mês — e escreve o que fazer com isso, em que ordem e em que prazo.</p>

          <ul class="vit-attrs" role="list">
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <path d="M3.5 6.6l5.5-2 6 2 5.5-2v12.8l-5.5 2-6-2-5.5 2z" fill="none"
                      stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
                <path d="M9 4.6v12.8M15 6.6v12.8" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linejoin="round"/>
              </svg>
              Inventário de tudo o que já existe
            </li>
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <path d="M5 19.2l.8-3.9L15.2 5.9a1.9 1.9 0 012.7 2.7L8.5 18.4z" fill="none"
                      stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
                <path d="M4 21.4h16" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linecap="round"/>
              </svg>
              Política de investimentos por escrito
            </li>
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <path d="M12 3.3l7 2.6v5.9c0 4.1-2.9 7.2-7 8.7-4.1-1.5-7-4.6-7-8.7V5.9z" fill="none"
                      stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
              </svg>
              Reserva de liquidez antes de qualquer risco
            </li>
          </ul>
        </div>

        <div class="vit-media">
          <img class="vit-foto" src="../../../showroom/demo/ph-wide-2.webp"
               alt="Homem de terno observando a cidade pela janela de um escritório alto"
               style="--vit-pos:44% 50%" decoding="async">
          <span class="vit-scrim" aria-hidden="true"></span>
          <div class="vit-cap">
            <span class="vit-cat">Planejamento patrimonial</span>
            <h3 class="vit-titulo">O mapa antes do primeiro aporte</h3>
          </div>
        </div>
      </article>

      <!-- ===================================================== destaque 02 -->
      <article class="vit-painel" id="vit-painel-2" data-vit-painel
               role="tabpanel" aria-labelledby="vit-aba-2">

        <div class="vit-info">
          <div class="vit-cifra">
            <span class="display-xl vit-num">3<span class="vit-num-un">gerações</span></span>
            <span class="vit-num-rot">o alcance do desenho: quem decide hoje, quem recebe e quem ainda vai receber</span>
            <p class="nota vit-nota">Refere-se ao horizonte do planejamento sucessório. Não é medida de patrimônio, de prazo nem de resultado.</p>
          </div>

          <p class="vit-frase">Sucessão não se resolve em cartório no dia seguinte a um enterro. Ela se desenha em vida, com a família na mesma mesa, e vira estrutura: quem administra, quem recebe, quando e com que condição.</p>

          <ul class="vit-attrs" role="list">
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <circle cx="9" cy="8.4" r="3.2" fill="none" stroke="currentColor" stroke-width="1.4"/>
                <path d="M3.4 20.2c0-3.1 2.5-5.6 5.6-5.6s5.6 2.5 5.6 5.6" fill="none"
                      stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
                <path d="M16.2 6.1a3.2 3.2 0 010 5.9M17.6 15.1c1.9.9 3 2.8 3 5.1" fill="none"
                      stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
              </svg>
              A família toda na mesma conversa
            </li>
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <path d="M6.4 3.4h8.2l3.4 3.4v13.8H6.4z" fill="none"
                      stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
                <path d="M14.6 3.4v3.4H18" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linejoin="round"/>
                <path d="M9.2 12h5.6M9.2 15.4h4" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linecap="round"/>
              </svg>
              Holding, doação em vida e testamento
            </li>
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <circle cx="8" cy="12" r="3.4" fill="none" stroke="currentColor" stroke-width="1.4"/>
                <path d="M11.4 12h9.1M17.6 12v3M20.5 12v2.2" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linecap="round"/>
              </svg>
              Quem decide o quê, definido antes
            </li>
          </ul>
        </div>

        <div class="vit-media">
          <img class="vit-foto" src="../../../showroom/demo/ph-wide-1.webp"
               alt="Casal caminhando por uma estrada de terra entre vinhedos ao entardecer"
               style="--vit-pos:58% 50%" decoding="async">
          <span class="vit-scrim" aria-hidden="true"></span>
          <div class="vit-cap">
            <span class="vit-cat">Sucessão familiar</span>
            <h3 class="vit-titulo">O que passa adiante, escrito em vida</h3>
          </div>
        </div>
      </article>

      <!-- ===================================================== destaque 03 -->
      <article class="vit-painel" id="vit-painel-3" data-vit-painel
               role="tabpanel" aria-labelledby="vit-aba-3">

        <div class="vit-info">
          <div class="vit-cifra">
            <span class="display-xl vit-num">4<span class="vit-num-un">comitês por ano</span></span>
            <span class="vit-num-rot">revisões formais da carteira, cada uma com ata e o motivo de cada decisão</span>
            <p class="nota vit-nota">Frequência de revisão contratada. Comitê é processo, não desempenho: a peça não afirma nada sobre o resultado da carteira.</p>
          </div>

          <p class="vit-frase">A carteira é desenhada a partir de um mandato escrito — o que pode entrar, em que proporção e até onde vai o risco aceito — e volta à mesa a cada trimestre para ser conferida contra ele.</p>

          <ul class="vit-attrs" role="list">
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <rect x="3.4" y="5.2" width="17.2" height="15.4" rx="1.6" fill="none"
                      stroke="currentColor" stroke-width="1.4"/>
                <path d="M8 3.2v4M16 3.2v4M3.4 10.2h17.2" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linecap="round"/>
              </svg>
              Comitê trimestral, com ata
            </li>
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <circle cx="12" cy="12" r="8.6" fill="none" stroke="currentColor" stroke-width="1.4"/>
                <path d="M15.4 8.6l-2.1 4.7-4.7 2.1 2.1-4.7z" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linejoin="round"/>
              </svg>
              Mandato escrito antes da primeira ordem
            </li>
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <path d="M12 4.2v15.6M5.6 8h12.8M5.4 19.8h13.2" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linecap="round"/>
                <path d="M8.4 8l-2.8 5.6h5.6zM15.6 8l-2.8 5.6h5.6z" fill="none"
                      stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
              </svg>
              Rebalanceamento por faixa, não por palpite
            </li>
          </ul>
        </div>

        <div class="vit-media">
          <img class="vit-foto" src="../../../showroom/demo/ph-wide-4.webp"
               alt="Sala de reunião com mesa comprida e vista para a cidade"
               style="--vit-pos:56% 50%" decoding="async">
          <span class="vit-scrim" aria-hidden="true"></span>
          <div class="vit-cap">
            <span class="vit-cat">Gestão de carteira</span>
            <h3 class="vit-titulo">Uma carteira que volta à mesa</h3>
          </div>
        </div>
      </article>

      <!-- ===================================================== destaque 04 -->
      <article class="vit-painel" id="vit-painel-4" data-vit-painel
               role="tabpanel" aria-labelledby="vit-aba-4">

        <div class="vit-info">
          <div class="vit-cifra">
            <span class="display-xl vit-num">2<span class="vit-num-un">jurisdições</span></span>
            <span class="vit-num-rot">Brasil e exterior lidos como um patrimônio só, e não como duas contas separadas</span>
            <p class="nota vit-nota">Número de jurisdições cobertas pelo escopo do trabalho. Não é recomendação de investir fora nem estimativa de retorno.</p>
          </div>

          <p class="vit-frase">Quem tem conta, imóvel ou participação fora do país tem dois calendários fiscais, duas moedas e duas regras de sucessão. A frente existe para que as duas pontas sejam decididas juntas.</p>

          <ul class="vit-attrs" role="list">
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <circle cx="12" cy="12" r="8.6" fill="none" stroke="currentColor" stroke-width="1.4"/>
                <path d="M3.4 12h17.2" fill="none" stroke="currentColor" stroke-width="1.4"/>
                <path d="M12 3.4c2.3 2.4 3.5 5.3 3.5 8.6s-1.2 6.2-3.5 8.6c-2.3-2.4-3.5-5.3-3.5-8.6S9.7 5.8 12 3.4z"
                      fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
              </svg>
              Conta e custódia fora, na estrutura certa
            </li>
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <path d="M6.4 3.4h8.2l3.4 3.4v13.8H6.4z" fill="none"
                      stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
                <path d="M14.6 3.4v3.4H18" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linejoin="round"/>
                <path d="M9 14.8l2 2 4-4.4" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
              </svg>
              Declaração de bens no exterior em dia
            </li>
            <li class="vit-attr">
              <svg class="vit-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <circle cx="12" cy="12" r="8.6" fill="none" stroke="currentColor" stroke-width="1.4"/>
                <path d="M12 6.8v5.5l3.6 2.1" fill="none" stroke="currentColor"
                      stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
              </svg>
              Câmbio planejado, não decidido no susto
            </li>
          </ul>
        </div>

        <div class="vit-media">
          <img class="vit-foto" src="../../../showroom/demo/ph-wide-3.webp"
               alt="Veleiro solitário em mar aberto sob céu carregado"
               style="--vit-pos:58% 50%" decoding="async">
          <span class="vit-scrim" aria-hidden="true"></span>
          <div class="vit-cap">
            <span class="vit-cat">Patrimônio internacional</span>
            <h3 class="vit-titulo">Dois países na mesma leitura</h3>
          </div>
        </div>
      </article>

      <!-- O selo composto. Fora dos painéis de propósito: ele é assinatura da
           dobra, não conteúdo de uma frente — se vivesse dentro de um painel,
           sumiria na primeira troca. O texto tem de caber na volta: o arco
           gasta ~4,38 unidades por caractere em 232,5 disponíveis, ou seja
           ~53 caracteres contando os separadores. A frase abaixo, duplicada
           para fechar o laço, gasta 42. -->
      <span class="vit-selo">
        <span class="selo">
          <svg class="selo-svg" viewBox="0 0 100 100" aria-hidden="true" focusable="false">
            <defs>
              <path id="vit-selo-arco" fill="none"
                    d="M 50,50 m -37,0 a 37,37 0 1,1 74,0 a 37,37 0 1,1 -74,0"></path>
            </defs>
            <text class="selo-texto">
              <textPath href="#vit-selo-arco" startOffset="0">
                Escopo · Método · Escopo · Método ·
              </textPath>
            </text>
          </svg>
          <span class="selo-miolo" aria-hidden="true">
            <svg viewBox="0 0 24 24" focusable="false">
              <path d="M12 4v14M6 13l6 6 6-6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
          </span>
          <span class="visually-hidden">Escopo e método.</span>
        </span>
      </span>

    </div>

    <!-- A FILEIRA. Sem script, quatro âncoras de verdade para os quatro
         destaques logo acima; com script, o tablist. O aria-controls aponta
         para o mesmo id do href — um atributo não substitui o outro: o href
         é a rota sem JavaScript, o aria-controls é o que amarra aba e painel
         para quem ouve a página.

         Os ids são únicos na PÁGINA, não no componente. Duas vitrines na
         mesma página pediriam renumeração à mão — e não pedem, porque o
         script renumera as cópias a partir da segunda. Ver script.js. -->
    <div class="vit-fileira" data-vit-fileira role="tablist"
         aria-label="Frentes de trabalho" data-rv data-d="3">

      <a class="vit-esc" id="vit-aba-1" href="#vit-painel-1" data-vit-esc
         role="tab" aria-selected="true" aria-controls="vit-painel-1">
        <span class="vit-esc-n">01</span>
        <span class="vit-esc-cat">Planejamento</span>
        <span class="vit-esc-nome">Diagnóstico e plano</span>
        <span class="vit-esc-frase">Onde o patrimônio está hoje e em que ordem ele se move.</span>
      </a>

      <a class="vit-esc" id="vit-aba-2" href="#vit-painel-2" data-vit-esc
         role="tab" aria-selected="false" aria-controls="vit-painel-2">
        <span class="vit-esc-n">02</span>
        <span class="vit-esc-cat">Sucessão</span>
        <span class="vit-esc-nome">Holding, doação e testamento</span>
        <span class="vit-esc-frase">O que passa adiante, decidido em vida e por escrito.</span>
      </a>

      <a class="vit-esc" id="vit-aba-3" href="#vit-painel-3" data-vit-esc
         role="tab" aria-selected="false" aria-controls="vit-painel-3">
        <span class="vit-esc-n">03</span>
        <span class="vit-esc-cat">Carteira</span>
        <span class="vit-esc-nome">Alocação e comitê</span>
        <span class="vit-esc-frase">A carteira volta à mesa quatro vezes por ano.</span>
      </a>

      <a class="vit-esc" id="vit-aba-4" href="#vit-painel-4" data-vit-esc
         role="tab" aria-selected="false" aria-controls="vit-painel-4">
        <span class="vit-esc-n">04</span>
        <span class="vit-esc-cat">Internacional</span>
        <span class="vit-esc-nome">Conta, câmbio e imposto</span>
        <span class="vit-esc-frase">Brasil e exterior lidos como um patrimônio só.</span>
      </a>

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

estilo.css · 456 linhas · 20,1 KB

/* ==========================================================================
   VITRINE COM DESTAQUE COMUTÁVEL — prefixo vit-
   Um card largo de duas colunas (texto à esquerda, foto sangrada à direita com
   a legenda DENTRO dela) e, embaixo, a fileira de quatro escolhas que o troca.

   Compõe: efeitos/selo-circular. Nada aqui redesenha o selo — o .vit-selo é só
   o invólucro que o ancora no canto da foto. O CSS do selo entra por
   preview-css.txt (MANUAL §8), e no site consumidor pelo <link> dele.

   Tokens usados:
     --max --pad          só via .wrap do core; nada aqui mexe na coluna
     --sec-gap            respiro da dobra, via .sec-pad
     --display --sans     tipografia do número, do título e dos rótulos
     --ease               curva única do projeto
     --accent             categoria e título DENTRO da foto (fundo real = foto)
     --accent-ink         realce sobre o FUNDO DA SEÇÃO: unidade do número,
                          ícone de traço, número da aba acesa
     --light              título dentro da foto
     --dark-rgb           scrim da foto
     --bg --fg --fg-2 --fg-muted --rule --rule-2   escopo de tom da seção
     --radius-media       canto do card e das escolhas

   AS DUAS TINTAS DE REALCE, e a regra que decide (MANUAL §3): o que está sobre
   o FUNDO DA SEÇÃO lê --accent-ink; o que está sobre FOTO COM VÉU lê --accent
   puro. Não é preciosismo — numa .sec-paper com o sage da Liberta, o accent
   puro dá 1.36:1 como texto sobre o creme, e o --accent-ink de um escopo claro
   cairia para ~3.5:1 sobre o scrim escuro da foto. Trocar um pelo outro numa
   varredura "por consistência" piora metade desta peça em silêncio.

   O ÍCONE DE TRAÇO ENTRA NO PRIMEIRO GRUPO, e é a única decisão aqui que não
   é óbvia: ele é desenho, não texto, mas é um traço de 1,4px — a fronteira de
   3:1 de contraste não-textual não sobra para arriscar 1.36:1 numa dobra
   clara. Ele lê --accent-ink como o texto ao lado dele.
   ========================================================================== */

.vit{
  /* Vão entre as duas colunas do destaque. */
  --vit-gap:clamp(18px,2.1vw,32px);

  /* PISO do card de destaque, e é piso mesmo: a coluna de texto cresce além
     dele com a copy do padrão editorial (medido: 537px a 1440, contra um piso
     de 468). Ele serve para o caso contrário — copy curta, em que o card
     encolheria até virar uma faixa e a foto perderia o tamanho de dobra.
     Quem garante que a altura não DANÇA entre uma aba e outra é o
     visibility:hidden dos painéis, mais abaixo, e não este número. */
  --vit-alt:clamp(380px,34vw,468px);

  /* A fusão da foto na troca. Curta de propósito: o conteúdo troca seco, e o
     que atravessa é só a imagem. Acima de ~400ms a peça começa a parecer um
     carrossel, que é exatamente o que ela não é. O script lê este mesmo
     número (SAIDA em script.js) — mudar um pede mudar o outro. */
  --vit-fusao:.28s;

  /* Quanto o véu desfocado desce ABAIXO da legenda, em fração da altura DELA.
     Não é fração da foto, e a diferença é a mesma que o midia/grade-cases-foto
     mediu: a legenda é ancorada no alto e cresce PARA DENTRO da imagem, então
     véu medido em % da foto é ultrapassado por um título de três linhas em
     tela estreita e a última linha cai sobre a parte nítida. Medido em fração
     da legenda, o véu acompanha o texto que ele existe para servir. */
  --vit-veil:80%;

  --vit-pad:clamp(24px,2.6vw,40px);      /* respiro da coluna de texto */
  --vit-cap-pad:clamp(16px,1.8vw,26px);  /* respiro da legenda dentro da foto */
}

/* ---------------------------------------------------------------- o palco */
/* position:relative só por causa do selo, que é absoluto e mora aqui — fora
   dos painéis, porque ele é assinatura da dobra e não conteúdo de uma frente.
   Dentro de um painel ele sumiria na primeira troca. */
.vit-palco{
  position:relative;
  margin-top:clamp(32px,4vw,56px);
}

/* COM SCRIPT os quatro painéis ocupam a MESMA célula de grade e só um aparece
   por vez; SEM SCRIPT o palco é bloco comum e eles se empilham, todos
   legíveis. A classe .is-live é escrita pelo script — é ela, e não o CSS, que
   decide qual dos dois desenhos vale. O contrário (CSS empilhando e script
   revelando) deixaria três quartos da peça invisíveis para quem está sem
   JavaScript. */
.vit.is-live .vit-palco{display:grid}
.vit.is-live .vit-painel{grid-area:1/1;z-index:1}

/* O PAINEL QUE NÃO ESTÁ ESCOLHIDO SAI POR visibility, E NÃO POR [hidden].
   Parece o caminho errado e é o certo, por uma razão medida: com
   `display:none` a célula da grade tem a altura do painel VISÍVEL, e os
   quatro não têm a mesma altura natural — medido a 1440, 537px nos painéis
   1, 3 e 4 e 517px no 2, porque a ressalva dele fecha em duas linhas em vez
   de três. Vinte pixels não são nada até serem: a fileira de escolhas está
   logo abaixo, e ela subia e descia debaixo do cursor de quem acabou de
   clicar. A 1024 a diferença medida era de 22px, e pela outra ponta (a frase
   de dois painéis ganha uma linha antes dos outros dois).

   Com visibility os quatro continuam ocupando a célula, a altura do palco é
   sempre a do MAIOR, e a fileira não se move em troca nenhuma. O segundo
   ganho aparece na própria fusão: o painel que entra já está com layout
   pronto e imagem rasterizada, então a travessia é pintura e nada mais.

   NÃO É opacity, e a diferença é toda: `visibility:hidden` tira o painel da
   árvore de acessibilidade E da ordem de foco, enquanto `opacity:0` deixa um
   bloco invisível e tabulável — a armadilha que o conversao/planos-honorarios
   já registrava. */
.vit-painel.is-fora{visibility:hidden}

/* O painel que SAI fica pintado por --vit-fusao e depois some. z-index 0 e
   não "sem z-index": a troca pode ir do painel 3 para o 1, e aí o que sai
   vem DEPOIS no documento e pintaria por cima do que entra. */
.vit.is-live .vit-painel.is-saindo{z-index:0}

/* Sem script os painéis se empilham, e precisam de vão entre si. Preso ao
   :not(.is-live) porque, empilhados na mesma célula, um margin-top deslocaria
   o painel inteiro para fora do lugar. */
.vit:not(.is-live) .vit-painel + .vit-painel{margin-top:var(--vit-gap)}

/* ------------------------------------------------------------- o destaque */
/* Texto à esquerda, foto à direita — e é essa ordem no DOM, sem inversão por
   CSS: quem lê a página na ordem do documento recebe o número e a frase antes
   da fotografia, que é a ordem certa de quem ouve. Em telas estreitas a foto
   sobe para o topo (ver o fim do arquivo), e aí a inversão é declarada em
   grid-row, não em `order` — grid-row move a CAIXA sem mexer na ordem de
   leitura nem na ordem de foco. */
.vit-painel{
  display:grid;
  /* minmax(0,…) nas duas colunas: o mínimo automático de uma célula de grade
     é o conteúdo dela, e sem isto uma frase longa empurraria a coluna de
     texto e estreitaria a foto de um painel só — os quatro destaques
     deixariam de ter a mesma geometria. */
  grid-template-columns:minmax(0,1fr) minmax(0,1.02fr);
  min-height:var(--vit-alt);
  overflow:hidden;
  /* Contexto de empilhamento próprio: o desfoque do véu da legenda não vaza
     para o painel que está por baixo durante a fusão. */
  isolation:isolate;
  border:1px solid var(--rule);
  border-radius:var(--radius-media);
  /* Superfície tênue: um VÉU DE LUZ sobre o fundo da seção, e não uma
     sombra. A diferença foi medida e não é de gosto — a primeira versão
     misturava --fg (a tinta do escopo), o que escurece o card numa dobra
     clara e derruba o contraste do que está escrito nele: a tinta de realce
     do escopo claro caía a 4,25:1 sobre este fundo, contra os 4,59:1 que ela
     tem sobre o creme puro, e 4,5 é o mínimo para texto normal. Com --light
     o card CLAREIA nos dois escopos — vira rgb(22,22,22) sobre o escuro da
     casa e sobe a 4,63:1 no claro. */
  background:color-mix(in srgb,var(--light) 5%,transparent);
}

/* §6.5 — quem dá display a algo que às vezes é [hidden] desliga à mão. O
   `display:grid` acima é regra de autor e venceria o [hidden] do navegador.
   A COMUTAÇÃO desta peça não usa o atributo (ver visibility, mais abaixo),
   mas quem derruba uma frente inteira no site consumidor usa — e sem esta
   linha o painel marcado continuaria na tela. Derrubar uma frente é apagar
   (ou esconder) a aba E o painel: uma aba sem painel vira botão morto. */
.vit-painel[hidden]{display:none}

/* ------------------------------------------------------- coluna de texto */
.vit-info{
  display:flex;flex-direction:column;
  min-width:0;
  padding:var(--vit-pad);
}

/* O número grande veste a primitiva .display-xl do core. Os dois parafusos
   dela entram AQUI, no ancestral, e não no próprio elemento: é a forma que o
   core documenta (eles são fallback dentro do var(), e escritos no mesmo nó
   venceriam o valor do ancestral). */
.vit-cifra{
  --display-xl:clamp(46px,5.2vw,78px);
  --display-xl-lh:1;
}

/* align-items:baseline é o que alinha a unidade pela base do algarismo, e não
   pelo meio dele. O gap em `em` acompanha o corpo do número. */
.vit-num{
  display:flex;align-items:baseline;gap:.32em;
  /* A .display-xl já pinta --fg; a linha fica fora daqui de propósito. */
  flex-wrap:wrap;
}

/* A unidade é texto de leitura, não peça gráfica: família de texto, corpo
   pequeno e a tinta de realce do escopo. Sem o letter-spacing normal ela
   herdaria os .05em do body, que em caixa alta de 14px separa demais. */
.vit-num-un{
  font-family:var(--sans);font-weight:400;
  font-size:clamp(13px,1.05vw,15.5px);
  line-height:1.2;letter-spacing:.02em;
  color:var(--accent-ink,var(--accent));
}

/* O RÓTULO DIZ O QUE O NÚMERO MEDE, e é ele que impede o algarismo de ser
   lido como desempenho. Não é decoração: sem esta linha, "90" sozinho ao lado
   de uma foto de escritório vira qualquer coisa que o leitor quiser. */
.vit-num-rot{
  display:block;margin-top:12px;
  font-size:clamp(13.5px,1.05vw,15px);line-height:1.5;
  color:var(--fg-2);
  max-width:36ch;
}

/* A RESSALVA. Veste a primitiva .nota do core, que traz só a tipografia; a
   margem, o filete e a medida são locais, como a primitiva manda. O filete em
   cima é a ligação visual com o número — junto com a ordem de leitura, é o
   que amarra a ressalva ao algarismo. */
.vit-nota{
  --nota-max:46ch;
  margin-top:14px;padding-top:14px;
  border-top:1px solid var(--rule);
}

.vit-frase{
  margin-top:clamp(18px,1.8vw,26px);
  font-size:clamp(14.5px,1.15vw,16px);line-height:1.55;
  color:var(--fg-2);
  max-width:48ch;
}

/* margin-top:auto prende os três atributos no pé da coluna, qualquer que seja
   o tamanho da frase — é o que faz os quatro destaques terem a mesma linha de
   base embaixo. */
.vit-attrs{
  margin-top:auto;
  padding-top:clamp(20px,2.2vw,30px);
  display:flex;flex-direction:column;gap:12px;
}

.vit-attr{
  display:flex;align-items:center;gap:11px;
  font-size:clamp(13.5px,1.05vw,14.5px);line-height:1.35;
  letter-spacing:normal;
  color:var(--fg-2);
}

/* flex:0 0 auto porque um ícone que encolhe para caber o texto deixa de ser
   ícone. A tinta é a de realce do escopo — ver o cabeçalho do arquivo. */
.vit-ico{
  flex:0 0 auto;
  width:20px;height:20px;
  color:var(--accent-ink,var(--accent));
}

/* -------------------------------------------------------------- a foto */
.vit-media{
  position:relative;
  min-width:0;
  overflow:hidden;
  isolation:isolate;
}

/* A foto é absoluta e a caixa é que manda na geometria: assim a mesma célula
   serve a um arquivo 16:9 e a um 3:2 sem recorte novo, e o enquadramento se
   ajusta por --vit-pos no style de cada painel.

   SEM width/height no <img>, e isso é armadilha medida (MANUAL §6): atributo
   de dimensão no elemento vence aspect-ratio e o card sai com a altura do
   arquivo. */
.vit-foto{
  position:absolute;inset:0;
  width:100%;height:100%;
  object-fit:cover;
  object-position:var(--vit-pos,50% 50%);
  transition:opacity var(--vit-fusao) var(--ease);
}

/* A FUSÃO. O painel que entra começa com a foto em zero e sem transição;
   removida a classe (depois de um reflow), a opacidade viaja até 1 por cima
   da foto que ainda está lá — cross-dissolve de verdade, e não um fade contra
   o fundo do card. A foto que sai NÃO desbota: fosse ela também transitando,
   o meio da travessia teria as duas em 50% e a dobra piscaria mais clara. */
.vit-painel.is-entrando .vit-foto{opacity:0;transition:none}

/* O texto do painel que sai some NA HORA, e só a foto atravessa: é isso que o
   desenho pede ("instantâneo no conteúdo, fusão curta na imagem"), e é
   também o que impede dois títulos e dois números de aparecerem sobrepostos
   durante os 280ms. */
.vit-painel.is-saindo .vit-info,
.vit-painel.is-saindo .vit-cap{display:none}

/* Scrim em degradê: escuro em cima, onde mora a legenda, e um retorno suave
   embaixo, que é onde o selo gira. O miolo fica limpo — é a parte da foto que
   se quer ver. Sem o retorno de baixo, o selo cairia sobre a parte clara de
   uma foto de céu aberto e o texto curvo dele sumiria. */
.vit-scrim{
  position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(to bottom,
    rgba(var(--dark-rgb),.92) 0%,
    rgba(var(--dark-rgb),.70) 18%,
    rgba(var(--dark-rgb),.28) 44%,
    rgba(var(--dark-rgb),.20) 70%,
    rgba(var(--dark-rgb),.54) 100%);
}

/* ---- a legenda, DENTRO da foto e ancorada EM CIMA ---- */
/* O contrato §6 manda a legenda para dentro da imagem; a ancoragem no alto é
   decisão desta peça e está explicada no markup — as duas colunas começam na
   mesma linha, e a metade de baixo da foto fica para o selo e para a descida
   do olho até a fileira. */
.vit-cap{
  position:absolute;left:0;right:0;top:0;z-index:2;
  padding:var(--vit-cap-pad);
}

/* O véu desfocado é um ::before DA LEGENDA, e não um irmão com altura própria.
   Duas razões, e as duas foram medidas em outro componente da casa:
     1. `bottom` negativo em porcentagem resolve contra a altura do .vit-cap,
        então o véu é sempre 1,8x o texto que ele serve — cresce com um título
        de três linhas e encolhe com um de uma;
     2. sendo pseudo-elemento IRMÃO do texto na ordem de pintura, e não
        ancestral dele, a máscara não toca o título. Máscara aplica-se aos
        descendentes: o texto dentro do elemento mascarado apareceria
        esmaecido na ponta.
   backdrop-filter SEMPRE com máscara em gradiente (contrato §6) — caixa reta
   produz borda dura contra a parte nítida da foto e lê "tarja". Aqui o
   gradiente da máscara vai do topo PARA BAIXO, o contrário da receita padrão,
   porque a legenda também está do outro lado. */
.vit-cap::before{
  content:"";
  position:absolute;
  top:0;left:0;right:0;
  bottom:calc(-1 * var(--vit-veil));
  z-index:-1;pointer-events:none;
  backdrop-filter:blur(8px) saturate(1.12);
  -webkit-backdrop-filter:blur(8px) saturate(1.12);
  -webkit-mask-image:linear-gradient(to bottom,#000 0%,rgba(0,0,0,.9) 46%,transparent 100%);
  mask-image:linear-gradient(to bottom,#000 0%,rgba(0,0,0,.9) 46%,transparent 100%);
}

/* --accent PURO: esta linha está sobre o scrim escuro DENTRO da foto, não
   sobre o fundo da seção. Ver o cabeçalho do arquivo. */
.vit-cat{
  display:block;
  font-family:var(--sans);font-weight:400;
  font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--accent);
}

/* --light e nunca --fg: o título vive dentro de uma ilha escura (foto sob
   scrim), e continua claro mesmo numa dobra .sec-paper. Mesma decisão do
   midia/grade-cases-foto e do conteudo/pilares-texto-na-foto. */
.vit-titulo{
  margin-top:9px;
  font-family:var(--display);font-weight:700;
  font-size:clamp(20px,2vw,29px);line-height:1.12;letter-spacing:-.02em;
  color:var(--light);
  max-width:19ch;
}

/* ---------------------------------------------------------------- o selo */
/* Só o invólucro. O disco, o texto curvo e a volta são do efeitos/selo-circular
   e não se repetem aqui. pointer-events:none porque ele cobre o canto da foto
   e não tem o que receber — clique nele não pode virar um buraco morto. */
.vit-selo{
  position:absolute;z-index:3;
  inset-block-end:clamp(14px,1.7vw,24px);
  inset-inline-end:clamp(14px,1.7vw,24px);
  pointer-events:none;
}

/* ------------------------------------------------------------- a fileira */
.vit-fileira{
  display:grid;
  grid-template-columns:repeat(4,minmax(0,1fr));
  gap:clamp(10px,1.1vw,16px);
  margin-top:clamp(12px,1.4vw,20px);
}

/* Cada escolha é um <a> de verdade — é ele que faz a rota sem JavaScript
   funcionar. O anel de foco é o :focus-visible do core, e cai em volta da
   caixa inteira porque a caixa É o link: este arquivo não escreve uma linha
   de CSS de foco, e escrever `outline:none` aqui apagaria o único sinal de
   onde o teclado está. */
.vit-esc{
  display:flex;flex-direction:column;gap:7px;
  padding:clamp(15px,1.5vw,21px) clamp(13px,1.3vw,19px);
  border:1px solid var(--rule);
  /* O filete de topo é o que acende na escolha acesa. Fica em 2px desde o
     repouso para a troca não mover o texto 1px para baixo. */
  border-top:2px solid var(--rule-2);
  border-radius:var(--radius-media);
  color:inherit;text-decoration:none;
  transition:border-color .35s var(--ease),background .35s var(--ease);
}

/* O estado aceso sai do aria-selected, e não de uma classe paralela. Uma
   fonte só: não existe como o desenho discordar do que o leitor de tela
   anuncia, porque os dois leem o mesmo atributo. Sem script, o markup já
   nasce com o primeiro aceso. */
.vit-esc[aria-selected="true"]{
  border-top-color:var(--accent);
  background:color-mix(in srgb,var(--light) 7%,transparent);
}
.vit-esc:hover{border-color:var(--rule-2)}

.vit-esc-n{
  font-family:var(--display);font-weight:700;
  font-size:11.5px;letter-spacing:.14em;line-height:1;
  color:var(--fg-muted);
}
.vit-esc[aria-selected="true"] .vit-esc-n{color:var(--accent-ink,var(--accent))}

.vit-esc-cat{
  font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;line-height:1.2;
  color:var(--fg-muted);
}

.vit-esc-nome{
  font-family:var(--display);font-weight:700;
  font-size:clamp(14.5px,1.15vw,16.5px);line-height:1.2;letter-spacing:-.015em;
  color:var(--fg);
}

/* A frase curta some quando a escolha está acesa? Não — ela fica. O painel
   aberto já diz mais, mas apagar a frase encolheria a caixa da aba acesa e a
   fileira dançaria a cada troca. */
.vit-esc-frase{
  font-size:12.5px;line-height:1.45;letter-spacing:normal;
  color:var(--fg-muted);
}

/* --------------------------------------------------------- movimento reduzido */
/* Colado nas regras que animam (contrato §6.6). A troca CONTINUA acontecendo
   — a pessoa pediu por ela —, só que sem a fusão: a foto aparece inteira de
   uma vez. O script lê a mesma preferência e pula a fase de saída, para não
   deixar um painel pintado por 280ms sem nada acontecendo na tela. */
@media (prefers-reduced-motion:reduce){
  .vit-foto{transition:none}
  .vit-esc{transition:none}
}

/* ---------------------------------------------------------------- mobile */
/* DUAS QUEDAS, e a primeira é a que importa: em coluna única a foto sobe para
   o topo do card, porque uma coluna de texto de 320px seguida da foto lá
   embaixo perde a legenda — que é o que nomeia a frente.

   A troca é por grid-row, e não por `order`: as duas movem a caixa, mas
   `order` também move o foco em alguns contextos de flex, e aqui a ordem de
   leitura e a de foco têm de continuar sendo a do documento. */
@media (max-width:980px){
  .vit-painel{
    grid-template-columns:minmax(0,1fr);
    min-height:0;
  }
  .vit-media{grid-row:1;aspect-ratio:16/10}
  .vit-info{grid-row:2}
  /* A fileira cai para 2x2. Não há passo de três: quatro escolhas em três
     colunas deixam uma célula vazia. */
  .vit-fileira{grid-template-columns:repeat(2,minmax(0,1fr))}
  /* O selo sai. Ele é assinatura, e num card de 390px de largura o disco de
     132px come um terço da foto e cai por cima da legenda quando o título vai
     a três linhas. */
  .vit-selo{display:none}
}

@media (max-width:560px){
  .vit-media{aspect-ratio:3/2}
  .vit-esc{gap:5px}
  .vit-esc-frase{font-size:12px}
}

script.js · 289 linhas · 12,8 KB

/* ==========================================================================
   VITRINE COM DESTAQUE COMUTÁVEL — prefixo vit-
   A troca de destaque, e só ela. A grade, o texto na foto, o scrim e a fusão
   são CSS; o selo é outro componente. O que o script faz é o que o CSS não
   sabe fazer sozinho: decidir qual painel está de pé, dizer isso à árvore de
   acessibilidade e dar conta do teclado que o padrão de abas exige.

   O QUE ELE FAZ, EM ORDEM:
     1. renumera ids, href e aria-controls quando há mais de uma vitrine na
        página — sem isso, a segunda cópia apontaria para os painéis da
        primeira, e o navegador nem acusaria;
     2. põe .is-live na seção, que é o que liga, no CSS, o empilhamento dos
        painéis numa célula só;
     3. marca com .is-fora os painéis que não estão escolhidos (o CSS os tira
        por visibility, e não por display — ver estilo.css, é o que impede a
        dobra de mudar de altura a cada troca), dá tabindex="0" ao que ficou
        (o APG pede isso para painel sem nada focável dentro) e escreve o
        roving tabindex das abas;
     4. troca: no clique e nas setas do teclado, com fusão curta na imagem e
        conteúdo instantâneo, e um anúncio em aria-live para quem não vê.

   O ESTADO SEM JAVASCRIPT É O ESTADO DE PARTIDA DO MARKUP: os quatro
   destaques empilhados e legíveis, e a fileira funcionando como quatro
   âncoras. Este script ESCONDE o que sobra — o contrário (CSS escondendo e
   script revelando) deixaria três quartos da peça invisíveis para quem tem o
   JavaScript bloqueado. Mesmo critério do conversao/planos-honorarios.

   POR QUE O ROVING TABINDEX NÃO ESTÁ NO MARKUP. Escrito à mão lá, ele tiraria
   três das quatro escolhas da ordem de tabulação de quem está SEM script — e
   aí a rota sem JavaScript, que é a razão de as escolhas serem <a href>,
   quebraria de verdade. Os papéis (role, aria-selected, aria-controls) ficam
   no markup, porque descrevem a estrutura e não dependem de estado; o
   tabindex é estado e mora aqui.

   ATIVAÇÃO AUTOMÁTICA nas setas, e não manual (seta move, Enter confirma). O
   APG permite quando mostrar o painel é barato, e aqui é: o conteúdo já está
   no DOM e a troca não busca nada. Ativação manual custaria um Enter a cada
   leitura e faria a seta não fazer nada visível — pior para todo mundo.

   NADA É GRAVADO e nada anda sozinho: sem localStorage, sem relógio, sem
   troca automática. Aba que gira no relógio rouba a leitura no meio de um
   parágrafo e ainda obrigaria a peça a carregar o botão de pausa da WCAG
   2.2.2 — quem troca é sempre a pessoa.

   MOVIMENTO REDUZIDO: a troca continua, sem a fusão. O painel que sai é
   escondido na hora em vez de ficar pintado por 280ms, porque com a transição
   desligada pelo CSS esse tempo seria uma pausa morta, não uma travessia.
   ========================================================================== */

(function () {

  var raizes = document.querySelectorAll('[data-vit]');
  if (!raizes.length) return;                        // guard silencioso

  /* Bate com --vit-fusao no estilo.css. Mudar um pede mudar o outro: menor
     aqui e a foto que sai some antes de a nova ter chegado; maior e sobra um
     painel morto pintado por baixo. */
  var FUSAO = 280;

  /* A flag do core é a fonte. O fallback existe para o componente colado
     sozinho, sem motion.js — e repete a regra do ?motion=on. */
  function reduzido() {
    if (window.LIB && typeof window.LIB.reduce === 'boolean') return window.LIB.reduce;
    return matchMedia('(prefers-reduced-motion: reduce)').matches &&
           location.search.indexOf('motion=on') < 0;
  }

  function iniciar(raiz, indice) {

    /* Parte da raiz do próprio componente, nunca do documento: duas vitrines
       na mesma página não podem misturar as abas uma da outra. */
    var fileira = raiz.querySelector('[data-vit-fileira]');
    var abas    = raiz.querySelectorAll('[data-vit-esc]');
    var paineis = raiz.querySelectorAll('[data-vit-painel]');
    var aviso   = raiz.querySelector('.vit-anuncio');

    /* Uma frente só, ou fileira removida pelo consumidor: não há o que
       comutar, e a peça continua correta como está — um destaque grande e
       nada mais. Sai sem tocar em nada, inclusive sem pôr .is-live, que é o
       que esconderia os outros painéis. */
    if (!fileira || abas.length < 2 || paineis.length < 2) return;

    var atual = null;      // aba acesa
    var saindo = null;     // painel ainda pintado, no meio da fusão
    var timer = 0;
    var i;

    /* ------------------------------------------------- ids únicos na página */

    /* O markup traz ids fixos (vit-painel-1…), porque a rota sem JavaScript
       precisa deles no HTML — âncora não se inventa em tempo de execução. A
       partir da SEGUNDA vitrine da página eles colidiriam: o href da aba
       levaria ao painel da primeira, e o aria-controls apontaria para o painel
       errado sem que nada acusasse. O sufixo resolve os três lugares de uma
       vez (id do painel, id da aba, href e aria-controls da aba). */
    if (indice > 0) {
      var sufixo = '-' + (indice + 1);
      for (i = 0; i < paineis.length; i++) {
        if (paineis[i].id) paineis[i].id += sufixo;
      }
      for (i = 0; i < abas.length; i++) {
        var alvo = abas[i].getAttribute('aria-controls');
        if (abas[i].id) abas[i].id += sufixo;
        if (alvo) {
          abas[i].setAttribute('aria-controls', alvo + sufixo);
          abas[i].setAttribute('href', '#' + alvo + sufixo);
        }
      }
      for (i = 0; i < paineis.length; i++) {
        var rot = paineis[i].getAttribute('aria-labelledby');
        if (rot) paineis[i].setAttribute('aria-labelledby', rot + sufixo);
      }
      /* O arco do selo composto também é um id, e é o gotcha documentado do
         efeitos/selo-circular: dois selos na mesma página com o mesmo id
         fazem os DOIS textos curvos seguirem o primeiro arco. Como os arcos
         são idênticos ninguém percebe — até o dia em que um selo tiver
         tamanho diferente do outro. Lê o alvo do próprio textPath em vez de
         cravar o nome: quem trocar o id no markup leva a correção junto. */
      var curva = raiz.querySelector('textPath');
      if (curva) {
        var ref = (curva.getAttribute('href') || '').slice(1);
        var arco = ref ? raiz.querySelector('[id="' + ref + '"]') : null;
        if (arco) {
          arco.id = ref + sufixo;
          curva.setAttribute('href', '#' + ref + sufixo);
        }
      }
    }

    /* ------------------------------------------------------------ utilidades */

    function painelDe(aba) {
      var id = aba.getAttribute('aria-controls');
      /* getElementById e não querySelector na raiz: o id já é único na página
         (é isso que o bloco acima garante), e o painel é irmão da fileira,
         não descendente dela. */
      return id ? document.getElementById(id) : null;
    }

    function nomeDe(aba) {
      var n = aba.querySelector('.vit-esc-nome');
      var c = aba.querySelector('.vit-esc-cat');
      return (c ? c.textContent.trim() : '') + (n ? ', ' + n.textContent.trim() : '');
    }

    function anunciar(texto) { if (aviso) aviso.textContent = texto; }

    /* Fecha uma fusão pendente na hora. Chamada tanto pelo relógio quanto por
       uma segunda troca disparada antes de a primeira terminar — segurar a
       seta do teclado faz exatamente isso, e sem este atalho a peça ficaria
       com dois painéis pintados um sobre o outro. */
    function concluir() {
      if (timer) { clearTimeout(timer); timer = 0; }
      if (!saindo) return;
      saindo.classList.remove('is-saindo');
      saindo.classList.add('is-fora');
      saindo.removeAttribute('tabindex');
      saindo = null;
    }

    function entrar(painel) {
      painel.classList.remove('is-fora');
      /* Painel sem nada focável dentro: o APG pede tabindex="0" para que quem
         navega por teclado consiga parar nele e rolar o conteúdo. Só o painel
         VISÍVEL recebe — os escondidos já saem da ordem por [hidden], e o que
         está saindo perde o tabindex em concluir(). */
      painel.setAttribute('tabindex', '0');
      painel.classList.add('is-entrando');
      /* Reflow: sem ele o navegador junta o "voltou a ser visível" e o
         "perdeu a classe" no mesmo recálculo, e a transição não roda — a foto
         apareceria seca. */
      void painel.offsetWidth;
      painel.classList.remove('is-entrando');
    }

    /* ------------------------------------------------------------- a troca */

    function acender(aba, comFoco) {
      if (!aba || aba === atual) return;
      var painel = painelDe(aba);
      if (!painel) return;

      concluir();
      var sai = atual ? painelDe(atual) : null;

      /* O roving tabindex: só a aba acesa fica na ordem de tabulação. É isto
         que faz um Tab atravessar a fileira inteira em vez de parar quatro
         vezes, e é a metade do padrão de abas que ninguém vê. */
      for (var k = 0; k < abas.length; k++) {
        var on = abas[k] === aba;
        abas[k].setAttribute('aria-selected', on ? 'true' : 'false');
        abas[k].setAttribute('tabindex', on ? '0' : '-1');
      }
      atual = aba;
      entrar(painel);
      anunciar(nomeDe(aba) + '. Painel em exibição.');
      if (comFoco) aba.focus();

      if (!sai || sai === painel) return;

      if (reduzido()) {
        sai.classList.add('is-fora');
        sai.removeAttribute('tabindex');
        return;
      }

      /* A foto que sai fica pintada por baixo enquanto a que entra sobe de
         zero a um. É o que faz a travessia parecer uma fusão e não um piscar
         contra o fundo do card. */
      sai.classList.add('is-saindo');
      sai.removeAttribute('tabindex');
      saindo = sai;
      timer = setTimeout(function () { timer = 0; concluir(); }, FUSAO);
    }

    /* ------------------------------------------------------ inicialização */

    raiz.classList.add('is-live');

    /* A aba de partida é a que o markup marca com aria-selected="true"; se
       ninguém estiver marcado, é a primeira. Nada de confiar no :target ou no
       hash da URL aqui — abrir a página num link antigo não pode escolher uma
       frente diferente da que a copy do markup declarou como primeira. */
    var inicial = abas[0];
    for (i = 0; i < abas.length; i++) {
      if (abas[i].getAttribute('aria-selected') === 'true') { inicial = abas[i]; break; }
    }

    var painelInicial = painelDe(inicial);
    for (i = 0; i < paineis.length; i++) {
      if (paineis[i] === painelInicial) {
        paineis[i].classList.remove('is-fora');
        paineis[i].setAttribute('tabindex', '0');
      } else {
        paineis[i].classList.add('is-fora');
        paineis[i].removeAttribute('tabindex');
      }
    }
    for (i = 0; i < abas.length; i++) {
      var ligada = abas[i] === inicial;
      abas[i].setAttribute('aria-selected', ligada ? 'true' : 'false');
      abas[i].setAttribute('tabindex', ligada ? '0' : '-1');
    }
    atual = inicial;

    /* ---------------------------------------------------------- eventos */

    /* Delegação na fileira: um listener só, e uma vitrine com seis frentes
       não custa seis listeners. O preventDefault só existe porque a aba é um
       <a href> de verdade — sem script ele navega até o painel, e é para isso
       que está lá; com script, navegar seria rolar a página por baixo do dedo
       de quem só queria trocar de aba. */
    fileira.addEventListener('click', function (e) {
      var aba = e.target.closest ? e.target.closest('[data-vit-esc]') : null;
      if (!aba || !fileira.contains(aba)) return;
      e.preventDefault();
      acender(aba, false);
    });

    /* Teclado do padrão de abas (APG): ←/→ andam e trocam, Home/End vão às
       pontas, e a fila é circular — da última a seta direita volta à
       primeira. O foco acompanha a troca, senão a seta seguinte partiria do
       lugar errado.

       Enter e Espaço não aparecem aqui de propósito: o <a> já dispara clique
       com Enter, e o clique acima faz o resto. */
    fileira.addEventListener('keydown', function (e) {
      var aba = e.target.closest ? e.target.closest('[data-vit-esc]') : null;
      if (!aba) return;

      var pos = -1, k;
      for (k = 0; k < abas.length; k++) if (abas[k] === aba) pos = k;
      if (pos < 0) return;

      var destino = -1;
      if (e.key === 'ArrowRight' || e.key === 'ArrowDown') destino = (pos + 1) % abas.length;
      else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') destino = (pos - 1 + abas.length) % abas.length;
      else if (e.key === 'Home') destino = 0;
      else if (e.key === 'End') destino = abas.length - 1;
      else return;

      e.preventDefault();
      acender(abas[destino], true);
    });
  }

  for (var n = 0; n < raizes.length; n++) iniciar(raizes[n], n);

})();

Uma frente por vez, do tamanho de uma dobra. Em cima, o destaque: um card largo de duas colunas — à esquerda um número grande com o rótulo do que ele mede, a ressalva presa a ele, uma frase e três atributos com ícone de traço; à direita a foto sangrada, com a categoria e o título escritos dentro da imagem, ancorados no alto, sobre véu desfocado com máscara. No canto de baixo da foto gira o selo circular. Embaixo, a fileira de três a quatro escolhas — número, categoria, nome e frase curta —, e clicar numa delas troca o destaque: o conteúdo entra seco e só a foto atravessa, em 280ms.

O problema editorial que ela resolve não é "mostrar serviços": é dar tamanho de dobra a uma frente sem esconder as outras. Quatro frentes de assessoria em quatro colunas viram quatro cartõezinhos em que nenhuma tem foto, número nem espaço para uma frase inteira; quatro dobras empilhadas custam quatro rolagens e a quarta ninguém lê. A vitrine dá a uma delas a dobra inteira e deixa as outras três à vista como escolha, na mesma tela.


Compliance

O número grande é o ponto de compliance desta peça, e é ele que decide o que pode entrar. Um algarismo em corpo de 75px ao lado de uma foto de escritório é lido como afirmação, e num site de assessoria a afirmação mais perigosa é a de desempenho.

Regra O que isso quer dizer
O número mede o TRABALHO, nunca o dinheiro prazo, frequência de revisão, comitês por ano, gerações cobertas pelo desenho, jurisdições no escopo, anos de casa, famílias atendidas. Os quatro do markup são inofensivos por construção: 90 dias, 3 gerações, 4 comitês por ano, 2 jurisdições
Nunca rentabilidade, retorno, "+52%", "1,8× o CDI", "R$ 4 bi sob gestão em 3 anos", patrimônio médio de cliente — com número ou com palavra, no rótulo ou na frase
O rótulo (.vit-num-rot) não é opcional é ele que diz o que o algarismo mede. Sem rótulo, "90" sozinho vira o que o leitor quiser que seja
Toda cifra vem com a ressalva <p class="nota vit-nota">, logo abaixo do rótulo, separada por um filete. As quatro do markup terminam negando resultado — é esse o padrão a manter, não o texto exato
Nada de marca real nem nas fotos, nem no texto. Os exemplos não citam banco, corretora, gestora nem veículo
Nada de cliente nomeado a frente descreve o trabalho, não o caso de alguém
A foto não é de cliente e se for de pessoa real, exige autorização de imagem: nesta dobra a foto não é creditada em lugar nenhum e não há onde pôr a ressalva

Se a área técnica do cliente pedir um número que é de resultado, a resposta não é amaciar o texto: é outro componente. Número apurado com fonte e data mora em dados/grade-indicadores; cotação com validade declarada, em dados/cards-cotacoes.


Quando usar

Quando não usar

Contra os vizinhos

Precisa de Use
Uma frente por vez, com foto, número e três compromissos este
Comparar modelos lado a lado, com preço e botão conversao/planos-honorarios
Conjunto fechado de quatro cards que levam a outra página midia/grade-cases-foto
Cinco a oito frentes com foto, sem tamanho de dobra para cada listas/paineis-empilhados
Recursos de peso desigual, com um parágrafo que abre no clique conteudo/bento-recursos-expansivel
Etapas cuja ordem é a informação listas/linha-do-tempo-etapas
Números apurados, com fonte e data dados/grade-indicadores

Padrão editorial

Slot Regra
Olho (.eyebrow) 2 a 3 palavras, opcional. Diz que o que vem é um conjunto: "Frentes de trabalho".
Título (h2.sec-title) 4 a 8 palavras, uma em <em>. Nomeia o conjunto e diz quantas são — "Quatro frentes, e o escopo de cada uma". O número na frase é o que avisa que há escolha.
Apoio (.sec-lead) 1 a 2 frases. Diz que há uma escolha embaixo. Sem isso, a fileira é lida como quatro cards de conteúdo, e ninguém clica.
Categoria na foto (.vit-cat) 12 a 28 caracteres, do mesmo conjunto fechado nas três ou quatro. Acima de 28 a linha em caixa alta com .16em de tracking encosta na borda da legenda.
Título do destaque (.vit-titulo) 4 a 8 palavras, dentro da foto. Diz o que a frente faz, nunca o que ela rende. As quatro do markup têm 5 a 7 palavras (28 a 36 caracteres).
Número (.vit-num) 1 a 3 dígitos. Ver Compliance — é o slot mais perigoso da peça. Número com separador de milhar quebra o alinhamento com a unidade.
Unidade (.vit-num-un) 1 a 3 palavras, na linha de base do algarismo: "dias", "gerações", "comitês por ano". Acima de três palavras ela quebra e o número deixa de ser uma peça só.
Rótulo (.vit-num-rot) 8 a 16 palavras. Diz o que o número mede. Não é opcional.
Ressalva (.vit-nota) 100 a 170 caracteres. De onde vem o número e o que ele não afirma. Uma por destaque, sempre.
Frase (.vit-frase) 180 a 210 caracteres, uma ou duas frases. As do markup medem 184 a 196 — a faixa é estreita de propósito (ver abaixo).
Atributos (.vit-attr) exatamente 3 por destaque, de 4 a 8 palavras, no mesmo tipo gramatical. Cada um leva um ícone de traço desenhado no markup: não há biblioteca de ícones para consultar, e é assim de propósito (contrato §11).
Escolha: número (.vit-esc-n) 2 dígitos sempre, inclusive abaixo de dez. É o que alinha a fileira.
Escolha: categoria (.vit-esc-cat) 1 a 2 palavras — a categoria do painel, encurtada: "Planejamento patrimonial" vira "Planejamento".
Escolha: nome (.vit-esc-nome) 2 a 5 palavras. O que a frente entrega, não o nome dela de novo: "Holding, doação e testamento" ganha de "Sucessão", que a linha de cima já disse.
Escolha: frase (.vit-esc-frase) 40 a 60 caracteres, uma frase. Acima de 60 ela vai a quatro linhas na célula de 170px do celular, e a fileira fica mais alta que o card.
Texto do selo (textPath) 30 a 53 caracteres, opcional. O arco gasta ~4,38 unidades por caractere em 232,5 disponíveis; o excedente não é desenhado e some sem aviso.

Quantidade: 3 ou 4 frentes. Quatro é o desenho de origem e o que fecha a fileira em 2×2 no celular.

As frases dos destaques no mesmo comprimento, e isso aqui é regra e não gosto. A peça não muda de altura entre uma aba e outra — quem garante isso é o visibility:hidden dos painéis, não a copy —, mas a dobra tem sempre a altura do maior painel. Uma frente com metade do texto das outras não desalinha nada: ela deixa um vão morto no pé da coluna e lê como frente de segunda classe. Medido com a copy do markup: frases de 184 a 196 caracteres, ressalvas de 105 a 141, e os quatro painéis fechando entre 517 e 537px a 1440 — os 20px de diferença vêm da ressalva do painel 2, que fecha em duas linhas em vez de três.

Especificação dos assets

São três ou quatro fotos, e nenhuma delas atravessa esta biblioteca (contrato §11). O preview usa ph-wide-2, ph-wide-1, ph-wide-4 e ph-wide-3 de showroom/demo/.

Proporção do arquivo paisagem 16:9, e o recorte é do CSS. A célula é quase quadrada no desktop (medido: 552×535 a 1440) e 16:10 no celular — não recorte os arquivos, ajuste o enquadramento por --vit-pos no style de cada <img>
Dimensão mínima 1600 × 900. A célula maior é a do celular em coluna única (348px de largura), mas o desktop pede 552px de largura por 535 de altura de um arquivo 16:9, o que consome a altura inteira dele
Peso até 260 KB cada. São três ou quatro na mesma dobra e todas carregam de uma vez — ver o gotcha do eager
Onde o texto cobre o terço de cima, sempre: categoria e título vivem ali, sobre scrim mais véu desfocado. Medido, a legenda mede 142px a 1440 e 103px em 390. Nada de importante no alto: nem rosto, nem horizonte, nem o detalhe que justifica a foto
Onde o selo cobre o canto inferior direito, num disco de 132px. No celular o selo sai
Enquadramento assunto no terço central ou inferior. object-position padrão é 50% 50%; os quatro do markup declaram o próprio
O que não pode ter texto dentro da foto (colide com a legenda); logo de instituição real; marca d'água; moldura ou sombra no arquivo — a quina vem de --radius-media

As fotos precisam ser do mesmo tratamento. Elas não aparecem lado a lado — aparecem uma no lugar da outra, e a troca é uma fusão de 280ms. Duas fotos de temperatura diferente fazem a fusão parecer um erro de renderização, e não uma travessia. O critério objetivo é o mesmo do midia/grade-cases-foto: luminância média dos arquivos dentro de uma faixa de 20 pontos.


Como usar

<link rel="stylesheet" href="componentes/efeitos/selo-circular/estilo.css">
<link rel="stylesheet" href="componentes/conteudo/vitrine-destaque/estilo.css">
<script src="componentes/conteudo/vitrine-destaque/script.js"></script>

A primeira folha é do componente composto: o selo do canto é o efeitos/selo-circular, e nada no estilo.css daqui redesenha o disco, o texto curvo ou a volta. No preview isso é declarado em preview-css.txt (MANUAL §8). Não existe preview-js.txt porque o selo é CSS puro — não há script dele para carregar. Tirando o selo (ver Variações), a primeira linha sai junto.

Ganchos

Gancho Papel
[data-vit] a raiz. O script parte daqui, e é o que ele conta para renumerar cópias
[data-vit-fileira] a fileira, que é o role="tablist"
[data-vit-esc] cada escolha: <a href="#id"> com role="tab"
[data-vit-painel] cada destaque: role="tabpanel"
--vit-pos enquadramento da foto, no style de cada <img>
.is-live escrita pelo script na raiz. Liga o empilhamento dos painéis numa célula só
.is-fora / .is-saindo / .is-entrando estado de cada painel na troca
escopo de tom .sec-dark, .sec-deep, .sec-paper ou .sec-light na seção. Nenhuma linha de CSS muda

Parafusos

Variável Padrão O que faz
--vit-gap clamp(18px,2.1vw,32px) vão entre as duas colunas do destaque
--vit-alt clamp(380px,34vw,468px) piso do card. Com a copy do padrão editorial o card fecha acima disso (537px a 1440); o piso serve para copy curta, em que o card viraria uma faixa
--vit-fusao .28s duração da fusão da foto. Bate com FUSAO no script.js — mudar um pede mudar o outro
--vit-veil 80% quanto o véu desfocado desce abaixo da legenda, em fração da altura dela
--vit-pad clamp(24px,2.6vw,40px) respiro da coluna de texto
--vit-cap-pad clamp(16px,1.8vw,26px) respiro da legenda dentro da foto

Variações


As duas rotas, e o que cada uma entrega

Com JavaScript é o padrão de abas do APG, inteiro. Medido no DOM: role="tablist" na fileira, role="tab" nas quatro escolhas com aria-selected numa só, role="tabpanel" nos quatro destaques com aria-labelledby apontando para a aba, aria-controls apontando para o painel, roving tabindex (0,-1,-1,-1 andando com a escolha) e tabindex="0" no painel visível — que o APG pede para painel sem nada focável dentro. ←/→ andam e trocam, com a fila circular; Home/End vão às pontas; o foco acompanha. Uma região aria-live="polite" anuncia a troca ("Sucessão, Holding, doação e testamento. Painel em exibição.").

Sem JavaScript a peça continua inteira, e isso foi medido com o JS desligado de verdade (--sem-js, que é quando o @media (scripting:none) do core casa): os quatro painéis visíveis e empilhados (topos em 421, 988, 1535 e 2103), o palco em opacity:1 e filter:none, as quatro escolhas na ordem de tabulação (tabIndex 0 nas quatro) e as quatro âncoras resolvendo — abrindo preview.html#vit-painel-3 a página para com o painel 3 no topo da janela, ao pixel.


Gotchas

Escopo fundo do card unidade e ícone (--accent-ink) ressalva (--fg-muted) rótulo, frase e atributos
.sec-dark rgb(22,22,22) 11,01:1 5,52:1 10,81:1
.sec-deep rgb(36,36,36) 9,52:1 4,77:1 9,34:1
.sec-paper rgb(238,234,228) 4,63:1 4,41:1 8,71:1
.sec-light rgb(238,234,228) 4,80:1 4,32:1 9,07:1

Os dois números em negrito são da ressalva, e a conta é do --fg-muted do core — não de uma cor desta peça. A primitiva .nota define essa tinta, e ela vale hoje para dezessete componentes. Projeto que precise de AA estrito em dobra clara corrige no --fg-muted do tema, e a correção chega a todos de graça.

Movimento reduzido / toque

Movimento reduzido desliga a fusão e mantém a troca — a pessoa pediu por ela. Medido com a preferência ligada: transition-duration da foto em 0s, LIB.reduce em true, a volta do selo em animation:none (o selo desliga sozinho, no CSS dele), e a troca completa em menos de 40ms, sem painel pintado esperando o relógio. O script lê a mesma preferência e pula a fase de saída: com a transição desligada, os 280ms seriam uma pausa morta e não uma travessia. A entrada da dobra é do core e o core já a desliga.

Toque: nada aqui depende de hover. O único hover da peça é o realce de borda da escolha, e ele é acréscimo — o estado aceso sai do aria-selected, que existe com ou sem ponteiro. Medido em 390px com emulação de toque: sem estouro horizontal, a foto sobe para o topo do card (grid-row:1, topo em 438 contra 679 do texto), a fileira vira 2×2 de 170px por célula com 159px de altura, e o selo sai — num card de 350px o disco de 132px comeria um terço da foto e cairia sobre a legenda com título de três linhas.


Histórico

Projeto Onde Data O quê
referência: skinboost-design-review.vercel.app dobra #catalogo 2026-09-21 Clone de referência, reescrito do zero. Nenhuma linha do original.

O que não atravessou. three.js e GSAP/ScrollTrigger, que lá animavam a troca — aqui a travessia é uma transição de opacidade de 280ms num <img>. Os ícones Phosphor, substituídos por doze SVG desenhados no markup. Os assets de Figma, substituídos pelos placeholders de showroom/demo/. E as cores cruas, substituídas por token: a peça roda nos quatro escopos de tom sem uma variação de código.

O que entrou, e não existia no original: o padrão de abas do APG inteiro, a região aria-live, a rota sem JavaScript (no original o destaque inicial e as trocas dependem do script) e a camada de compliance — que aqui deu forma à peça, e não só ao texto. O número grande da referência é uma métrica de produto; aqui ele foi fechado em estrutura do trabalho, com ressalva presa e uma regra escrita no componente.json que proíbe qualquer cifra de desempenho.

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

Tentação Aqui Por quê
[hidden] nos painéis inativos visibility:hidden com display:none a dobra muda de altura entre abas (537 contra 517 a 1440) e a fileira dança debaixo do cursor
opacity:0 para poder animar a saída visibility + fusão só na foto bloco invisível e tabulável é armadilha; visibility sai da árvore de acessibilidade e da ordem de foco
loading="lazy" nas quatro fotos todas eager imagem lazy dentro de conteúdo escondido não é buscada, e a primeira fusão atravessaria para um vazio
roving tabindex escrito no markup escrito pelo script no HTML ele tiraria três das quatro escolhas da ordem de tabulação de quem está sem JavaScript
<button> nas escolhas <a href="#id"> com role="tab" é o href que faz a rota sem script existir; o role e o preventDefault fazem o resto
legenda em tarja abaixo da foto dentro da imagem, ancorada no alto contrato §6, com a máscara do véu invertida para acompanhar
color-mix de --fg na superfície do card color-mix de --light misturar a tinta do escopo escurece o card no claro e derruba o realce a 4,25:1
ícone de biblioteca de terceiro doze SVG desenhados aqui contrato §11 — ícone proprietário não atravessa
recopiar o selo circular compor efeitos/selo-circular o desenho e a volta vivem lá; aqui só o invólucro que ancora
troca automática, como carrossel só por gesto aba no relógio rouba a leitura e cobraria o botão de pausa da WCAG 2.2.2

Divergências conhecidas vs. produção: nenhuma — a peça nasce aqui.

Calibragem no primeiro uso. Espere ajustar três coisas: o --vit-pos de cada foto (o corte que o assunto aguenta com a legenda no alto), o comprimento das frases dos destaques (é o que decide a altura da dobra inteira, porque ela é a do maior painel) e — antes de tudo — qual número cada frente vai mostrar, que é conversa com o compliance do cliente e não decisão de design.