<!-- ==========================================================================
SLIDESHOW CINEMATOGRÁFICO — três fotos de tela cheia que trocam sozinhas
Prefixo: cine-
Requer: core (tokens.css, base.css, motion.js) + estilo.css e script.js
motion.js passou a ser NECESSÁRIO em 2026-09-21: é dele que vem o
LIB.pausavel, a fiação do botão de pausa. Sem ele a dobra ainda abre e
troca sozinha, mas o botão fica [hidden] — e a WCAG 2.2.2 pede que ele
exista. Nenhum gancho de ENTRADA do core é usado aqui — a dobra abre com
a página, não ao entrar na tela (ver "coreografia").
O QUE É. Um hero de tela cheia com três slides. Cada slide é uma foto
sangrada em zoom lento (Ken Burns), uma headline que entra palavra a
palavra e um botão próprio. No pé, uma barra de abas numeradas com uma
linha de progresso que enche no tempo do slide. Troca sozinho a cada 7s;
clicar numa aba, as setas do teclado ou arrastar no toque trocam na hora.
DUAS PILHAS, UMA SÓ VERDADE. A foto e o texto de um slide vivem em lugares
diferentes do DOM porque têm caixas diferentes: a foto sangra até a borda
(é filha absoluta de .cine-media) e o texto vive dentro do .wrap, que é a
única regra que controla a coluna (§4). O que amarra as duas é o ÍNDICE:
o script move .is-on pelo slide, pela cópia e pela aba de mesmo número.
Nenhuma margem negativa e nenhum 100vw.
ARIA: CARROSSEL COM ABAS. É o padrão "carousel with tabbed slide picker"
do W3C APG. A seção declara aria-roledescription="carrossel" e um nome;
a barra do pé é um role="tablist" de verdade; o painel de cada aba é a
CÓPIA, não a foto — o painel tem de ser o que se lê e o que se clica, e
a foto é o fundo que acompanha. A foto do slide inativo sai da árvore de
acessibilidade junto com a cópia dele.
SEM ID NO MARKUP. aria-controls e aria-labelledby precisam de id, e id
em markup de biblioteca colide na primeira página que repetir a dobra.
Quem cria os ids — únicos por instância — e amarra os dois atributos é o
script, na inicialização. O §2 do contrato pede exatamente isto: id só
quando for âncora de navegação da página.
SEM JS. O primeiro slide já nasce com .is-on, então a dobra abre inteira:
foto, olho, headline, apoio e botão. O que não existe sem script é a
troca — e por isso a barra de abas só aparece quando o script marca a
raiz com .is-live. Botão que não faz nada é pior que botão que não existe.
TRÊS FOTOS EM eager. As três entram no DOM e as três carregam de uma vez
(sem loading="lazy"): a fusão da primeira troca acontece 7s depois de
abrir, e imagem que começa a baixar na hora da troca pisca. O preço está
na ficha, com o teto de peso por foto.
========================================================================== -->
<sectionclass="cine sec-dark"data-cinearia-roledescription="carrossel"aria-label="Frentes de trabalho da Liberta">
<!-- MÍDIA. Os três slides ocupam a mesma caixa, empilhados. O grão é do
core e fica UMA vez, fora dos slides: ele é textura da dobra, não da
foto, e dentro de cada slide seria desenhado três vezes e esmaeceria
junto com a troca. -->
<divclass="cine-media">
<ulclass="cine-slides">
<liclass="cine-slide is-on"data-cine-slide>
<imgclass="cine-img"src="../../../showroom/demo/ph-wide-1.webp"alt="Casal caminhando por uma estrada de terra entre vinhedos, ao entardecer"style="--cine-pos:64% 46%"decoding="async">
<spanclass="cine-scrim"></span>
</li>
<liclass="cine-slide"data-cine-slidearia-hidden="true">
<imgclass="cine-img"src="../../../showroom/demo/ph-wide-3.webp"alt="Veleiro sozinho em mar aberto, sob céu carregado"style="--cine-pos:64% 54%"decoding="async">
<spanclass="cine-scrim"></span>
</li>
<liclass="cine-slide"data-cine-slidearia-hidden="true">
<imgclass="cine-img"src="../../../showroom/demo/ph-wide-2.webp"alt="Homem de terno observando a cidade pela janela de um escritório"style="--cine-pos:40% 44%"decoding="async">
<spanclass="cine-scrim"></span>
</li>
</ul>
<spanclass="grain"></span>
</div>
<divclass="wrap cine-body">
<!-- AS TRÊS CÓPIAS OCUPAM A MESMA CÉLULA DE GRID e são alinhadas pela
BASE: assim o botão de todos os slides cai na mesma linha e a dobra
não pula de altura a cada troca. A cópia inativa continua no fluxo,
invisível — é ela que reserva a altura da mais alta. -->
<divclass="cine-copies"data-cine-copies>
<divclass="cine-copy is-on"data-cine-copyrole="tabpanel"aria-roledescription="slide">
<spanclass="eyebrow cine-eyebrow">Planejamento sucessório</span>
<h1class="cine-title">
<spanclass="cine-w"style="--cine-i:0">A</span>
<spanclass="cine-w"style="--cine-i:1">sucessão</span>
<spanclass="cine-w"style="--cine-i:2">que</span>
<spanclass="cine-w"style="--cine-i:3">começa</span>
<spanclass="cine-w"style="--cine-i:4">antes</span>
<spanclass="cine-w"style="--cine-i:5">da</span>
<spanclass="cine-w"style="--cine-i:6">urgência.</span>
</h1>
<pclass="cine-desc">
Holding, testamento e um acordo que a família entende — feitos com
calma, enquanto ainda é escolha.
</p>
<divclass="cine-act">
<aclass="btn btn-primary"href="#diagnostico">Agende seu diagnóstico</a>
</div>
</div>
<divclass="cine-copy"data-cine-copyrole="tabpanel"aria-roledescription="slide"aria-hidden="true">
<spanclass="eyebrow cine-eyebrow">Estrutura internacional</span>
<h1class="cine-title">
<spanclass="cine-w"style="--cine-i:0">Patrimônio</span>
<spanclass="cine-w"style="--cine-i:1">no</span>
<spanclass="cine-w"style="--cine-i:2">exterior,</span>
<spanclass="cine-w"style="--cine-i:3">com</span>
<spanclass="cine-w"style="--cine-i:4">o</span>
<spanclass="cine-w"style="--cine-i:5">pé</span>
<spanclass="cine-w"style="--cine-i:6">no</span>
<spanclass="cine-w"style="--cine-i:7">chão.</span>
</h1>
<pclass="cine-desc">
Estrutura offshore, conta e custódia fora do país, com relatório
consolidado: a parte técnica resolvida antes da primeira remessa.
</p>
<divclass="cine-act">
<aclass="btn btn-primary"href="#internacional">Ver a estrutura</a>
</div>
</div>
<divclass="cine-copy"data-cine-copyrole="tabpanel"aria-roledescription="slide"aria-hidden="true">
<spanclass="eyebrow cine-eyebrow">Alocação independente</span>
<h1class="cine-title">
<spanclass="cine-w"style="--cine-i:0">Uma</span>
<spanclass="cine-w"style="--cine-i:1">carteira</span>
<spanclass="cine-w"style="--cine-i:2">que</span>
<spanclass="cine-w"style="--cine-i:3">atravessa</span>
<spanclass="cine-w"style="--cine-i:4">instituições.</span>
</h1>
<pclass="cine-desc">
Sem vínculo com bancos ou corretoras. A alocação é decidida por
objetivo e distribuída entre instituições — e o relatório chega
consolidado, numa página só.
</p>
<divclass="cine-act">
<aclass="btn btn-primary"href="#carteira">Falar com um assessor</a>
</div>
</div>
</div>
<!-- A BARRA. O tablist tem só abas dentro, como o padrão manda; o botão
de pausa é irmão dele. A pausa existe porque a dobra troca sozinha
por mais de cinco segundos, e quem não usa "reduzir movimento" no
sistema precisa de um jeito explícito de parar (WCAG 2.2.2). -->
<divclass="cine-bar">
<divclass="cine-tabs"data-cine-tabsrole="tablist"aria-label="Frentes de trabalho">
<buttonclass="cine-tab is-on"type="button"data-cine-tabrole="tab"aria-selected="true"tabindex="0">
<spanclass="cine-tab-bar"><spanclass="cine-tab-fill"></span></span>
<spanclass="cine-tab-n">01</span>
<spanclass="cine-tab-t">Sucessão</span>
</button>
<buttonclass="cine-tab"type="button"data-cine-tabrole="tab"aria-selected="false"tabindex="-1">
<spanclass="cine-tab-bar"><spanclass="cine-tab-fill"></span></span>
<spanclass="cine-tab-n">02</span>
<spanclass="cine-tab-t">Internacional</span>
</button>
<buttonclass="cine-tab"type="button"data-cine-tabrole="tab"aria-selected="false"tabindex="-1">
<spanclass="cine-tab-bar"><spanclass="cine-tab-fill"></span></span>
<spanclass="cine-tab-n">03</span>
<spanclass="cine-tab-t">Carteira</span>
</button>
</div>
<!-- .btn-play é a primitiva do core (base.css): os dois desenhos dentro
do botão, com só um visível por vez, comandados por [data-pausado]
no PRÓPRIO botão — e quem escreve esse atributo é o LIB.pausavel.
A .cine-play que sobra desenha o disco: ela não veste .btn-disc
porque aqui a borda e a tinta são medidas contra FOTO, em --light, e
não contra o fundo da seção (--rule-2 sumiria sobre a imagem).
O BOTÃO NASCE [hidden] e quem o mostra é o core. Sob movimento
reduzido o script simplesmente não chama o LIB.pausavel, e ele fica
como nasceu: não há troca automática para pausar, e botão que não
comanda nada é pior que botão nenhum.
OS DOIS RÓTULOS MORAM AQUI, em data-rot-*. Até 2026-09-21 eles eram
duas strings cravadas dentro do script.js — copy em JavaScript, onde
nenhum redator vai procurar. -->
<buttonclass="btn-play cine-play"type="button"data-cine-playhiddendata-rot-pausar="Pausar a troca automática"data-rot-tocar="Retomar a troca automática"aria-label="Pausar a troca automática">
<svgclass="btn-play-pausa"viewBox="0 0 12 12"aria-hidden="true"focusable="false">
<rectx="2"y="1.5"width="2.6"height="9"rx="1"fill="currentColor"/>
<rectx="7.4"y="1.5"width="2.6"height="9"rx="1"fill="currentColor"/>
</svg>
<svgclass="btn-play-tocar"viewBox="0 0 12 12"aria-hidden="true"focusable="false">
<pathd="M2.8 1.6 10.2 6 2.8 10.4Z"fill="currentColor"stroke="currentColor"stroke-width="1.4"stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</section>
estilo.css · 460 linhas · 18,7 KB
/* ==========================================================================
SLIDESHOW CINEMATOGRÁFICO — prefixo cine-
Hero de tela cheia com três fotos que trocam sozinhas, headline por
palavra e barra de abas com progresso.
Tokens usados:
--dark-rgb as duas camadas de scrim sobre a foto
--light texto e abas — a dobra é uma ILHA ESCURA (ver abaixo)
--accent linha de progresso da aba e o número dela
--display tipografia da headline e das abas
--ease curva do projeto
--nav-h folga para o menu fixo passar por cima da foto
--radius-pill o disco do botão de pausa
A coluna vem do .wrap do core, então --max e --pad não são lidos aqui.
ILHA ESCURA. Tudo que está sobre a foto lê --light, e não --fg (§8 do
brief, §6 do contrato): assim a mesma dobra roda numa página de escopo
claro sem virar texto escuro sobre foto escura. Quem depende do escopo de
tom aqui é só o fundo da seção, que aparece atrás das fotos enquanto elas
carregam.
UMA DURAÇÃO SÓ. --cine-dur é o relógio da dobra: o zoom da foto, a linha
de progresso da aba e o setTimeout do script leem os MESMOS 7s. O script
lê o valor por getComputedStyle na raiz — nunca há dois números para
manter em sincronia (ver o gotcha na ficha).
========================================================================== */.cine{
/* Relógio da dobra. Tempo que cada slide fica em cena — e, junto, a
duração do zoom e da linha de progresso. Abaixo de 5s não dá para ler a
headline inteira; acima de 10s a barra parece travada. */--cine-dur:7s;
/* Fusão entre uma foto e a próxima. */--cine-swap:1.2s;
/* Fusão do bloco de texto. Mais curta que a da foto de propósito: o texto
precisa sair antes de o novo entrar, senão as duas headlines se
sobrepõem legíveis por meio segundo. */--cine-fade:.45s;
/* Desfoque da foto que entra e da que sai. É o que dá a leitura de
"cinema" em vez de "slideshow": a foto se resolve, não aparece. Custa
caro em tela cheia — o bloco de mobile lá embaixo baixa o valor. */--cine-blur:16px;
/* Ken Burns: escala no fim do slide. 1.08 em 7s é ~1,1% por segundo, no
limite do que o olho lê como "vivo" sem enjoar. */--cine-zoom:1.08;
/* Onda da headline: atraso da primeira palavra e passo entre palavras. */--cine-word-base:.1s;
--cine-word-lag:54ms;
--cine-title:clamp(34px,5vw,72px);
position:relative;
overflow:hidden;
/* Duas declarações, não uma: svh é a altura da janela SEM contar a barra
do navegador que se recolhe, e é o que evita a dobra "vazar" um pedaço
no celular. Onde svh não existe, a primeira linha continua valendo.
Não usa .sec-pad: a dobra é uma tela inteira, e o respiro é o
padding do .cine-body. */min-height:100vh;
min-height:100svh;
}
/* ---------------------------------------------------------------- mídia */.cine-media{position:absolute;inset:0;overflow:hidden}
.cine-slides{position:absolute;inset:0}
/* O slide inativo é invisível E fora do fluxo de pintura: sem o
visibility:hidden ele continuaria sendo um retângulo de tela cheia com
filter:blur() aplicado, e o §11 do brief é explícito — estado de repouso
não paga quadro. O visibility só troca DEPOIS da fusão (delay igual a
--cine-swap), senão a foto que sai desaparece de uma vez. */.cine-slide{
position:absolute;inset:0;
opacity:0;visibility:hidden;
filter:blur(var(--cine-blur));
transition:opacity var(--cine-swap) var(--ease),
filter var(--cine-swap) var(--ease),
visibility 0s linear var(--cine-swap);
}
.cine-slide.is-on{
opacity:1;visibility:visible;
filter:blur(0);
transition:opacity var(--cine-swap) var(--ease),
filter var(--cine-swap) var(--ease),
visibility 0s;
}
/* .is-out é a foto que está saindo. Ela precisa continuar visível durante a
fusão — e continuar ZOOMANDO: ver o bloco do Ken Burns. */.cine-slide.is-out{visibility:visible}
/* O corte é por slide, no markup (style="--cine-pos:60% 46%"), e não uma
regra por posição: retrato em pé, veleiro no terço direito e sala com
janela à esquerda não sobrevivem ao mesmo valor. Mesmo recurso do
conteudo/pilares-texto-na-foto e do midia/carrossel-perfis. */.cine-img{
position:absolute;inset:0;
width:100%;height:100%;
object-fit:cover;
object-position:var(--cine-pos,50%46%);
/* A escala parte daqui e a animação a substitui. Escrita também no estado
de repouso para a foto sem animação (sem JS, ou movimento reduzido) não
nascer num tamanho diferente do primeiro quadro do zoom. */transform:scale(1);
}
/* Dois gradientes por slide, e não um só para a mídia inteira: o desfoque da
fusão é aplicado no SLIDE, então o scrim tem de estar dentro dele. Um
scrim por fora ficaria nítido sobre uma foto desfocada, e o retângulo
apareceria. O de baixo ancora o texto; o da esquerda dá contraste à
coluna; a última parada do vertical escurece o topo, onde passa o menu. */.cine-scrim{
position:absolute;inset:0;
background:
linear-gradient(to top,
rgba(var(--dark-rgb),.94) 0%,
rgba(var(--dark-rgb),.66) 32%,
rgba(var(--dark-rgb),.20) 62%,
rgba(var(--dark-rgb),.46) 100%),
linear-gradient(to right,
rgba(var(--dark-rgb),.74) 0%,
rgba(var(--dark-rgb),.30) 44%,
rgba(var(--dark-rgb),0) 80%);
}
/* ---------------------------------------------------------------- corpo *//* Texto e abas ancorados embaixo, como no hero/full-bleed-media: o alto da
tela é da foto. A linha 1fr empurra a barra de abas para o pé. */.cine-body{
position:relative;z-index:2;
min-height:100vh;
min-height:100svh;
display:grid;
grid-template-rows:1fr auto;
padding-top:calc(var(--nav-h) + clamp(40px,8vh,88px));
padding-bottom:clamp(28px,4.5vh,44px);
}
.cine-copies{display:grid;align-items:end;padding-bottom:clamp(34px,6vh,64px)}
/* Todas as cópias na MESMA célula, alinhadas pela base. A célula fica com a
altura da cópia mais alta, e é isso que impede a barra de abas de pular
de lugar quando um slide tem headline de duas linhas e outro de três. */.cine-copy{
grid-area:1 / 1;
align-self:end;
max-width:min(720px,100%);
opacity:0;visibility:hidden;
transition:opacity var(--cine-fade) var(--ease),
visibility 0s linear var(--cine-fade);
}
.cine-copy.is-on{
opacity:1;visibility:visible;
transition:opacity var(--cine-fade) var(--ease),visibility 0s;
}
/* O olho é a primitiva .eyebrow do core; esta classe só troca a tinta,
porque aqui ele está sobre foto. Seletor duplo (.cine .cine-eyebrow) de
propósito: .eyebrow e .cine-eyebrow têm a mesma especificidade, e o §6.4
do contrato proíbe depender de qual folha carregou primeiro. Mesma
solução do .hgal-cap .hgal-type. */.cine .cine-eyebrow{color:color-mix(in srgb,var(--light) 66%,transparent)}
/* Não usa .display-xl do core: aquela primitiva é a peça gráfica de uma
dobra de conteúdo, com corpo que enche a coluna e entrelinha .95. Esta
headline divide a tela com uma foto e um botão, e passa a maior parte do
tempo em duas linhas — são dois desenhos diferentes. */.cine-title{
font-family:var(--display);font-weight:700;
font-size:var(--cine-title);
line-height:1.06;letter-spacing:-.03em;
color:var(--light);
max-width:17ch;
margin-top:20px;
}
/* inline-block é o que permite transformar palavra a palavra. O espaço entre
os spans continua sendo espaço de texto — não vire flex aqui, ou a quebra
de linha deixa de acompanhar o corpo da fonte. */.cine-w{display:inline-block}
.cine-desc{
font-size:clamp(15px,1.35vw,18px);line-height:1.6;
color:color-mix(in srgb,var(--light) 78%,transparent);
max-width:52ch;
margin-top:22px;
}
.cine-act{margin-top:30px}
/* ---------------------------------------------------------------- entrada *//* A COREOGRAFIA REENTRA A CADA SLIDE. Por isso não é data-rv nem data-split:
os dois ganchos do core disparam UMA vez, quando o bloco entra na tela, e
se desobservam. Aqui o mesmo texto tem de entrar de novo toda vez que a
cópia recebe .is-on — o que só uma transição por classe faz.
Três estados, e a ordem é o que garante a página sem JS:
1. em repouso, tudo VISÍVEL — sem script, a dobra abre pronta;
2. .is-live (posta pelo script antes da primeira pintura) esconde;
3. .in + .is-on revela, com o atraso por palavra.
Os atrasos moram no estado 3, e não no 1: assim a onda escalona na
ENTRADA e a saída é imediata, escondida atrás da fusão da cópia. */.cine.is-live .cine-eyebrow,
.cine.is-live .cine-w,
.cine.is-live .cine-desc,
.cine.is-live .cine-act{
opacity:0;
filter:blur(12px);
transform:translateY(20px);
transition:opacity .9svar(--ease),
filter .9svar(--ease),
transform .9svar(--ease);
}
.cine.is-live.in .cine-copy.is-on .cine-eyebrow,
.cine.is-live.in .cine-copy.is-on .cine-w,
.cine.is-live.in .cine-copy.is-on .cine-desc,
.cine.is-live.in .cine-copy.is-on .cine-act{
opacity:1;filter:blur(0);transform:none;
}
.cine.is-live.in .cine-copy.is-on .cine-eyebrow{transition-delay:0s}
.cine.is-live.in .cine-copy.is-on .cine-w{
transition-delay:calc(var(--cine-word-base) + var(--cine-i,0) * var(--cine-word-lag));
}
.cine.is-live.in .cine-copy.is-on .cine-desc{transition-delay:.42s}
.cine.is-live.in .cine-copy.is-on .cine-act{transition-delay:.56s}
/* Quem pediu menos movimento recebe o texto inteiro, parado, sem onda e sem
desfoque. A troca de slide continua existindo — ela é comando da pessoa,
não movimento imposto (o script desliga o relógio; ver script.js).
Colado na regra que desliga, como manda o §6.5. */@media(prefers-reduced-motion:reduce){
.cine.is-live .cine-eyebrow,
.cine.is-live .cine-w,
.cine.is-live .cine-desc,
.cine.is-live .cine-act{
opacity:1;filter:none;transform:none;transition:none;
}
}
/* ---------------------------------------------------------------- ken burns *//* A MESMA regra vale para o slide que entra e para o que sai. Não é
duplicação: é o que impede a foto que sai de dar um pulo.
`animation … forwards` só segura o estado final enquanto a regra continuar
valendo. Tirar .is-on da foto que sai apagaria o animation-name, o
navegador voltaria ao transform de repouso (scale 1) e a foto encolheria
0,08 em um quadro, bem no meio da fusão. Com .is-out a regra continua
casando, o zoom segue correndo enquanto ela esmaece, e o script só tira a
classe depois de --cine-swap, quando a foto já está invisível.
O outro lado da moeda está no script: uma foto que volta a ser .is-on sem
ter perdido o animation-name NÃO reinicia o zoom sozinha. Ver
reiniciarAnim() no script.js. */.cine.in .cine-slide.is-on .cine-img,
.cine.in .cine-slide.is-out .cine-img{
animation:cineKen var(--cine-dur) linear forwards;
}
@keyframes cineKen{
from{transform:scale(1)}
to{transform:scale(var(--cine-zoom,1.08))}
}
/* Pausa. A especificidade aqui é maior de propósito: a regra acima usa a
SHORTHAND `animation`, e toda shorthand zera as suas longhands — inclusive
animation-play-state. Uma regra de pausa mais fraca seria escrita e depois
apagada pela shorthand, e o botão de pausa "não funcionaria" sem erro
nenhum no console. Medido. */.cine.is-paused.in .cine-slide.is-on .cine-img,
.cine.is-paused.in .cine-slide.is-out .cine-img{animation-play-state:paused}
/* Sem zoom para quem pediu menos movimento — e a foto fica no tamanho de
repouso, que é o mesmo primeiro quadro do zoom. */@media(prefers-reduced-motion:reduce){
.cine.in .cine-slide.is-on .cine-img,
.cine.in .cine-slide.is-out .cine-img{animation:none}
/* Fusão só de opacidade, mais curta, e sem o desfoque. */.cine-slide{
filter:none;
transition:opacity .4s linear,visibility 0s linear .4s;
}
.cine-slide.is-on{
filter:none;
transition:opacity .4s linear,visibility 0s;
}
}
/* ---------------------------------------------------------------- abas *//* A barra só existe com JavaScript: sem ele as abas seriam três botões que
não fazem nada. Quem acende é o script, na inicialização (.is-live). */.cine-bar{display:none}
.cine.is-live .cine-bar{
display:flex;align-items:flex-start;
gap:clamp(14px,2.4vw,30px);
}
.cine-tabs{
display:flex;flex:11 auto;min-width:0;
gap:clamp(12px,2.4vw,32px);
}
/* Botão de verdade, sem cara de botão: o reset é local porque o .btn do core
é outra coisa (pílula de rótulo, com fundo e padding fixos). Cada aba
ocupa a mesma fração da barra — é o que faz as três linhas de progresso
lerem como uma régua repartida. */.cine-tab{
flex:110;min-width:0;
display:grid;
grid-template-columns:auto minmax(0,1fr);
align-items:baseline;
column-gap:10px;row-gap:14px;
padding:0;border:0;background:none;
text-align:start;
color:color-mix(in srgb,var(--light) 54%,transparent);
transition:color .35svar(--ease);
}
.cine-tab:hover{color:var(--light)}
.cine-tab.is-on{color:var(--light)}
/* O trilho de 2px de cada aba, lado a lado, é o filete do pé da dobra —
por isso não há border-top na barra: seria uma linha em cima da outra. */.cine-tab-bar{
grid-column:1 / -1;
height:2px;
background:color-mix(in srgb,var(--light) 20%,transparent);
overflow:hidden;
}
.cine-tab-fill{
display:block;height:100%;
background:var(--accent);
transform:scaleX(0);transform-origin:left center;
}
/* Enche no tempo do slide. Mesma duração, mesma fonte: --cine-dur. */.cine.in .cine-tab.is-on .cine-tab-fill{
animation:cineFill var(--cine-dur) linear forwards;
}
@keyframes cineFill{
from{transform:scaleX(0)}
to{transform:scaleX(1)}
}
/* Mesma história da pausa do Ken Burns: precisa vencer a shorthand. */.cine.is-paused.in .cine-tab.is-on .cine-tab-fill{animation-play-state:paused}
/* Sem relógio não há o que encher: a linha da aba ativa fica cheia e vira um
marcador estático de onde a pessoa está. */@media(prefers-reduced-motion:reduce){
.cine.in .cine-tab.is-on .cine-tab-fill{animation:none;transform:scaleX(1)}
}
.cine-tab-n{
font-family:var(--display);font-weight:700;
font-size:11px;letter-spacing:.1em;
color:color-mix(in srgb,var(--accent) 90%,transparent);
}
.cine-tab-t{
font-family:var(--display);font-weight:700;
font-size:clamp(11px,1vw,12.5px);
letter-spacing:.11em;text-transform:uppercase;
color:currentColor;
}
/* ---------------------------------------------------------------- pausa *//* A dobra troca sozinha por mais de cinco segundos: a WCAG 2.2.2 pede um
jeito explícito de parar, e o hover não conta (não existe no toque, e
quem usa leitor de tela não "passa o ponteiro").
O BOTÃO VESTE .btn-play DO CORE, e de lá vêm três coisas que este arquivo
deixou de escrever (2026-09-21): o [hidden] desligado à mão (§6.5), a troca
de ícone e o tamanho do desenho. Restou o disco — que aqui NÃO é o
.btn-disc: a borda e a tinta são medidas contra a FOTO, em --light, e o
--rule-2 do escopo sumiria sobre a imagem. O --play-ico é o parafuso da
primitiva, e 12px é o que cabe num disco de 36. */.cine-play{
flex:00 auto;
--play-ico:12px;
width:36px;height:36px;padding:0;
display:grid;place-items:center;
background:none;
border:1px solid color-mix(in srgb,var(--light) 24%,transparent);
border-radius:var(--radius-pill);
color:color-mix(in srgb,var(--light) 66%,transparent);
transition:color .3svar(--ease),border-color .3svar(--ease);
}
.cine-play:hover{color:var(--light);border-color:color-mix(in srgb,var(--light) 52%,transparent)}
/* O [hidden] não é repetido aqui de propósito: o display:grid acima venceria o
display:none que a folha do navegador dá ao [hidden] (§6.5, mesma pegadinha
do .hgal-nav da galeria em scroll horizontal), e quem o desarma agora é a
primitiva, em .btn-play[hidden] — seletor de classe MAIS atributo, que vence
este de classe só em qualquer ordem de carregamento (§6.4). *//* ---------------------------------------------------------------- toque *//* Sem ponteiro não há hover: a aba ativa tem de se distinguir por outra
coisa que não a cor de destaque do hover. A linha de progresso e a cor
cheia do rótulo já fazem isso — aqui só se apaga o estado de hover, que
em toque fica "grudado" no último item tocado. */@media(hover:none){
.cine-tab:hover{color:color-mix(in srgb,var(--light) 54%,transparent)}
.cine-tab.is-on:hover{color:var(--light)}
}
/* ---------------------------------------------------------------- mobile */@media(max-width:760px){
.cine{
--cine-title:clamp(30px,8.6vw,40px);
/* 16px de desfoque numa foto de tela cheia é caro, e no celular é caro
no aparelho errado. 8px mantém a leitura de fusão e corta pela metade
a área que o navegador precisa refazer. Números na ficha. */--cine-blur:8px;
--cine-swap:1s;
}
.cine-body{
padding-top:calc(var(--nav-h) + 24px);
padding-bottom:22px;
}
.cine-copies{padding-bottom:30px}
.cine-title{max-width:none;margin-top:14px}
.cine-desc{margin-top:16px;max-width:44ch}
.cine-act{margin-top:22px}
/* Número EM CIMA do rótulo, não no lugar dele: "01" sozinho não diz nada,
e é o rótulo que informa para onde a aba leva. Uma coluna só por aba,
e o rótulo pode quebrar em duas linhas sem desalinhar as vizinhas —
todas as abas começam na mesma altura. */.cine-tab{grid-template-columns:minmax(0,1fr);row-gap:9px}
.cine-tab-n{font-size:9.5px}
.cine-tab-t{font-size:10px;letter-spacing:.06em}
.cine-tabs{gap:12px}
.cine.is-live .cine-bar{gap:12px}
.cine-play{width:32px;height:32px}
}
@media(max-width:420px){
/* Botão de largura cheia: a essa altura ele é o único caminho adiante e
divide a tela com a barra de abas. */.cine-act .btn{width:100%}
}
/* ==========================================================================
SEM JAVASCRIPT, O CONTEUDO APARECE. §5 do contrato.
Estas regras revelam por CLASSE PROPRIA (`.cine.in`), e nao pelo `[data-rv]` do
core — por isso o `@media (scripting: none)` do core/motion.css nao as
alcanca: ele cobre `[data-rv]`, `.w` e `.ttl`, e mais nada.
Sem esta excecao, com o JavaScript desligado o olho, a headline e o apoio do primeiro slide nao apareciam. Medido com
`node bin/medir.js <preview> --sem-js`: 19 nos com texto proprio invisiveis
========================================================================== */@media (scripting: none){
.cine-w,
.cine-eyebrow,
.cine-desc,
.cine-cta{opacity:1;visibility:visible;transform:none;filter:none;transition:none}
/* O primeiro slide e' o unico que faz sentido sem script: sem JS nao ha
troca, e mostrar os tres empilhados seria pior que mostrar um. */.cine-slide:first-child{opacity:1;visibility:visible}
}
script.js · 428 linhas · 18,4 KB
/* ==========================================================================
SLIDESHOW CINEMATOGRÁFICO — o relógio, a troca e o teclado.
O movimento é todo CSS: a fusão das fotos, o zoom (Ken Burns), a onda de
palavras e a linha de progresso da aba são transição e @keyframes. Este
script não desenha quadro nenhum. Ele faz seis coisas:
1. marca a raiz com .is-live — é o que faz a barra de abas existir, e
por isso é a PRIMEIRA linha: sem JavaScript a dobra abre inteira, com
o primeiro slide, e sem três botões que não levam a lugar nenhum;
2. cria os ids e amarra aria-controls / aria-labelledby (o markup não
traz id: id de biblioteca colide na primeira página que repetir a
dobra);
3. abre a coreografia com .in, dois quadros depois da primeira pintura,
e no MESMO instante liga o relógio — assim a linha de progresso e o
setTimeout partem juntos e nunca se descolam;
4. troca de slide sozinho a cada --cine-dur, lido do CSS;
5. pausa quando ninguém está vendo (aba oculta, dobra fora da tela) ou
quando alguém está lendo (ponteiro sobre o texto ou sobre as abas,
foco dentro da seção, botão de pausa apertado) — e retoma do ponto
exato em que parou. O botão em si é fiado pelo LIB.pausavel do core;
aqui só se atende ao aviso de que o estado trocou;
6. ouve clique, setas do teclado, Home/End e arrasto horizontal.
MOVIMENTO REDUZIDO: o relógio não liga. A dobra fica no primeiro slide, e
as abas continuam trocando — gesto pedido pela pessoa não é movimento
imposto a ela. Quem desliga o zoom e a onda de palavras é o CSS, e o botão
de pausa não chega a ser ligado: fica [hidden] como nasceu.
Nenhum listener de scroll, nenhum requestAnimationFrame em laço: a única
coisa presa ao tempo é um setTimeout, e ele é cancelado em toda pausa.
========================================================================== */
(function () {
var raizes = document.querySelectorAll('[data-cine]');
if (!raizes.length) return; // guard/* Contador de instâncias — a base dos ids únicos por dobra. */var conta = 0;
/* 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;
}
/* "7s" e "700ms" viram milissegundos. O valor vem de getComputedStyle, que
devolve o token do jeito que foi escrito (com o espaço da declaração),
não um número normalizado — daí o replace antes do parseFloat. */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 slides = [].slice.call(raiz.querySelectorAll('[data-cine-slide]'));
var copias = [].slice.call(raiz.querySelectorAll('[data-cine-copy]'));
var abas = [].slice.call(raiz.querySelectorAll('[data-cine-tab]'));
var n = Math.min(slides.length, copias.length, abas.length);
/* Menos de dois slides não é slideshow: sai em silêncio e a dobra fica
sendo um hero estático — inclusive sem a barra de abas, porque o
.is-live só é escrito depois daqui. */if (n < 2) return;
var caixaAbas = raiz.querySelector('[data-cine-tabs]');
var caixaTexto = raiz.querySelector('[data-cine-copies]');
var botao = raiz.querySelector('[data-cine-play]');
var estilo = getComputedStyle(raiz);
var dur = emMs(estilo.getPropertyValue('--cine-dur'), 7000);
var swap = emMs(estilo.getPropertyValue('--cine-swap'), 1200);
var reduz = reduzido();
var imgs = [], focaveis = [], i;
for (i = 0; i < n; i++) {
imgs.push(slides[i].querySelector('img'));
focaveis.push(copias[i].querySelectorAll('a[href],button,[tabindex]'));
}
/* Qual slide o markup deixou aceso. É o primeiro por convenção, mas ler
do DOM evita que trocar a ordem no HTML quebre a primeira pintura. */var atual = 0;
for (i = 0; i < n; i++) if (slides[i].classList.contains('is-on')) { atual = i; break; }
/* ------------------------------------------------------------ ids e ARIA *//* Padrão "carousel with tabbed slide picker" do W3C APG. O painel de cada
aba é a CÓPIA (texto e botão), não a foto: painel tem de ser o que se
lê e o que se clica. */var pre = 'cine-' + (++conta) + '-';
for (i = 0; i < n; i++) {
abas[i].id = pre + 't' + i;
copias[i].id = pre + 'p' + i;
abas[i].setAttribute('aria-controls', copias[i].id);
copias[i].setAttribute('aria-labelledby', abas[i].id);
}
/* ------------------------------------------------------------ estado */function ligarCopia(k) {
copias[k].classList.add('is-on');
copias[k].removeAttribute('aria-hidden');
copias[k].removeAttribute('inert');
var f = focaveis[k], j;
for (j = 0; j < f.length; j++) f[j].removeAttribute('tabindex');
}
/* Três travas para o mesmo problema, e cada uma cobre o buraco da outra:
aria-hidden tira da árvore de acessibilidade; inert tira do foco e do
clique (é recente — Chrome 102, Safari 15.5, Firefox 112); tabindex
-1 é a rede para quem não tem inert. O visibility:hidden do CSS já
tiraria da ordem de tabulação sozinho, mas ele só entra DEPOIS da
fusão (tem delay), e nesse intervalo a cópia que sai continuaria
alcançável por Tab. */function desligarCopia(k) {
copias[k].classList.remove('is-on');
copias[k].setAttribute('aria-hidden', 'true');
copias[k].setAttribute('inert', '');
var f = focaveis[k], j;
for (j = 0; j < f.length; j++) f[j].setAttribute('tabindex', '-1');
}
function pintarAbas() {
for (var k = 0; k < n; k++) {
var ativa = (k === atual);
if (ativa) abas[k].classList.add('is-on');
else abas[k].classList.remove('is-on');
abas[k].setAttribute('aria-selected', ativa ? 'true' : 'false');
/* Roving tabindex: uma parada de Tab para a barra inteira, e as setas
andam por dentro dela. É o que o padrão de abas pede. */
abas[k].setAttribute('tabindex', ativa ? '0' : '-1');
}
}
/* O leitor de tela não deve narrar slide que troca sozinho — viraria
interrupção a cada 7s. Com o relógio desligado (pausa travada ou
movimento reduzido) a troca passa a ser sempre pedida por alguém, e aí
anunciar é o certo. Recomendação do próprio APG. */function vivo() {
if (!caixaTexto) return;
caixaTexto.setAttribute('aria-live', (reduz || travado) ? 'polite' : 'off');
}
/* ------------------------------------------------------------ zoom *//* Reinicia a animação de zoom da foto que entra.
Existe por causa do .is-out: a foto que sai continua com o
animation-name aplicado durante a fusão (é o que a impede de encolher
no meio do caminho). Se ela voltar a ser a foto ativa antes de a fusão
terminar — dois cliques rápidos na barra de abas —, o animation-name
nunca chegou a mudar, e o navegador NÃO recomeça uma animação que já
está rodando: a foto reapareceria já ampliada, com o zoom pela metade.
Medido: sem a leitura de offsetWidth entre as duas escritas, o
navegador junta as duas no mesmo quadro, o valor computado nunca passa
por "none" e o reinício não acontece. A leitura força o cálculo de
layout no meio — é o único jeito de separar as duas escritas sem
esperar um quadro. Medido: com a leitura, currentTime volta a 0; sem
ela, fica nos 2629ms em que estava. Custo do layout forçado: 0,016ms
por troca (média de 200, headless), contra 0,002ms sem ele. */function reiniciarZoom(el) {
if (!el) return;
el.style.animation = 'none';
void el.offsetWidth;
el.style.animation = '';
}
/* ------------------------------------------------------------ troca */var saindo = -1, tSaida = 0;
function mostrar(idx) {
if (idx === atual || idx < 0 || idx >= n) return;
var ant = atual;
atual = idx;
/* A foto que sai ganha .is-out e mantém o zoom correndo enquanto
esmaece; o CSS explica por quê. A classe cai sozinha depois da
fusão — os 60ms de folga cobrem o arredondamento do timer. */if (tSaida) { clearTimeout(tSaida); tSaida = 0; }
if (saindo > -1 && saindo !== ant) slides[saindo].classList.remove('is-out');
slides[ant].classList.remove('is-on');
slides[ant].classList.add('is-out');
slides[ant].setAttribute('aria-hidden', 'true');
saindo = ant;
tSaida = (function (quem) {
return setTimeout(function () {
slides[quem].classList.remove('is-out');
if (saindo === quem) saindo = -1;
tSaida = 0;
}, swap + 60);
})(ant);
slides[atual].classList.remove('is-out');
reiniciarZoom(imgs[atual]);
slides[atual].classList.add('is-on');
slides[atual].removeAttribute('aria-hidden');
desligarCopia(ant);
ligarCopia(atual);
pintarAbas();
reiniciarRelogio();
}
/* Índice circular: -1 vira o último, n vira o primeiro. */function ir(idx) { mostrar((idx % n + n) % n); }
/* ------------------------------------------------------------ relógio */var timer = 0, t0 = 0, restante = dur;
var travado = false; /* pausa pedida no botão */var oculto = !!document.hidden; /* aba em segundo plano */var naTela = true; /* dobra dentro da janela */var sobPonteiro = false; /* ponteiro sobre o texto ou as abas */var comFoco = false; /* foco dentro da seção */function podeCorrer() {
return !reduz && !travado && !oculto && naTela && !sobPonteiro && !comFoco;
}
/* Guarda o que FALTA, não quanto já passou: é isso que faz a linha de
progresso e o timer retomarem juntos. Sem contar o tempo decorrido, um
hover de meio segundo devolveria 7s cheios ao timer enquanto a barra
continuaria de onde parou, e as duas se descolariam a cada pausa. */function parar() {
if (timer) {
clearTimeout(timer);
timer = 0;
restante = Math.max(0, restante - (Date.now() - t0));
}
raiz.classList.add('is-paused');
}
function correr() {
if (timer || !podeCorrer()) return;
t0 = Date.now();
timer = setTimeout(bater, restante);
raiz.classList.remove('is-paused');
}
function reiniciarRelogio() {
if (timer) { clearTimeout(timer); timer = 0; }
restante = dur;
/* Escreve a pausa antes de tentar correr: se não puder (ponteiro em
cima, por exemplo), a classe fica e segura as animações do slide
novo no primeiro quadro. Não há pintura entre as duas linhas. */
raiz.classList.add('is-paused');
correr();
}
function atualizar() { if (podeCorrer()) correr(); else parar(); }
function bater() {
timer = 0;
ir(atual + 1);
}
/* ------------------------------------------------------------ eventos */for (i = 0; i < n; i++) {
(function (k) {
/* Sem roubar o foco: quem clicou já está com o ponteiro ali, e mover
o foco na hora do clique faria o anel aparecer para quem usa mouse
em alguns navegadores. Quem chega pelo teclado passa pelo keydown
lá embaixo, que move o foco de propósito. */
abas[k].addEventListener('click', function () { ir(k); });
})(i);
}
/* Setas em qualquer lugar da seção; Home/End só quando o foco está numa
aba, senão roubaríamos de quem só queria ir ao topo da página. */
raiz.addEventListener('keydown', function (e) {
var pos = -1, j;
for (j = 0; j < n; j++) if (abas[j] === e.target) pos = j;
var alvo;
if (e.key === 'ArrowRight') alvo = atual + 1;
elseif (e.key === 'ArrowLeft') alvo = atual - 1;
elseif (e.key === 'Home' && pos > -1) alvo = 0;
elseif (e.key === 'End' && pos > -1) alvo = n - 1;
elsereturn;
e.preventDefault();
alvo = (alvo % n + n) % n;
mostrar(alvo);
/* O foco vai SEMPRE para a aba do slide novo. Sem isto, quem apertou a
seta estando no botão do slide anterior ficaria com o foco dentro de
uma cópia que acabou de sair da árvore, e o navegador o devolveria
ao body — a pessoa perde o lugar na página. */
abas[alvo].focus();
});
/* A FIAÇÃO DO BOTÃO VEM DO CORE (LIB.pausavel): é ele quem mostra o botão
— que nasce [hidden] —, guarda o estado, escreve data-pausado (de onde
o .btn-play tira qual ícone aparece) e leva o rótulo de
data-rot-pausar / data-rot-tocar para o aria-label. Os dois rótulos
moravam DENTRO deste arquivo até 2026-09-21: copy cravada em
JavaScript, onde nenhum redator vai procurar.
COM MOVIMENTO REDUZIDO NÃO SE CHAMA A FUNÇÃO, e é essa a regra que o
core documenta: não há troca automática para pausar, e um botão que não
comanda nada é pior que botão nenhum. Sem a chamada, ele fica [hidden]
como nasceu — sem precisar que ninguém o esconda.
A pergunta pelo LIB é a mesma de reduzido(): colado sem o motion.js o
componente ainda abre, e o botão continua escondido em vez de morto. */if (botao && !reduz && window.LIB && window.LIB.pausavel) {
window.LIB.pausavel(botao, function (pausado) {
travado = pausado;
vivo();
atualizar();
});
}
/* Pausa por ponteiro só onde alguém está lendo ou escolhendo: o bloco de
texto e a barra de abas. Na seção inteira seria pior do que não ter —
a dobra ocupa a tela toda e o ponteiro parado em qualquer canto
congelaria o carrossel para sempre. */var sobre = 0;
function entrou() { sobre++; sobPonteiro = true; atualizar(); }
function saiu() {
sobre--;
if (sobre > 0) return;
sobre = 0; sobPonteiro = false; atualizar();
}
if (caixaAbas) {
caixaAbas.addEventListener('mouseenter', entrou);
caixaAbas.addEventListener('mouseleave', saiu);
}
if (caixaTexto) {
caixaTexto.addEventListener('mouseenter', entrou);
caixaTexto.addEventListener('mouseleave', saiu);
}
raiz.addEventListener('focusin', function () { comFoco = true; atualizar(); });
raiz.addEventListener('focusout', function (e) {
/* relatedTarget é quem VAI receber o foco: continuar dentro da seção
não é sair dela. */if (e.relatedTarget && raiz.contains(e.relatedTarget)) return;
comFoco = false;
atualizar();
});
/* Aba em segundo plano: o setTimeout continuaria correndo (o navegador só
o afrouxa para no mínimo 1s) e a pessoa voltaria dois ou três slides
adiante, sem ter visto nenhum. Parar e retomar do ponto exato é o que
faz a volta parecer que nada aconteceu. */
document.addEventListener('visibilitychange', function () {
oculto = !!document.hidden;
atualizar();
});
var io = new IntersectionObserver(function (entradas) {
for (var k = 0; k < entradas.length; k++) naTela = entradas[k].isIntersecting;
atualizar();
}, { threshold: 0 });
io.observe(raiz);
/* ------------------------------------------------------------ arrasto *//* Sem setPointerCapture e sem preventDefault, de propósito:
- capturar o ponteiro retarget o clique para a seção, e o botão do
slide deixaria de abrir no toque;
- preventDefault mataria a rolagem vertical da página, que é o gesto
mais comum sobre um hero.
O navegador cuida disso sozinho: num gesto vertical ele começa a rolar
e dispara pointercancel, que zera o nosso. Num gesto horizontal não há
o que rolar, os pointermove continuam chegando e o limiar decide. */var px = 0, py = 0, arrastando = false, idp = -1;
raiz.addEventListener('pointerdown', function (e) {
if (typeof e.button === 'number' && e.button !== 0) return;
arrastando = true; idp = e.pointerId;
px = e.clientX; py = e.clientY;
});
raiz.addEventListener('pointerup', function (e) {
if (!arrastando || e.pointerId !== idp) return;
arrastando = false;
var dx = e.clientX - px, dy = e.clientY - py;
/* 40px de limiar e domínio horizontal: sem a segunda condição, uma
rolagem com o dedo que desviasse 40px para o lado trocaria o slide. */if (Math.abs(dx) < 40 || Math.abs(dx) < Math.abs(dy)) return;
ir(dx < 0 ? atual + 1 : atual - 1);
});
raiz.addEventListener('pointercancel', function () { arrastando = false; });
/* ------------------------------------------------------------ abertura *//* .is-live antes de qualquer outra coisa e sem esperar quadro nenhum: é
ele que esconde o texto para a coreografia ter de onde sair, e que faz
a barra de abas existir. O script mora no fim do <body>, então isto
roda antes da primeira pintura. */
raiz.classList.add('is-live');
for (i = 0; i < n; i++) {
if (i === atual) ligarCopia(i); else desligarCopia(i);
}
pintarAbas();
vivo();
/* Dois rAF encadeados garantem que o estado inicial (opacidade zero,
blur, deslocamento) já pintou antes de a classe ligar a transição —
com um só, o navegador às vezes junta os dois quadros e o texto
aparece pronto. Convenção aberta pelo hero/full-bleed-media.
O relógio parte DAQUI, e não do window.load: esperar as três fotos
atrasaria a headline no primeiro acesso, e o load pode nunca chegar
se um recurso qualquer da página travar. O que importa é que a
primeira troca acontece --cine-dur depois da abertura, e não no
carregamento. */function abrir() {
if (raiz.classList.contains('in')) return;
raiz.classList.add('in');
reiniciarRelogio();
}
requestAnimationFrame(function () { requestAnimationFrame(abrir); });
/* Rede para aba aberta em segundo plano, onde o rAF não roda. */
setTimeout(abrir, 450);
}
for (var r = 0; r < raizes.length; r++) iniciar(raizes[r]);
})();
Abertura de tela cheia com três slides. Cada slide é uma foto sangrada que entra em fusão com desfoque e passa os sete segundos seguintes num zoom lento — o Ken Burns —, com olho, headline que entra palavra a palavra, apoio e botão próprios. No pé da dobra, uma barra de três abas numeradas reparte a largura da coluna: a aba ativa tem uma linha de progresso que enche exatamente no tempo do slide, e é ela que diz quanto falta. Clicar numa aba, apertar as setas do teclado ou arrastar o dedo troca na hora. O relógio para quando o ponteiro entra no texto ou nas abas, quando o foco entra na seção, quando a aba do navegador é escondida, quando a dobra sai da tela e quando alguém aperta o botão de pausa — e retoma do ponto exato em que parou, nunca do começo.
Resolve o hero de quem tem três promessas do mesmo tamanho e não consegue eleger uma. É o caso comum da assessoria: sucessão, internacional e carteira não são etapas de um funil, são três portas — e escolher uma para o topo da home significa esconder duas. A alternativa honesta seria uma lista de três cards, que não abre página nenhuma; o slideshow dá a cada porta uma tela inteira, com foto e botão próprios, e devolve o controle a quem chega pela barra de abas em vez de obrigar a esperar.
Quando usar
Home com três frentes de negócio equivalentes, cada uma com foto editorial e um caminho adiante diferente.
Página que aguenta uma dobra de tela cheia e cujo assunto sustenta três fotos com a mesma direção de arte.
Quando existe material real para todas as três: foto, promessa curta e um CTA que leva a lugares distintos.
Quando não usar
Uma promessa central. Use hero/full-bleed-media: mesma linguagem de foto sangrada e scrim, sem o custo de três fotos e sem pedir tempo a quem chega. Este componente substitui aquele só quando as frentes são três e nenhuma pode ser a primeira.
Quando o topo precisa mostrar interface.hero/duas-colunas-produto tem o palco com cards flutuando sobre a foto.
Quando as três frentes são conteúdo de miolo, e não abertura.listas/leque-paineis mostra as três ao mesmo tempo, sem relógio e sem esconder nada — é quase sempre a escolha certa fora do topo.
Quando a página tem um formulário acima da dobra. Uma dobra que se move ao lado de um campo de texto rouba a atenção do campo.
Padrão editorial
Slot
Regra
Slides
Três. Dois lêem como indecisão, e a barra fica com metade da largura por aba. Quatro é o teto — acima disso o rótulo não cabe em 390px.
Olho
2 a 3 palavras, categoria e não benefício. É o rótulo da frente de trabalho: "Planejamento sucessório", não "Cuide do seu futuro".
Headline
5 a 8 palavras, uma frase só, com ponto final. Até ~46 caracteres. Sem <em>: o destaque aqui é a foto.
Apoio
90 a 150 caracteres, duas linhas em desktop. Diz o que está incluído, não o que é bom.
Botão
Um por slide, verbo no imperativo ou infinitivo, 2 a 4 palavras, destinos diferentes entre os slides. Se os três levam ao mesmo lugar, é um hero só com três fotos.
Aba — número
Dois dígitos, sequência simples (01, 02, 03). É pontuação, não dado.
Aba — rótulo
Uma palavra, até 14 caracteres, caixa baixa no HTML (a caixa alta é do CSS). Duas palavras quebram em duas linhas em 390px — funciona, mas desalinha o ritmo da régua.
O que mantém a dobra coesa é o paralelismo entre os três slides: mesma quantidade de palavras na headline (±2), apoio do mesmo tamanho, botões com a mesma forma verbal. Como as três cópias ocupam a mesma célula do grid e são alinhadas pela base, uma headline de três linhas ao lado de duas de uma linha faz o bloco de texto pular de altura visualmente a cada troca — o botão fica parado, o título é que sobe.
A onda de palavras é o que dá o ritmo, e ela tem prazo: 8 palavras × 54ms + 0,9s de transição = 1,4s até a frase inteira estar em pé. Com 20 palavras seriam 2,0s de um slide de 7s só montando a frase.
Especificação dos assets
Formato e dimensão mínima: paisagem, WebP, mínimo 2400 × 1350, ideal 3000 × 1688. A foto cobre a tela inteira, e num monitor de 2560px uma imagem de 1600 aparece esticada.
Peso alvo: até 220 KB por foto, e isso é um teto duro. As três carregam de uma vez (loading="eager", sem lazy) porque a primeira troca acontece 7s depois de abrir, e foto que começa a baixar na hora da fusão pisca. São ~660 KB antes de a dobra assentar; a primeira é o LCP da página. Com fotos mais pesadas que isso, troque o teto por dois slides.
Composição: assunto fora do terço esquerdo — o gradiente horizontal escurece a esquerda, que é onde o texto mora. Cada foto declara o seu corte em --cine-pos no style do <img>; o valor decide o que sobra em 390px, onde só ~26% da largura da foto fica visível.
Direção de arte igual nas três: mesma hora do dia, mesma temperatura de cor, mesma profundidade. Três fotos de bancos diferentes lidas em sequência de 7s denunciam o banco.
O que não pode ter: texto embutido, logo, marca d'água, rosto olhando para a câmera no terço esquerdo (fica atrás da headline) e foto clara demais para aguentar texto branco — o scrim ajuda, não salva.
Cole o markup.html como primeiro filho do <body>, troque as três fotos, os três --cine-pos e o texto. Precisa do core/tokens.css, do core/base.css e — desde 2026-09-21 — do core/motion.js: é dele que vem o LIB.pausavel, a fiação do botão de pausa. Sem ele a dobra ainda abre e troca sozinha (o script lê LIB.reduce quando existe e cai no matchMedia quando não), mas o botão fica [hidden], e é ele que a WCAG 2.2.2 exige.
Ganchos
Gancho
Papel
[data-cine]
Raiz. O script parte daqui, nunca do documento — a dobra pode se repetir na página.
[data-cine-slide]
A foto de um slide.
[data-cine-copy]
O bloco de texto de um slide — é o role="tabpanel" da aba de mesmo índice.
[data-cine-copies]
O empilhamento das cópias: pausa no hover e carrega o aria-live.
[data-cine-tabs]
O role="tablist". Pausa no hover.
[data-cine-tab]
Cada aba.
[data-cine-play]
O botão de pausa, vestindo a primitiva .btn-play. Nasce [hidden]; quem o mostra é o LIB.pausavel do core — e sob movimento reduzido o script simplesmente não o chama.
data-rot-pausar / data-rot-tocar
Os dois rótulos do botão, no atributo. Nomes do core — é ele quem lê e escreve o aria-label.
--cine-dur
O relógio da dobra. Tempo do slide, duração do zoom e da linha de progresso — os três lêem esta variável, e o script a lê do CSS. 7s no padrão.
--cine-swap
Fusão entre uma foto e a próxima. 1.2s.
--cine-fade
Fusão do bloco de texto. .45s — mais curta que a da foto de propósito.
--cine-blur
Desfoque da fusão. 16px no desktop, 8px abaixo de 760px.
--cine-zoom
Escala no fim do Ken Burns. 1.08. 1 desliga.
--cine-word-base / --cine-word-lag
Atraso da primeira palavra (.1s) e passo entre palavras (54ms).
--cine-title
Corpo da headline. clamp(34px,5vw,72px).
--cine-pos
object-position da foto, inline em cada <img>.
--cine-i
Índice da palavra, inline em cada .cine-w. É o que escalona a onda.
.is-live
Na raiz: o script está rodando. Faz a barra de abas existir e esconde o texto para a coreografia ter de onde sair.
.in
Na raiz: abertura. Dispara a coreografia, o zoom e a linha de progresso, e marca o instante zero do relógio.
.is-on
No slide, na cópia e na aba de mesmo índice.
.is-out
No slide que sai — mantém o zoom correndo durante a fusão.
.is-paused
Na raiz: relógio parado. Congela o zoom e a linha de progresso.
[data-pausado]
No próprio botão, escrito pelo core. É de onde o .btn-play tira qual ícone aparece. Substituiu a classe .is-off.
Variações
dois ou quatro slides — acrescente ou tire um <li>, uma .cine-copy e uma aba, sempre os três juntos e na mesma ordem.
sem troca automática — deixe um slide só: com menos de dois o script sai em silêncio e a dobra vira um hero estático, sem barra de abas.
ritmo de vitrine — --cine-dur:5s e --cine-swap:.8s, com headlines de até cinco palavras.
sem zoom — --cine-zoom:1, para fotos que já têm movimento próprio.
banner de página interna — min-height:62svh na seção e no .cine-body, --cine-title menor, sem o apoio.
Gotchas
animation … forwards não segura nada depois que a regra deixa de casar. Tirar .is-on da foto que sai apagaria o animation-name, e ela voltaria ao transform de repouso — encolheria 0,08 em um quadro, no meio da fusão. Por isso existe o .is-out: a mesma regra casa com .is-one com .is-out, o zoom continua correndo enquanto a foto esmaece, e o script só tira a classe depois de --cine-swap. Quem "limpar" essa duplicação de seletor devolve o pulo.
A volta rápida precisa de um layout forçado, e isso foi medido. Se a foto voltar a ser a ativa antes de a fusão terminar (dois cliques seguidos na barra), o animation-name nunca chegou a mudar — e o navegador não recomeça uma animação que já está rodando. Medido: currentTime em 2629ms; escrevendo animation:none e devolvendo animation:'' na sequência, continua 2629ms; com uma leitura de offsetWidth entre as duas escritas, volta a 0. Custo do layout forçado: 0,016ms por troca (média de 200), contra 0,002ms sem ele. É a única razão de o void el.offsetWidth estar lá — não apague.
A pausa precisa vencer a shorthand animation em especificidade.animation:cineKen … é uma shorthand e zera todas as longhands dela, inclusive animation-play-state. A regra que aplica o zoom tem quatro classes (.cine.in .cine-slide.is-on .cine-img); a regra de pausa tem cinco (.cine.is-paused.in …) exatamente por isso. Escrita com menos, o botão de pausa "não funcionaria" — sem erro nenhum no console.
O relógio guarda o que falta, não o que já passou. Cada pausa desconta Date.now() - t0 de restante, e a retomada agenda só o restante. Sem isso, um hover de meio segundo devolveria 7s cheios ao setTimeout enquanto a linha de progresso continuaria de onde parou, e as duas se descolariam a cada pausa — o slide trocaria com a barra pela metade.
Aba oculta pausa de verdade, e não é firula.setTimeout em aba de segundo plano não para: o navegador apenas o afrouxa para no mínimo 1s. Sem tratar visibilitychange, quem volta depois de um minuto encontra a dobra oito slides adiante, sem ter visto nenhum. Medido: escondendo a aba, .is-paused entra no mesmo instante, o slide continua o mesmo depois de 9s, e ao voltar o relógio retoma do restante — não do começo.
O --cine-blur cai pela metade no celular, e o número explica. A/B da mesma troca em 1440×900, headless sem GPU dedicada: com 16px, média de 4,66ms por quadro e pior quadro de 54,2ms; com 8px, média 4,21ms e pior quadro 16,8ms; sem desfoque, 4,17ms e 4,6ms. O custo é quase todo no primeiro quadro da fusão, quando o navegador precisa montar a camada desfocada de tela inteira. Em desktop, um quadro perdido no início de uma transição de 1,2s não se vê; num aparelho de entrada, se vê. Se a dobra estiver picotando no celular, baixe --cine-blur antes de mexer em qualquer outra coisa.
O slide inativo é visibility:hidden, e não só opacity:0. Três retângulos de tela cheia com filter:blur() permanente custam memória de camada mesmo invisíveis. O visibility só troca depois da fusão (transição de 0s com atraso igual a --cine-swap), senão a foto que sai desapareceria de uma vez em vez de esmaecer. O mesmo vale para as cópias — e ali ele tem um segundo papel: tirar o botão do slide antigo da ordem de tabulação.
inert é recente, e por isso não está sozinho. Cada cópia inativa recebe aria-hidden, inertetabindex="-1" nos links. Medido no Chrome atual: 'inert' in HTMLElement.prototype é true, então ali as três travas são redundantes — mas inert só chegou no Chrome 102, no Safari 15.5 e no Firefox 112, e o tabindex é o que segura o resto. Sem alguma delas, o Tab entra num painel invisível e o foco some da tela.
.is-live é o que faz a dobra existir sem JavaScript. Ele é escrito na primeira linha da inicialização, antes da primeira pintura, e faz duas coisas opostas: esconde o texto (para a coreografia ter de onde sair) e acende a barra de abas. Sem script, nenhuma das duas acontece — a dobra abre inteira, com o primeiro slide pronto e sem três botões que não levam a lugar nenhum. Se alguém "simplificar" pondo o estado inicial direto no .cine-w, a página sem JS fica com a headline invisível.
min-height aparece duas vezes, com valores diferentes, e está certo.100vh e logo abaixo 100svh: onde svh existe, a segunda linha vence e a dobra para de vazar um pedaço sob a barra do navegador no celular; onde não existe, a primeira continua valendo. Não dá para resolver isso com uma variável — custom property aceita qualquer token, então --h:100svh seria aceita em um navegador antigo e só falharia no var(), deixando a altura em auto.
O relógio começa na abertura, não no window.load. Esperar as três fotos atrasaria a headline no primeiro acesso, e o load pode nunca chegar se um recurso qualquer da página travar. A consequência assumida: em conexão ruim, o zoom da primeira foto pode começar antes de ela pintar. O que o load garantiria — não trocar de slide no instante em que a página abre — já está garantido de outro jeito: a primeira troca é sempre --cine-dur depois da abertura.
A pausa por ponteiro é só no texto e nas abas. Na seção inteira seria pior que não ter: a dobra ocupa a tela toda, e um ponteiro parado em qualquer canto congelaria o carrossel para sempre — sem que ninguém entendesse por quê.
O arrasto não captura o ponteiro e não cancela nada.setPointerCapture retargetaria o clique para a seção e o botão do slide deixaria de abrir no toque; preventDefault mataria a rolagem vertical da página. O navegador resolve sozinho: num gesto vertical ele começa a rolar e dispara pointercancel. O limiar é 40px e domínio horizontal (|dx| > |dy|) — sem a segunda condição, rolar com o dedo trocaria de slide.
A seta do teclado leva o foco para a aba do slide novo, sempre. Quem apertou → estando no botão do slide anterior ficaria com o foco dentro de uma cópia que acabou de sair da árvore, e o navegador o devolveria ao <body> — a pessoa perde o lugar na página. Home/End, ao contrário das setas, só valem com o foco numa aba: fora dali, elas pertencem à página.
O botão de pausa não é enfeite de acessibilidade. A dobra troca sozinha por mais de cinco segundos, e a WCAG 2.2.2 pede um jeito explícito de parar. Hover não conta (não existe no toque) e foco não conta (quem usa leitor de tela em modo de leitura não move o foco). Com movimento reduzido o botão some, porque aí não há troca automática para pausar.
Histórico
Projeto
Onde
Data
Diferenças
autoral
construído do zero na biblioteca
2026-09-03
—
migração
fiação do botão de pausa promovida ao core
2026-09-21
Saíram deste componente a troca de ícone por .is-off, o [hidden] desligado à mão, o tamanho do ícone e — o que mais importava — os dois rótulos, que eram copy cravada dentro do script.js. Entraram .btn-play (core/base.css), LIB.pausavel (core/motion.js) e data-rot-pausar / data-rot-tocar no markup. Por quê: este componente e o hero/video-fundo faziam a mesma fiação cada um do seu jeito, e a leva de etapas, filme e depoimentos em rodízio traria mais três cópias — o §6 manda promover na terceira. O disco continua sendo .cine-play e não vira .btn-disc: aqui a borda e a tinta são medidas contra a foto, em --light, e o --rule-2 do escopo sumiria sobre a imagem
Origem. Componente autoral (contrato §11): não é clone de uma referência, é a tradução de um padrão de interação que aparece em muito site premium — o herói rotativo das plataformas de streaming, com a fileira de abas e a barra que enche; as páginas de produto da Apple e da Nike, com foto de tela cheia em zoom lento; os templates de hero premiados no Awwwards e vendidos em Framer e Webflow. Não há URL de referência e não há uma linha copiada: o arquivo nasceu aqui, em token, prefixo, primitiva do core e padrão ARIA. Status externo: nunca rodou em produção nossa. O primeiro site que usar promove, e esta ficha registra qual foi. O registro consolidado vai em projetos/autorais.md.
Decisões de tradução para o contrato:
Um relógio só. O padrão de mercado costuma ter o tempo do slide no JavaScript e a duração da barra no CSS, e os dois desandam na primeira revisão. Aqui existe --cine-dur, e o script a lê com getComputedStyle — o zoom, a barra de progresso e o setTimeout nunca podem discordar.
Nada entra seco (§5). A fusão do original de mercado é opacity pura. Aqui a foto que entra vem de blur(16px) a zero e a que sai faz o caminho contrário; a headline entra palavra a palavra, com blur e subida.
Palavra a palavra por span com índice, não por data-split. O gancho do core quebra o texto em .w e dispara uma vez, quando o bloco entra na tela; aqui a mesma frase precisa reentrar a cada troca. É a mesma decisão do hero/centrado-prova-social, e pelo mesmo motivo adicional: .w é classe do core, que componente não pode estilizar — sem poder estilizar, não há como escalonar.
Cor por token. Scrim em rgba(var(--dark-rgb),…), texto e abas em --light com color-mix (a dobra é uma ilha escura, então lê --light e não --fg, e continua legível numa página de escopo claro), linha de progresso em --accent, grão pela primitiva .grain do core.
Medida por variável. Duração, fusão, desfoque, escala do zoom, passo da onda e corpo do título viraram --cine-* na raiz do componente; o corte de cada foto é --cine-pos inline, como em midia/carrossel-perfis.
Acessibilidade que o padrão de mercado quase nunca tem: padrão "carousel with tabbed slide picker" do W3C APG, com aria-roledescription="carrossel" na seção e "slide" em cada painel; role="tablist" de verdade com roving tabindex (uma parada de Tab para a barra inteira); aria-controls/aria-labelledby amarrados pelo script, com ids únicos por instância, porque id em markup de biblioteca colide na primeira página que repetir a dobra; aria-live que só liga quando o relógio está desligado, para o leitor de tela não ser interrompido a cada 7s; pausa explícita por botão (WCAG 2.2.2) somada à pausa por ponteiro, por foco, por aba oculta e por saída da tela; e o desligamento completo do relógio em movimento reduzido, com as abas continuando a funcionar.
Movimento reduzido desliga o relógio, não a dobra. Medido com prefers-reduced-motion: reduce: nada troca sozinho em 8,6s (a aba ativa continua sendo a 0), animation-name do <img> é none, a foto fica em scale(1), a barra da aba ativa fica cheia (vira marcador estático em vez de mentir que está contando), as palavras entram juntas e sem blur, e o botão de pausa não chega a ser ligado: o script não chama o LIB.pausavel e o botão fica [hidden] como nasceu (display:none, sem data-pausado). Botão que não comanda nada é pior que botão nenhum. As abas continuam trocando de slide — gesto pedido pela pessoa não é movimento imposto a ela.
A copy dos rótulos mora no markup, não no script. Até 2026-09-21 as duas frases ("Pausar a troca automática" / "Retomar a troca automática") eram strings dentro do script.js — copy em JavaScript, onde nenhum redator vai procurar. Hoje são data-rot-pausar e data-rot-tocar no <button>, e quem as escreve no aria-label é o core.
Não há aria-pressed no botão, e é decisão. O rótulo troca, que é o padrão de controle de mídia; somar aria-pressed a um nome que muda produz o anúncio duplo e contraditório "Retomar, pressionado". O estado mora no nome ou no atributo, nunca nos dois — está explicado no comentário do .btn-play no core/base.css.