← Biblioteca componentes/listas/leque-paineis/ficha.md CódigoTela cheia
listas/leque-paineis

Leque de painéis verticais

🟢 estavel complexidade média prefixo fan- servicos lista imagem clique acordeao expansivel depoimento prova-social rodizio citacao

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

<link rel="stylesheet" href="componentes/listas/leque-paineis/estilo.css">
<script src="componentes/listas/leque-paineis/script.js"></script>

markup.html · 242 linhas · 12,7 KB

<!-- ==========================================================================
     LEQUE — painéis verticais que expandem
     Prefixo: fan-
     Requer: core/tokens.css, core/base.css, core/motion.css, core/motion.js
              + este estilo.css e este script.js

     De 4 a 6 painéis. Com 3 sobra largura e o efeito some; com 7 o painel
     fechado fica estreito demais para o título vertical.
     O primeiro painel nasce com .is-open — a seção nunca abre toda fechada.

     ----------------------------------------------------------------------
     ESTE ARQUIVO TRAZ DUAS SEÇÕES, E VOCÊ COLA UMA. São os dois modos do
     mesmo componente, com o mesmo motor e o mesmo CSS:

       1. MODO SERVIÇO (abaixo) — número, rótulo vertical, título e uma frase
          de benefício. É a forma original, de 2026-08, e não mudou nada.
       2. MODO DEPOIMENTO (mais abaixo) — .fan-depo no bloco raiz. O painel
          aberto carrega aspas, citação em corpo grande, nome, relação, uma
          linha de contexto e o contador. O fechado mostra só o nome e o
          sinal +/−. Opcionalmente com rodízio (data-fan-auto).

     POR QUE UMA VARIAÇÃO E NÃO UM COMPONENTE NOVO: a mecânica é idêntica —
     um aberto por vez, hover/clique/foco/seta, a mesma curva de 1,1s, o
     mesmo véu com máscara. O que muda é conteúdo e três medidas, e as três
     saem de parafuso do próprio componente (--fan-open, --fan-full-w,
     --fan-say). Dois componentes significaria dois CSS para manter em par.
     ========================================================================== -->

<section class="sec-paper sec-pad" id="solucoes">
  <div class="wrap">
    <span class="eyebrow" data-rv>O que oferecemos</span>
    <h2 class="sec-title ttl">Soluções completas para quem leva o <em>patrimônio</em> a sério</h2>

    <div class="fan" data-fan>
      <article class="fan-panel is-open" tabindex="0">
        <img class="fan-img" src="../../../showroom/demo/ph-3.webp" alt="">
        <div class="fan-veil"></div>
        <span class="fan-num">01</span>
        <div class="fan-vert">Consultoria de<br>investimento</div>
        <div class="fan-full">
          <h3 class="fan-title">Consultoria de investimento</h3>
          <p class="fan-text">Abordagem personalizada e adaptada ao seu perfil de risco, para que cada decisão contribua com seus objetivos de vida e patrimônio.</p>
        </div>
      </article>

      <article class="fan-panel" tabindex="0">
        <img class="fan-img" src="../../../showroom/demo/ph-4.webp" alt="">
        <div class="fan-veil"></div>
        <span class="fan-num">02</span>
        <div class="fan-vert">Planejamento<br>sucessório</div>
        <div class="fan-full">
          <h3 class="fan-title">Planejamento sucessório</h3>
          <p class="fan-text">Estruturamos seu patrimônio de forma eficiente e segura, garantindo que o legado seja protegido e transferido às futuras gerações.</p>
        </div>
      </article>

      <article class="fan-panel" tabindex="0">
        <img class="fan-img" src="../../../showroom/demo/ph-6.webp" alt="">
        <div class="fan-veil"></div>
        <span class="fan-num">03</span>
        <div class="fan-vert">Multicustódia</div>
        <div class="fan-full">
          <h3 class="fan-title">Multicustódia</h3>
          <p class="fan-text">Atuamos com total independência, oferecendo consultoria personalizada na instituição financeira de sua preferência.</p>
        </div>
      </article>

      <article class="fan-panel" tabindex="0">
        <img class="fan-img" src="../../../showroom/demo/ph-7.webp" alt="">
        <div class="fan-veil"></div>
        <span class="fan-num">04</span>
        <div class="fan-vert">Investimentos<br>globais</div>
        <div class="fan-full">
          <h3 class="fan-title">Investimentos globais</h3>
          <p class="fan-text">Diversificação internacional com estruturas offshore e trusts para proteger e multiplicar seu patrimônio em mercados globais.</p>
        </div>
      </article>

      <article class="fan-panel" tabindex="0">
        <img class="fan-img" src="../../../showroom/demo/ph-8.webp" alt="">
        <div class="fan-veil"></div>
        <span class="fan-num">05</span>
        <div class="fan-vert">Consultoria<br>empresarial</div>
        <div class="fan-full">
          <h3 class="fan-title">Consultoria empresarial</h3>
          <p class="fan-text">Atendimento completo para sócios e empresas: eficiência tributária, mercado de capitais, crédito, câmbio e gestão de caixa.</p>
        </div>
      </article>
    </div>
  </div>
</section>


<!-- ==========================================================================
     VARIAÇÃO — MODO DEPOIMENTO, com rodízio
     Cole ESTA seção OU a de cima. As duas juntas na mesma página funcionam
     (o script parte de cada [data-fan]), mas editorialmente são a mesma
     forma duas vezes.

     O QUE MUDA NO MARKUP, e só isto:
       - .fan-depo entra ao lado de .fan, no bloco raiz;
       - [data-fan] sobe para um invólucro .fan-box, porque agora existe um
         rodapé (a ressalva e os controles) que precisa estar DENTRO da raiz
         para o script encontrá-lo. Sem rodapé, [data-fan] continua podendo
         morar no próprio .fan — é o que a seção de cima faz;
       - .fan-num vira .fan-sign (o +/−) e o número passa para .fan-count,
         dentro do painel aberto, como contador de posição;
       - .fan-full troca título+frase por aspas, citação, nome, relação e
         contexto.

     O RODÍZIO É OPT-IN: sem data-fan-auto a seção fica igual à de cima em
     comportamento — abre por hover, clique, foco e seta, e mais nada anda.
     Com ele, o leque troca sozinho a cada --fan-auto (6s no padrão) e para
     em quatro situações: aba oculta, dobra fora da tela, ponteiro sobre um
     painel, foco dentro de um painel.

     OS TRÊS BOTÕES NASCEM [hidden] dentro de uma barra [hidden]: sem
     JavaScript nenhum deles comanda coisa alguma, e controle morto é pior
     que controle ausente. Quem mostra a barra é o script; quem mostra o
     botão de pausa é o LIB.pausavel, e com movimento reduzido ele não é
     chamado — não há rodízio para pausar.

     COMPLIANCE, e é a parte que não é decoração. Depoimento em site de
     assessoria tem três travas, e as três estão nesta seção:
       1. o retrato é de banco de imagem / gerado por IA e SE DECLARA na
          peça, com a primitiva .nota do core, colada ao leque — não no
          rodapé da página, onde ninguém lê;
       2. relato ilustrativo NÃO é depoimento de cliente, e o texto da
          ressalva diz isso com essas palavras;
       3. nenhuma citação cita rentabilidade, retorno, percentual ou valor
          de patrimônio. Todas falam de processo: clareza, organização,
          sucessão, o "não" que alguém ouviu. É inofensivo por construção —
          trocar o nome por um cliente real continuaria sendo publicável.
     A ficha tem a seção Compliance inteira, com o que exigir de um
     depoimento REAL antes de publicar.
     ========================================================================== -->

<section class="sec-dark sec-pad" id="historias">
  <div class="wrap">
    <span class="eyebrow" data-rv>Histórias de quem é assessorado</span>
    <h2 class="sec-title ttl">O que muda quando alguém <em>organiza</em> a conversa</h2>

    <div class="fan-box" data-fan data-fan-auto data-rv data-d="1">
      <div class="fan fan-depo">

        <article class="fan-panel is-open" tabindex="0">
          <img class="fan-img" src="../../../showroom/demo/ph-3.webp" alt="">
          <div class="fan-veil"></div>
          <span class="fan-sign" aria-hidden="true"></span>
          <div class="fan-vert">Marina A.</div>
          <div class="fan-full">
            <div class="fan-top">
              <span class="fan-mark" aria-hidden="true">&ldquo;</span>
              <span class="fan-count">01 / 04</span>
            </div>
            <blockquote class="fan-say">Eu tinha conta em quatro lugares e nenhuma ideia do conjunto. A primeira coisa que fizeram foi juntar tudo numa página só.</blockquote>
            <p class="fan-who">Marina A.<span class="fan-rel">54 anos — família em segunda geração</span></p>
            <p class="fan-ctx">Sobre a primeira reunião de diagnóstico.</p>
          </div>
        </article>

        <article class="fan-panel" tabindex="0">
          <img class="fan-img" src="../../../showroom/demo/ph-4.webp" alt="">
          <div class="fan-veil"></div>
          <span class="fan-sign" aria-hidden="true"></span>
          <div class="fan-vert">Otávio R.</div>
          <div class="fan-full">
            <div class="fan-top">
              <span class="fan-mark" aria-hidden="true">&ldquo;</span>
              <span class="fan-count">02 / 04</span>
            </div>
            <blockquote class="fan-say">Meu medo era a sucessão virar briga. Sentaram com o advogado e o contador na mesma mesa e explicaram cada caminho, inclusive os que não recomendavam.</blockquote>
            <p class="fan-who">Otávio R.<span class="fan-rel">61 anos — sócio de indústria</span></p>
            <p class="fan-ctx">Sobre a montagem da holding familiar.</p>
          </div>
        </article>

        <article class="fan-panel" tabindex="0">
          <img class="fan-img" src="../../../showroom/demo/ph-6.webp" alt="">
          <div class="fan-veil"></div>
          <span class="fan-sign" aria-hidden="true"></span>
          <div class="fan-vert">Helena V.</div>
          <div class="fan-full">
            <div class="fan-top">
              <span class="fan-mark" aria-hidden="true">&ldquo;</span>
              <span class="fan-count">03 / 04</span>
            </div>
            <blockquote class="fan-say">Eu queria alguém que soubesse me dizer não. Foi a primeira vez que ouvi &ldquo;esse produto não serve para você&rdquo; de quem estava do outro lado da mesa.</blockquote>
            <p class="fan-who">Helena V.<span class="fan-rel">47 anos — médica, consultório próprio</span></p>
            <p class="fan-ctx">Sobre a revisão anual da carteira.</p>
          </div>
        </article>

        <article class="fan-panel" tabindex="0">
          <img class="fan-img" src="../../../showroom/demo/ph-7.webp" alt="">
          <div class="fan-veil"></div>
          <span class="fan-sign" aria-hidden="true"></span>
          <div class="fan-vert">Rui C.</div>
          <div class="fan-full">
            <div class="fan-top">
              <span class="fan-mark" aria-hidden="true">&ldquo;</span>
              <span class="fan-count">04 / 04</span>
            </div>
            <blockquote class="fan-say">O que mudou não foi o que eu tenho, foi o quanto eu entendo. Hoje eu explico a minha carteira para os meus filhos sem precisar consultar ninguém.</blockquote>
            <p class="fan-who">Rui C.<span class="fan-rel">68 anos — aposentado, ex-servidor público</span></p>
            <p class="fan-ctx">Sobre a conversa de transição com os herdeiros.</p>
          </div>
        </article>

      </div>

      <div class="fan-foot">
        <p class="nota fan-nota">Relatos e retratos ilustrativos, criados para demonstrar a peça — as imagens são de banco de imagem e não retratam clientes. Não são depoimentos de clientes da casa.</p>

        <div class="fan-ctl" data-fan-ctl hidden>
          <button class="btn-disc" type="button" data-fan-prev aria-label="Depoimento anterior">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
              <path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
          </button>

          <button class="btn-disc btn-play" type="button" data-fan-play hidden
                  data-rot-pausar="Pausar a troca de depoimentos" data-rot-tocar="Retomar a troca de depoimentos"
                  aria-label="Pausar a troca de depoimentos">
            <svg class="btn-play-pausa" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
              <path d="M9 5v14M15 5v14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
            </svg>
            <svg class="btn-play-tocar" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
              <path d="M8 5.5v13l11-6.5z" fill="currentColor"/>
            </svg>
          </button>

          <button class="btn-disc" type="button" data-fan-next aria-label="Próximo depoimento">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
              <path d="M9 5l7 7-7 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
          </button>
        </div>
      </div>
    </div>
  </div>
</section>

estilo.css · 287 linhas · 12,2 KB

/* ==========================================================================
   LEQUE — painéis verticais que expandem
   Prefixo: fan-
   Tokens usados: --accent --dark --dark-rgb --display --sans --radius-media
                  --ease --radius-pill --fg-muted

   O painel vive sobre foto escura, então o texto de dentro é sempre claro,
   independente do escopo de tom da seção. Por isso usa --light (branco puro) e não
   --fg: o leque é uma ilha escura dentro de uma seção que pode ser clara.

   PELO MESMO MOTIVO O REALCE LÊ --accent E NÃO --accent-ink. O §3 do contrato
   manda o texto de destaque ler --accent-ink, e a regra está certa para texto
   que vive no fundo da seção: em escopo claro o accent puro não fecha
   contraste. Só que aqui o texto NÃO vive no fundo da seção — vive sobre foto
   em grayscale(1) brightness(.45), que é escuro em qualquer tema. Uma dobra
   .sec-paper entregaria --accent-ink já misturado com a tinta CLARA do escopo,
   e o número sairia lavado sobre a foto. Quem está fora dos painéis (a barra
   de controles e a ressalva) lê os tokens do escopo normalmente, porque esses
   sim moram no fundo da seção.

   DOIS MODOS, UM MOTOR. O leque nasceu para SERVIÇO (número, rótulo vertical,
   título e uma frase de benefício) e ganhou o modo DEPOIMENTO em 2026-09 —
   .fan-depo no bloco raiz. A diferença é de conteúdo e de medida, nunca de
   mecânica: a expansão, o hover, o clique, o foco e as setas são exatamente os
   mesmos. Tudo o que o depoimento precisa de diferente sai de parafuso do
   próprio componente (--fan-open, --fan-full-w, --fan-say), e é por isso que
   ele não virou um segundo componente.
   ========================================================================== */

.fan{
  /* -------- parafusos do componente (§3: custom property própria é livre) --
     --fan-open    flex do painel aberto. 3.4 é o valor de 5 painéis de
                   serviço; com 4 painéis 4.2 fica melhor, com 6, 3.
     --fan-full-w  largura FIXA do bloco de texto do painel aberto
     --fan-auto    período do rodízio, lido pelo script quando há data-fan-auto
     --fan-say     corpo da citação, só usado no modo depoimento
     Antes de existirem, "abrir mais" e "citação maior" só se conseguiam
     editando o componente — que é exatamente o que o §10.5 proíbe. */
  --fan-open:3.4;
  --fan-full-w:min(440px,84vw);
  --fan-auto:6s;

  display:flex;gap:8px;
  height:min(62vh,520px);
  margin-top:clamp(38px,6vh,60px);
}

.fan-panel{
  position:relative;flex:1;min-width:0;
  border-radius:var(--radius-media);
  overflow:hidden;cursor:pointer;
  /* 1.1s é o que faz a expansão ler como "leque" e não como "clique".
     Abaixo de 0.8s vira estalo; acima de 1.4s parece travado. */
  transition:flex 1.1s cubic-bezier(.19,.85,.24,1);
}
.fan-panel.is-open{flex:var(--fan-open)}
.fan-panel:focus-visible{outline:2px solid var(--accent);outline-offset:-4px}

.fan-img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  filter:grayscale(1) brightness(.45);
  transition:filter 1.1s;
}
.fan-panel.is-open .fan-img{filter:grayscale(.12) brightness(.6)}
/* O painel fechado sob o ponteiro só ACENDE um pouco, para dizer que é
   clicável. Não abre e não muda de tamanho: hover não mexe na imagem
   (CLAUDE.md §6). Quem abre é o clique. */
.fan-panel:not(.is-open):hover .fan-img{filter:grayscale(.7) brightness(.56)}

/* Movimento reduzido: a troca continua acontecendo — ela é resposta a um
   gesto pedido (hover, clique, seta), e não movimento imposto —, mas sem o
   1,1s de deslize. O painel troca de largura no quadro seguinte. Quem pediu
   menos movimento não perde a peça, perde a coreografia. Colada às regras que
   desliga, como manda o §6.6. */
@media(prefers-reduced-motion:reduce){
  .fan-panel,.fan-img{transition:none}
}

/* Véu de vidro só no painel aberto, para o texto longo ter contraste.
   A máscara em gradiente é obrigatória: sem ela o backdrop-filter corta
   reto no meio da foto e lê "chapado". */
.fan-veil{
  position:absolute;left:0;right:0;bottom:0;height:56%;z-index:1;pointer-events:none;
  backdrop-filter:blur(8px) saturate(1.15);
  -webkit-backdrop-filter:blur(8px) saturate(1.15);
  -webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.9) 44%,transparent 100%);
  mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.9) 44%,transparent 100%);
  opacity:0;transition:opacity .5s;
}
.fan-panel.is-open .fan-veil{opacity:1}

/* Escurecimento permanente da base — vale para painel aberto e fechado. */
.fan-panel::after{
  content:"";position:absolute;inset:0;z-index:1;
  background:linear-gradient(to top,
    rgba(var(--dark-rgb),.94) 0%,
    rgba(var(--dark-rgb),.78) 22%,
    rgba(var(--dark-rgb),.3) 52%,
    transparent 74%);
}

.fan-num{
  position:absolute;top:20px;left:22px;z-index:2;
  font-size:11px;letter-spacing:.18em;color:var(--accent);
}

/* Rótulo do painel FECHADO, girado. No modo serviço é o nome do serviço; no
   modo depoimento é o nome de quem fala. */
.fan-vert{
  position:absolute;left:0;bottom:0;z-index:2;padding:22px;
  writing-mode:vertical-rl;transform:rotate(180deg);
  font-family:var(--display);font-weight:700;
  font-size:14px;line-height:1.35;letter-spacing:-.01em;
  color:var(--light);transition:opacity .4s;
}
.fan-panel.is-open .fan-vert{opacity:0}

/* Conteúdo do painel aberto.
   Largura FIXA, não porcentagem: sem isso o texto reflui enquanto o painel
   encolhe e a saída fica sanfonada. */
.fan-full{
  position:absolute;left:0;bottom:0;z-index:2;
  padding:clamp(22px,2.6vw,34px);
  width:var(--fan-full-w);
  opacity:0;pointer-events:none;
  transition:opacity .18s ease;                 /* some rápido */
}
.fan-panel.is-open .fan-full{
  opacity:1;
  transition:opacity .5s .22s;                  /* entra devagar e atrasado */
}
.fan-title{
  font-family:var(--display);font-weight:700;
  font-size:clamp(20px,2.4vw,27px);line-height:1.2;letter-spacing:-.025em;
  color:var(--light);margin:0 0 12px;
}
.fan-text{font-size:15px;line-height:1.7;
  color:color-mix(in srgb,var(--light) 86%,transparent);max-width:40ch}

/* ==========================================================================
   MODO DEPOIMENTO — .fan-depo no bloco raiz
   Mesmo motor, outro conteúdo. Três medidas mudam e todas são parafuso. Os
   números abaixo saíram do Chrome headless a 1440×900, com os quatro
   depoimentos deste markup:

   --fan-full-w 560 (contra 440). É ELE que manda na medida da citação, e não
   o --fan-open: a largura do .fan-full é fixa de propósito (ver o gotcha do
   refluxo). Com 440 a coluna útil dá 372px, ou 24ch de corpo display — a
   citação quebra em 4 linhas de meia dúzia de palavras e lê como poema. Com
   560 dá 492px, 32ch, 3 linhas.

   --fan-open 4.6 (contra 3.4) é CONSEQUÊNCIA da linha de cima, não gosto. Com
   4 painéis e 3.4 o painel aberto mede 570px, e um bloco de texto de 560
   dentro dele sobra 10px de foto à direita: a citação encosta na borda e o
   painel deixa de parecer uma foto com legenda para parecer uma caixa de
   texto. 4.6 dá 649px e devolve 89px de imagem ao lado do texto.

   --fan-say é o corpo da citação, que NÃO é o corpo do .fan-text. Depoimento
   é a voz de alguém e pede tipografia de fala, não de parágrafo de apoio.
   ========================================================================== */
.fan-depo{
  --fan-open:4.6;
  --fan-full-w:min(560px,86vw);
  --fan-say:clamp(18px,2vw,24px);
}

/* Linha de topo do painel aberto: aspas à esquerda, contador à direita. Em
   fluxo normal, não absoluto — o contador absoluto precisava de um conserto
   de posição em cada media query e não ganhava nada com isso. */
.fan-top{
  display:flex;align-items:flex-start;justify-content:space-between;
  gap:16px;margin-bottom:12px;
}

/* As aspas são DESENHO, não texto: aria-hidden no markup, porque um leitor de
   tela que anuncia "aspa esquerda" antes de cada citação vira ruído. */
.fan-mark{
  font-family:var(--display);font-weight:700;
  font-size:52px;line-height:.62;
  color:var(--accent);
}

/* Contador 01 / 04. Escrito no markup, não pelo script: sem JavaScript a peça
   continua dizendo quantos depoimentos existem. */
.fan-count{
  flex:0 0 auto;padding-top:6px;
  font-size:11px;letter-spacing:.18em;color:var(--accent);
}

.fan-say{
  margin:0 0 18px;
  font-family:var(--display);font-weight:400;
  font-size:var(--fan-say);line-height:1.34;letter-spacing:-.02em;
  color:var(--light);
}

/* Nome e relação empilhados. O nome é a âncora da citação; a relação é o que
   a torna legível ("Família — segunda geração"), e não um cargo. */
.fan-who{display:block;font-size:14px;line-height:1.4;color:var(--light)}
.fan-rel{
  display:block;margin-top:3px;
  font-size:12.5px;letter-spacing:normal;
  color:color-mix(in srgb,var(--light) 62%,transparent);
}

/* A linha de contexto. Uma frase, e é ela que diz DE QUE conversa a citação
   saiu — sem isso o depoimento fica no ar. */
.fan-ctx{
  margin-top:12px;
  font-size:13px;line-height:1.6;
  color:color-mix(in srgb,var(--light) 70%,transparent);
  max-width:44ch;
}

/* O sinal +/− do painel fechado, no lugar em que o modo serviço põe o número.
   Duas barras desenhadas: a vertical encolhe a zero quando o painel abre, e o
   + vira −. É transform em pseudo-elemento, que nunca encosta em [data-rv]. */
.fan-sign{
  position:absolute;top:18px;left:20px;z-index:2;
  width:16px;height:16px;pointer-events:none;
}
.fan-sign::before,
.fan-sign::after{
  content:"";position:absolute;left:0;top:50%;
  width:100%;height:1.5px;margin-top:-.75px;
  background:var(--light);
  transition:transform .45s var(--ease);
}
.fan-sign::after{transform:rotate(90deg)}
.fan-panel.is-open .fan-sign::after{transform:rotate(90deg) scaleX(0)}
@media(prefers-reduced-motion:reduce){
  .fan-sign::before,.fan-sign::after{transition:none}
}

/* ==========================================================================
   INVÓLUCRO, RESSALVA E CONTROLES
   .fan-box só existe quando há rodapé (ressalva, controles). O leque sem
   rodízio continua podendo ser a própria raiz [data-fan] — é o markup de
   serviço de hoje, e ele não muda em nada.
   ========================================================================== */
.fan-box{display:flex;flex-direction:column}

.fan-foot{
  display:flex;align-items:flex-start;justify-content:space-between;
  gap:20px;margin-top:18px;
}

/* A ressalva veste a primitiva .nota do core, que traz só a tipografia.
   Margem e medida são posição, e posição é local — é o contrato da .nota. */
.fan-nota{max-width:52ch}

.fan-ctl{display:flex;gap:8px;flex:0 0 auto}
/* §6.5: quem dá display a algo que às vezes é [hidden] desliga à mão. A barra
   nasce escondida porque sem JavaScript os três botões não comandam nada. */
.fan-ctl[hidden]{display:none}
.fan-ctl svg{width:15px;height:15px}

/* ---- mobile ----
   Vira acordeão horizontal: a barra de 96px abre para 290px. O título
   deixa de ser vertical porque em 96px de altura não há eixo para girar. */
@media(max-width:900px){
  .fan{flex-direction:column;height:auto}
  .fan-panel{flex:0 0 96px;height:96px}
  .fan-panel.is-open{flex:0 0 290px;height:290px}
  .fan-vert{writing-mode:horizontal-tb;transform:none}
  .fan-full{width:100%}

  /* Depoimento no celular: o painel aberto precisa de mais altura que os
     290px do modo serviço — são aspas, citação, nome, relação e contexto
     empilhados, contra título e uma frase. Medido no Chrome headless com os
     quatro depoimentos deste markup: o bloco de texto mais alto dá 251px a
     760 de largura e 293px a 390. 380px é o menor número que deixa os dois
     inteiros e ainda sobra foto — a 390 sobram 87px dela. */
  .fan-depo .fan-panel.is-open{flex:0 0 380px;height:380px}
  /* O rótulo do painel fechado deixa espaço para o sinal, que no horizontal
     passa a dividir a mesma linha que o nome. */
  .fan-depo .fan-vert{padding-left:48px}
  .fan-sign{top:auto;bottom:24px}
  .fan-foot{flex-direction:column;align-items:stretch;gap:14px}
  .fan-ctl{justify-content:flex-start}
}

@media(max-width:560px){
  /* A citação encolhe em vez de o painel crescer: 24px de corpo numa coluna
     de 342px quebraria a frase em oito linhas curtas. */
  .fan-depo{--fan-say:17px}
}

script.js · 273 linhas · 11,7 KB

/* ==========================================================================
   LEQUE — painéis verticais que expandem
   Um painel aberto por vez, por clique, toque, foco ou seta do teclado.
   NÃO POR HOVER, desde 2026-09-29 (CLAUDE.md §6): abrir sob o ponteiro fazia
   a foto crescer e a fileira inteira se rearranjar só porque o mouse passou
   por cima a caminho de outra coisa. Agora a pessoa escolhe o painel.
   Suporta mais de um leque na mesma página: parte da raiz [data-fan],
   nunca do documento.

   O RODÍZIO É OPCIONAL E OPT-IN. Sem data-fan-auto na raiz este script faz
   hoje exatamente o que fazia antes de 2026-09: abre, fecha e anda no
   teclado. Nada do relógio liga, nenhum controle é procurado, e um markup de
   serviço colado em 2026-08 continua valendo linha por linha.

   COM data-fan-auto o leque troca de painel sozinho a cada --fan-auto, lido
   do CSS, e passa a precisar de três coisas que não são negociáveis:

     1. um jeito explícito de PARAR (WCAG 2.2.2). É a primitiva .btn-play do
        core, ligada por LIB.pausavel — o script não escreve rótulo nenhum,
        a copy dos dois estados mora no markup;
     2. pausa automática em todo momento em que o movimento atrapalharia:
        aba oculta, dobra fora da tela, ponteiro sobre um painel, foco dentro
        de um painel. Retoma de onde parou;
     3. o ícone do botão nunca discordando do relógio. Toda pausa automática
        passa por ctl.trocar(), que é a porta que o core abre justamente para
        isso — um leque congelado com ícone de "pausar" na tela seria um
        botão mentindo sobre o próprio estado.

   MOVIMENTO REDUZIDO: não há rodízio. O relógio não liga, LIB.pausavel NÃO é
   chamado e o botão de pausa fica [hidden] como nasceu — botão que não
   comanda nada é pior que botão nenhum. As setas anterior/próximo continuam,
   porque troca PEDIDA por alguém não é movimento imposto a ninguém.
   Precedente: hero/slideshow-cinematico.

   SEM LIB.pausavel (componente colado sem o core/motion.js) também não há
   rodízio, e a decisão é a mesma da linha de cima vista pelo outro lado: o
   relógio só pode andar onde existe o controle que o para.
   ========================================================================== */

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

  /* A flag do core é a fonte. O fallback existe para o componente colado
     sozinho, sem o 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;
  }

  /* "6s" e "600ms" viram milissegundos. getComputedStyle devolve o valor do
     jeito que foi escrito, com o espaço da declaração — daí o replace. */
  function emMs(txt, padrao) {
    if (!txt) return padrao;
    txt = ('' + txt).replace(/\s/g, '');
    var n = parseFloat(txt);
    if (isNaN(n) || n <= 0) return padrao;
    return txt.indexOf('ms') > -1 ? n : n * 1000;
  }

  function iniciar(raiz) {
    var paineis = [].slice.call(raiz.querySelectorAll('.fan-panel'));
    if (!paineis.length) return;

    var n = paineis.length;

    /* Índice do painel aberto, lido do DOM: trocar a ordem no HTML não pode
       quebrar a primeira pintura. */
    var atual = 0, i;
    for (i = 0; i < n; i++) if (paineis[i].classList.contains('is-open')) { atual = i; break; }

    /* Se ninguém marcou .is-open no HTML, abre o primeiro: a seção não pode
       nascer com todos os painéis fechados. */
    if (!raiz.querySelector('.fan-panel.is-open')) paineis[0].classList.add('is-open');

    /* ------------------------------------------------------------ abrir */

    function abrir(painel) {
      if (painel.classList.contains('is-open')) return;
      for (var k = 0; k < n; k++) {
        paineis[k].classList.remove('is-open');
        if (paineis[k] === painel) atual = k;
      }
      painel.classList.add('is-open');
      reiniciarRelogio();
    }

    /* Índice circular: -1 vira o último, n vira o primeiro. */
    function ir(idx) { abrir(paineis[(idx % n + n) % n]); }

    paineis.forEach(function (painel, i) {
      /* O ponteiro sobre um painel PAUSA o rodízio, e só isso: não abre.
         Hover não mexe em nada (CLAUDE.md §6, 2026-09-29) — abrir sob o
         ponteiro fazia a foto crescer e a fileira inteira se rearranjar só
         porque o mouse passou por cima a caminho de outra coisa. Quem abre é
         o clique, o toque, o foco e a seta. */
      painel.addEventListener('mouseenter', entrou);
      painel.addEventListener('mouseleave', saiu);
      painel.addEventListener('click', function () { abrir(painel); });

      /* Teclado: Tab percorre os painéis e o foco já abre — o foco é uma
         escolha, ao contrário do ponteiro de passagem. Setas andam de painel
         em painel sem sair do componente. */
      painel.addEventListener('focus', function () { abrir(painel); });
      painel.addEventListener('keydown', function (e) {
        var next = null;
        if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = paineis[i + 1];
        if (e.key === 'ArrowLeft'  || e.key === 'ArrowUp')   next = paineis[i - 1];
        if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); abrir(painel); return; }
        if (next) { e.preventDefault(); next.focus(); }
      });
    });

    /* ====================================================================
       RODÍZIO — daqui para baixo nada roda sem data-fan-auto.
       ==================================================================== */

    /* Estado do relógio. Declarado ANTES dos listeners acima porque eles o
       tocam: com var isso é içamento (hoisting) e funciona, mas quem lê
       precisa saber que é de propósito, e não descuido. */
    var timer = 0, t0 = 0, restante = 0, dur = 0;
    var ctl = null;               /* o controle devolvido por LIB.pausavel */
    var auto = raiz.hasAttribute('data-fan-auto');
    var reduz = reduzido();
    var roda = false;             /* o rodízio de fato existe nesta instância */
    var manual = false;           /* pausa pedida pela pessoa, no botão */
    var oculto = !!document.hidden;
    var naTela = true;
    var sobre = 0;                /* quantos painéis estão sob o ponteiro */
    var sobPonteiro = false;
    var comFoco = false;
    var porFora = false;          /* estamos dentro do nosso próprio trocar() */

    function podeCorrer() {
      return roda && !manual && !oculto && naTela && !sobPonteiro && !comFoco;
    }

    /* Guarda o que FALTA, não quanto já passou: é isso que faz a retomada
       continuar de onde parou em vez de devolver o período inteiro a cada
       passagem de ponteiro. */
    function parar() {
      if (!timer) return;
      clearTimeout(timer);
      timer = 0;
      restante = Math.max(0, restante - (Date.now() - t0));
    }

    function correr() {
      if (timer || !podeCorrer()) return;
      t0 = Date.now();
      timer = setTimeout(bater, restante);
    }

    function reiniciarRelogio() {
      if (!roda) return;
      if (timer) { clearTimeout(timer); timer = 0; }
      restante = dur;
      correr();
    }

    function bater() { timer = 0; ir(atual + 1); }

    /* O ícone tem de mostrar o estado REAL do relógio, não só a última coisa
       que a pessoa clicou. Toda pausa automática entra por aqui.

       porFora é a trava de reentrada: ctl.trocar() chama de volta o nosso
       callback no mesmo quadro, e sem a trava o callback leria a pausa
       automática como se fosse um clique e gravaria manual=true — bastaria
       passar o ponteiro uma vez para o leque ficar travado para sempre. */
    function sincronizarIcone() {
      if (!ctl) return;
      var parado = !podeCorrer();
      if (ctl.pausado() === parado) return;
      porFora = true;
      ctl.trocar(parado);
      porFora = false;
    }

    function atualizar() {
      if (!roda) return;
      if (podeCorrer()) correr(); else parar();
      sincronizarIcone();
    }

    function entrou() { sobre++; sobPonteiro = true; atualizar(); }
    function saiu() {
      sobre--;
      if (sobre > 0) return;
      sobre = 0; sobPonteiro = false; atualizar();
    }

    function ehPainel(el) {
      for (var k = 0; k < n; k++) if (paineis[k] === el) return true;
      return false;
    }

    /* Foco DENTRO de um painel pausa; foco nos três botões da barra, não.
       A distinção não é preciosismo: a barra fica fora do leque justamente
       para que dar foco ao botão de pausa não pause sozinho — se pausasse, o
       botão "retomar" jamais retomaria, porque o próprio foco nele seguraria
       o relógio parado.

       focusin/focusout na RAIZ, e não focus/blur em cada painel: andando de
       painel em painel com Tab, o blur do primeiro chega antes do focus do
       segundo, e o par soltava e segurava o relógio a cada tecla — o ícone
       piscava duas vezes por Tab. relatedTarget diz quem VAI receber o foco,
       e continuar num painel não é sair de todos. */
    raiz.addEventListener('focusin', function (e) {
      comFoco = ehPainel(e.target);
      atualizar();
    });
    raiz.addEventListener('focusout', function (e) {
      if (e.relatedTarget && ehPainel(e.relatedTarget)) return;
      comFoco = false;
      atualizar();
    });

    /* ------------------------------------------------------------ controles */

    /* Guard clause do §7.2: a barra pode simplesmente não existir. O markup de
       serviço não tem nenhum dos três botões, e procurar por eles não pode
       virar erro nem condição para o resto funcionar. */
    var barra = raiz.querySelector('[data-fan-ctl]');
    var btAnt = raiz.querySelector('[data-fan-prev]');
    var btPro = raiz.querySelector('[data-fan-next]');
    var btPlay = raiz.querySelector('[data-fan-play]');

    if (btAnt) btAnt.addEventListener('click', function () { ir(atual - 1); });
    if (btPro) btPro.addEventListener('click', function () { ir(atual + 1); });

    /* A barra nasce [hidden] e só aparece com script: anterior e próximo são
       tão inúteis sem JavaScript quanto o botão de pausa. Ela aparece mesmo
       com movimento reduzido — gesto pedido continua valendo. */
    if (barra) barra.hidden = false;

    if (!auto || reduz || !window.LIB || !window.LIB.pausavel || n < 2) return;

    var linha = raiz.classList.contains('fan') ? raiz : raiz.querySelector('.fan');
    dur = emMs(getComputedStyle(linha || raiz).getPropertyValue('--fan-auto'), 6000);
    restante = dur;
    roda = true;

    ctl = window.LIB.pausavel(btPlay, function (pausado) {
      /* Veio do nosso trocar(), não do dedo de ninguém: só o ícone mudou. */
      if (porFora) return;
      manual = pausado;
      atualizar();
    });
    /* Sem botão de pausa não há rodízio — LIB.pausavel devolve null e a
       WCAG 2.2.2 não estaria atendida. O leque volta a ser só manual. */
    if (!ctl) { roda = false; return; }

    /* Aba em segundo plano: o setTimeout continuaria correndo (o navegador só
       o afrouxa para no mínimo 1s) e a pessoa voltaria três painéis adiante
       sem ter visto nenhum. */
    document.addEventListener('visibilitychange', function () {
      oculto = !!document.hidden;
      atualizar();
    });

    /* Dobra fora da tela: nada anda para ninguém. */
    var io = new IntersectionObserver(function (entradas) {
      for (var k = 0; k < entradas.length; k++) naTela = entradas[k].isIntersecting;
      atualizar();
    }, { threshold: 0 });
    io.observe(raiz);

    atualizar();
  }

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

Painéis de foto lado a lado. O ativo ocupa 3 a 5× a largura dos outros e revela o conteúdo inteiro; os fechados mostram só um rótulo girado na vertical. Clique, toque, foco ou seta trocam o painel aberto — o hover só acende o painel fechado, sem abri-lo.

Resolve o problema clássico de "cinco blocos iguais, nenhum lido": aqui só um bloco de texto existe por vez, então o olho tem um destino.

Dois modos, um motor:

Modo Classe O painel aberto carrega O fechado mostra
Serviço (original, 2026-08) — número, título e uma frase de benefício número + nome do serviço na vertical
Depoimento (2026-09) .fan-depo aspas, citação em corpo grande, nome, idade/relação, linha de contexto e contador NN / NN nome da pessoa na vertical + sinal +/−

A mecânica é a mesma nos dois: um aberto por vez, a mesma curva de 1,1s, o mesmo véu com máscara, o mesmo teclado. O que muda é conteúdo e três medidas, e as três são parafuso do próprio componente. O modo depoimento ainda aceita rodízio (data-fan-auto), com anterior / pausar / próximo.


Compliance

Depoimento em site de assessoria não é texto, é declaração. Esta seção não é recomendação: é o que tem de estar resolvido antes de a peça ir ao ar, e é por isso que ela vem antes de tudo na ficha.

1. Retrato ilustrativo tem de se declarar — na peça

Se o rosto do painel é gerado por IA ou de banco de imagem, a peça diz isso, com a primitiva .nota do core, colada ao leque:

<p class="nota fan-nota">Relatos e retratos ilustrativos, criados para demonstrar a peça — as imagens são de banco de imagem e não retratam clientes. Não são depoimentos de clientes da casa.</p>

Não vale no rodapé da página. A ressalva mora no .fan-foot, no mesmo bloco visual do leque, e é isso que faz dela parte da leitura em vez de letra miúda jurídica. Quem rola até o depoimento lê a ressalva junto; quem vai embora antes do rodapé nunca a veria.

2. Relato ilustrativo não é depoimento de cliente

Texto escrito pela agência para mostrar a forma não pode ser apresentado como fala de alguém. Duas travas no markup de exemplo, e as duas são exigíveis em produção:

3. Depoimento real tem três exigências

Exigência Por quê
Autorização de uso de imagem e de voz, assinada, guardada e com prazo o rosto e a frase são dados pessoais; publicar sem autorização é exposição, não prova
Nenhuma menção a rentabilidade, retorno, percentual ou valor de patrimônio fala de cliente sobre resultado é publicidade de resultado, e resultado passado não é promessa de futuro — nem quando quem diz é o cliente
A relação declarada (é cliente? há quanto tempo? recebeu algo por isso?) depoimento pago ou de parte relacionada sem declaração é propaganda disfarçada

4. O exemplo é inofensivo por construção

As quatro citações deste markup foram escritas para continuarem publicáveis mesmo se fossem verdadeiras. Todas falam de processo — clareza, organização, sucessão, o "não" que alguém ouviu do outro lado da mesa — e nenhuma cita número, produto, percentual ou patrimônio. Ao substituir por texto real, esse é o teste: se esta frase fosse de um cliente identificado, ela poderia ir ao ar? Se a resposta depende de a frase ser fictícia, a frase está errada.

Nada de marca de terceiro. Nem banco, nem corretora, nem gestora — nem na citação, nem na linha de contexto, nem no .fan-rel.


Quando usar

Quando não usar

O irmão mais próximo, e quando ele ganha: prova/carrossel-depoimentos. Ele é uma esteira que não para, com cards de citação e retrato pequeno — e ganha quando os depoimentos são muitos e curtos, quando não há foto grande por pessoa, e quando a dobra quer afirmar "há muita gente aqui" sem obrigar ninguém a escolher. O leque ganha no caso oposto: poucos relatos, cada um com um retrato que merece ocupar meia dobra, e a leitura de um por vez como parte do argumento. Regra curta: se o rosto é conteúdo, leque; se o volume é o conteúdo, esteira.


Padrão editorial

Comum aos dois modos

Slot Regra
Olho 2 a 4 palavras: "O que oferecemos", "Histórias de quem é assessorado".
Título da seção 1 linha com uma palavra em <em>.
.fan-vert O rótulo do painel fechado. No modo serviço, o nome do serviço em 2 a 3 palavras, quebrado com <br> em até 2 linhas; no modo depoimento, o nome da pessoa, 1 a 2 palavras, sem <br>. Se não couber na vertical, o nome é longo demais.

Só no modo serviço

Slot Regra
.fan-num Sequência 01…05. Não pule número, não use ícone no lugar.
.fan-title Repete o nome do serviço por extenso, sem <br>.
.fan-text 1 frase, 18 a 30 palavras, teto de 40ch de largura. Diz o benefício, não o processo.

Só no modo depoimento

Slot Regra
.fan-say A citação. 18 a 34 palavras, 1 a 2 frases, na voz da pessoa. Compliance: nada de rentabilidade, retorno, percentual ou valor de patrimônio. Medido a 1440: a coluna dá 492px, ou 32ch de corpo display, e 25 palavras ocupam 3 linhas; acima de 34 palavras passa de 5 e a troca de painel fica pesada.
.fan-who O nome, igual ao do .fan-vert. Relato ilustrativo usa nome + inicial.
.fan-rel Idade e vínculo, até 44 caracteres: 54 anos — família em segunda geração. É o perfil, não o cargo, e nunca o nome de uma empresa real.
.fan-ctx Uma frase de 4 a 9 palavras dizendo de que conversa a citação saiu. Sem ela o relato fica no ar.
.fan-count NN / NN — posição e total, os dois com zero à esquerda. Escrito no markup, não pelo script.
.fan-nota A ressalva de origem. Obrigatória. Veste .nota do core (as duas classes no mesmo elemento). Ver Compliance.

Consistência é o que sustenta o efeito. Se um item tem 12 palavras e outro tem 40, a troca de painel fica desigual. Escreva os quatro a seis textos juntos, no mesmo fôlego.

Especificação das imagens


Como usar

<link rel="stylesheet" href="componentes/listas/leque-paineis/estilo.css">
<script src="componentes/listas/leque-paineis/script.js"></script>

O markup.html traz duas seções: o modo serviço e a variação de depoimento. Cole uma, nunca as duas — são a mesma forma com conteúdo diferente. (As duas juntas funcionam tecnicamente: o script parte de cada [data-fan] e mais de um leque na mesma página não conflita.)

Modo serviço — nada mudou

<div class="fan" data-fan>
  <article class="fan-panel is-open" tabindex="0">…</article>
  …
</div>

[data-fan] no próprio .fan, sem invólucro, sem controles. É o markup de 2026-08 linha por linha, e continua sendo o caminho padrão.

Modo depoimento, com rodízio

<div class="fan-box" data-fan data-fan-auto data-rv data-d="1">
  <div class="fan fan-depo">
    <article class="fan-panel is-open" tabindex="0">
      <img class="fan-img" …><div class="fan-veil"></div>
      <span class="fan-sign" aria-hidden="true"></span>
      <div class="fan-vert">Marina A.</div>
      <div class="fan-full">
        <div class="fan-top">
          <span class="fan-mark" aria-hidden="true">&ldquo;</span>
          <span class="fan-count">01 / 04</span>
        </div>
        <blockquote class="fan-say">…</blockquote>
        <p class="fan-who">Marina A.<span class="fan-rel">54 anos — …</span></p>
        <p class="fan-ctx">Sobre a primeira reunião de diagnóstico.</p>
      </div>
    </article>
    …
  </div>

  <div class="fan-foot">
    <p class="nota fan-nota">…a ressalva de compliance…</p>
    <div class="fan-ctl" data-fan-ctl hidden>
      <button class="btn-disc" data-fan-prev …>  <!-- ‹ -->
      <button class="btn-disc btn-play" data-fan-play hidden
              data-rot-pausar="…" data-rot-tocar="…">
      <button class="btn-disc" data-fan-next …>  <!-- › -->
    </div>
  </div>
</div>

Três coisas que valem a leitura:

  1. [data-fan] sobe para o .fan-box só porque agora existe um rodapé (ressalva + controles) que precisa estar dentro da raiz para o script encontrá-lo. Sem rodapé, [data-fan] continua podendo morar no próprio .fan.
  2. data-fan-auto é opt-in. Sem ele o modo depoimento se comporta igual ao de serviço: abre por clique, foco e seta, e nada anda sozinho.
  3. Os botões nascem [hidden] dentro de uma barra [hidden]. Quem mostra a barra é o script; quem mostra o botão de pausa é o LIB.pausavel do core.

Ganchos

Gancho Papel
[data-fan] raiz do leque — o .fan ou o .fan-box
[data-fan-auto] liga o rodízio. Ausente = nada anda sozinho
.fan-depo modo depoimento, no bloco raiz (.fan)
.fan-panel.is-open painel ativo — marque um no HTML
tabindex="0" no painel obrigatório: é o que dá navegação por teclado
[data-fan-ctl] a barra de controles; nasce [hidden]
[data-fan-prev] / [data-fan-next] anterior / próximo. Funcionam sempre, inclusive com movimento reduzido
[data-fan-play] o .btn-disc.btn-play do core. Nasce [hidden]; quem o mostra é o LIB.pausavel

Parafusos

Todos são custom property do próprio componente, declarados no bloco raiz — nenhum é token global (§3 do contrato). Sobrescreva-os no tema ou numa classe de página, nunca editando o componente.

Parafuso Padrão O que faz
--fan-open 3.4 (.fan-depo: 4.6) o flex do painel aberto. Com 4 painéis de serviço, 4.2; com 6, 3
--fan-full-w min(440px,84vw) (.fan-depo: min(560px,86vw)) a largura fixa do bloco de texto. É ele que manda na medida da linha
--fan-say clamp(18px,2vw,24px) o corpo da citação. Só usado no modo depoimento
--fan-auto 6s o período do rodízio, lido do CSS pelo script
--nota-size / --nota-max do core tipografia da ressalva

Variações


Gotchas


Movimento reduzido / toque

Com prefers-reduced-motion: reduce:

?motion=on na URL força tudo de volta, para gravar vídeo ou apresentar.

No toque (≤900px): o leque vira acordeão vertical. A barra fechada tem 96px e abre para 290px no modo serviço e 380px no depoimento — medido com os quatro relatos deste markup, o bloco de texto mais alto dá 251px a 760 de largura e 293px a 390, e 380 é o menor número que deixa os dois inteiros e ainda mostra foto. Abaixo de 560px a citação encolhe para 17px em vez de o painel crescer de novo. O rótulo deixa de ser vertical (em 96px de altura não há eixo para girar) e o sinal passa a dividir a linha com o nome, alinhado a ele com 1px de diferença. A barra de controles vira uma linha própria abaixo da ressalva.


Histórico

Projeto Onde Data Diferenças
Liberta Wealth index.html #solucoes 2026-08 Origem. Classes internas sem prefixo (.veil, .n, .vert, .full), .open em vez de .is-open, sem acesso por teclado, border-radius:10px cravado.
referência: skinboost-design-review.vercel.app dobra #historias — o leque de retratos com relato e rodízio 2026-09-21 Referência visual. Trouxe o modo depoimento (.fan-depo) e o rodízio opcional (data-fan-auto), sem tocar no modo serviço.

Normalizações aplicadas na extração (2026-08): prefixo em todas as classes internas; .open → .is-open (evita colidir com acordeão e drawer); raio cravado → --radius-media; escurecimento passou a ler --dark-rgb; sage cru → --accent; adicionado tabindex, :focus-visible, navegação por setas e Enter/Espaço; script passou a partir de [data-fan] em vez de .fan global, para suportar múltiplos leques.

O que a tradução de 2026-09-21 mudou. A referência preenche o mesmo leque com relato de pessoa em vez de serviço, e o faz trocar sozinho. Não virou componente novo porque o motor é idêntico — duplicar o CSS para mudar conteúdo seria manter duas cópias da mesma curva. Entrou como variação declarada, e tudo o que o modo pede de diferente saiu por parafuso do próprio componente (--fan-open 3.4 → 4.6, --fan-full-w 440 → 560, --fan-say novo), sem nenhum token global novo.

Divergência conhecida vs. produção: a Liberta Wealth rodou com border-radius:10px nos painéis, o que contraria a regra flat de canto vivo do próprio projeto. Aqui o valor vem do tema (--radius-media:0). Se for sincronizar a biblioteca de volta para o site, essa é uma mudança visual real — decida antes.