Abertura de página inteira em que a superfície é vídeo: um loop curto sangrado até a borda, escurecido por dois gradientes cruzados e por um véu desfocado com máscara em degradê, com a headline entrando linha por linha por dentro de uma máscara e o conteúdo ancorado na base — dentro da mídia, nunca abaixo dela. No pé, uma régua opcional de três números e o controle que para o loop.
O problema editorial que ela resolve é curto de descrever e caro de executar: atmosfera que uma foto não dá. Uma sala que respira, luz que atravessa uma janela, papel que se move — coisas que dizem "esta casa existe e é assim" antes de qualquer palavra. O que decide a peça, porém, não é o desenho: é a decisão de performance. Vídeo de fundo é a coisa mais cara que uma home pode carregar, e por isso metade desta ficha é sobre o arquivo, não sobre o CSS.

O preview mostra o POSTER, parado. A biblioteca não versiona vídeo (§11), e os dois
<source>do markup estão comentados com a instrução. Isso não é uma limitação do preview: é exatamente o estado de fallback que o componente promete — e é o que a pessoa vê se o vídeo falhar, se ela tiver movimento reduzido ligado ou se estiver economizando dados.
Quando usar
- Home institucional de serviço de alto ticket, onde o topo precisa entregar atmosfera e não informação: escritório, casa, luz, mão, papel, água.
- Quando existe um loop editado de propósito para isto — não um trecho de um vídeo institucional, não um take de campanha com corte. Ver a especificação abaixo: um loop que fecha e um poster que é quadro dele.
- Quando a headline é uma promessa curta, em duas linhas.
- Quando alguém do lado do cliente pode gerar e revisar o arquivo. Vídeo mal exportado é pior que foto boa, sempre.
Quando não usar
- Não há loop editado de propósito. É o caso mais comum, e o irmão
hero/full-bleed-mediaganha: mesma anatomia — mídia sangrada, scrim, headline em linhas, régua de números —, com foto parada. Uma foto de 300 KB entrega a mesma autoridade sem pôr megabytes na frente do LCP, e ainda tem parallax, que aqui não existe. Comece sempre por ele; troque para este só quando o material justificar. - O vídeo é conteúdo, e não superfície — alguém deve assistir a ele. Aqui o vídeo é
muted, sem controles, e ninguém vai vê-lo até o fim; a divisão com os três irmãos de vídeo da casa é por quem assiste e a quantos: - um vídeo, alguém assiste até o fim:
midia/showreel-expansivel— capa que cresce até a tela e entrega os controles nativos do navegador; - uma série de três a seis:
midia/faixa-videos— grade de capas com duração, no miolo da página; - o vídeo é prova, com nome e rosto:
prova/depoimento-video.
Os três abrem <dialog> e ficam no miolo. Este é o único que usa vídeo como superfície, e o único que abre a página.
- São três frentes e nenhuma pode ser a primeira. hero/slideshow-cinematico.
- O topo precisa mostrar interface. hero/duas-colunas-produto tem o palco com cards sobre a foto.
- O que convence é a frase, e a mídia só decoraria. hero/centrado-prova-social — texto centrado, prova social e um botão, sem asset nenhum.
- A página tem formulário acima da dobra. Mídia que se move ao lado de um campo de texto rouba a atenção do campo. Vale para o slideshow e vale em dobro aqui, porque o movimento é contínuo.
- O cliente não tem quem exporte o arquivo. Sem alguém para rodar o ffmpeg da especificação e conferir a emenda do loop, o resultado previsível é um MOV de 40 MB no <source> — e aí a peça não é lenta, é quebrada.
Padrão editorial
| Slot | Regra | Referência |
|---|---|---|
Olho (.eyebrow) |
2 a 3 palavras. Categoria, não benefício. Opcional. | "Assessoria independente" |
Headline (.vfd-title) |
6 a 12 palavras em 2 linhas, uma .vfd-ln cada, 3 a 6 palavras por linha, quebra escrita à mão. Uma palavra em <em> — só uma. |
"A independência que / seu patrimônio exige." |
Apoio (.vfd-desc) |
1 parágrafo, 25 a 42 palavras, teto de 56ch. | — |
Ação primária (.vfd-cta .btn-primary) |
2 a 4 palavras. Verbo no imperativo + objeto. Nunca "Saiba mais". | "Agende um diagnóstico" |
Ação secundária (.vfd-cta .btn-ghost) |
2 a 4 palavras. Opcional. | "Conheça a casa" |
Chave do número (.vfd-k) |
3 a 8 caracteres. 3 itens ou nenhum. Opcional. | "100%" · "SP · POA" · "Comitê" |
Rótulo do número (.vfd-v) |
20 a 56 caracteres, em 2 linhas de até 28, quebradas com <br>. Opcional. |
"Honorários acordados, / sem comissão de produto" |
Rótulo do botão tocando (.vfd-toggle, atributo data-rot-pausar) |
3 a 5 palavras. | "Pausar o vídeo de fundo" |
Rótulo do botão parado (.vfd-toggle, atributo data-rot-tocar) |
3 a 5 palavras. | "Retomar o vídeo de fundo" |
Os dois rótulos do botão moram em ATRIBUTO, não no conteúdo do nó. O script escreve o valor no aria-label conforme o estado do vídeo. Quem escrever a copy dentro do <button> vai vê-la sobreviver ao primeiro carregamento e desaparecer no primeiro clique, sem nada acusar.
Sobre a headline: duas linhas é estrutural. A terceira desmancha a cascata (o nth-child(2) já carrega o delay) e empurra a régua para fora da dobra — em celular, medido, a dobra fecha com 8px de folga.
Sobre a régua: três itens ou nenhum, e o motivo é geométrico, não editorial: aqui ela divide a linha do pé com o botão de pausa, e o quarto item aperta as colunas. Ela responde "por que confiar" — modelo de remuneração, praça, governança, certificação, tempo de casa. Nunca "quanto rende" (ver Compliance).
Sobre o apoio: é mais curto que o do irmão de foto (25–42 contra 25–45 palavras, teto de 56ch contra 58ch). Não é capricho: aqui o texto disputa a leitura com uma imagem que se move.
Especificação dos assets
São dois arquivos, e o segundo não é opcional.
O vídeo
| Duração | 8 a 12 s. Abaixo disso o loop lê como GIF e a emenda volta demais; acima, são bytes para um material que ninguém assiste — a pessoa rola em cinco segundos |
| Resolução | 1920×1080. Não 4K: o vídeo roda a 46% de opacidade atrás de um véu desfocado, onde nenhum detalhe de 4K sobrevive, e quadruplica o peso |
| Taxa | 24 ou 25 fps. Não há ação a resolver num fundo, e 24 corta ~20% do bitrate contra 30 |
| Peso alvo | até 1,8 MB no mp4 (a conta: 1400 kbps × 10 s = 1,75 MB). Ideal perto de 1,5 MB. O mesmo material mal exportado passa de 12 MB |
| Formatos | webm/VP9 primeiro no markup, mp4/H.264 depois. O navegador pega o primeiro que sabe tocar, e o VP9 costuma sair ~25% menor |
| Áudio | nenhuma trilha. O vídeo é muted para sempre; trilha de áudio é peso morto |
# mp4/H.264 — o que todo navegador toca
ffmpeg -i origem.mov -t 10 -an \
-vf "scale=1920:-2,fps=24" \
-c:v libx264 -profile:v high -level 4.0 -pix_fmt yuv420p \
-preset slower -crf 28 -maxrate 1400k -bufsize 2800k \
-movflags +faststart \
hero-fundo.mp4
# webm/VP9 — menor, entra antes no markup
ffmpeg -i origem.mov -t 10 -an \
-vf "scale=1920:-2,fps=24" \
-c:v libvpx-vp9 -crf 36 -b:v 0 -row-mt 1 \
hero-fundo.webm
# o poster: o QUADRO ZERO do vídeo já exportado, nunca outra foto
ffmpeg -i hero-fundo.mp4 -frames:v 1 -q:v 2 poster.png
cwebp -q 75 -resize 1600 0 poster.png -o hero-fundo-poster.webp
Três flags que parecem detalhe e não são:
-movflags +faststart. Move o índice do arquivo para o começo. Sem ele, o navegador não começa a tocar até o arquivo inteiro chegar — e o defeito é invisível na sua máquina, porque arquivo local se lê inteiro na hora. É a falha mais comum e a mais difícil de diagnosticar.-pix_fmt yuv420p -profile:v high. O que iOS e Safari de fato decodificam. Exportar emyuv444pou 10 bits produz um arquivo que toca no seu Chrome e cai calado no poster no iPhone do cliente.-an. Sem trilha de áudio. Vale ~10% do arquivo e nunca vai tocar.
O que o vídeo NÃO pode ter
- Texto queimado, logo ou lower-third. A headline mora exatamente ali, e dois textos brigam.
- Rosto reconhecível em close. Direito de imagem (§11), e um rosto na metade de baixo fica sob a copy.
- Corte brusco dentro do loop. Todo corte num trecho de 10 s que repete lê como falha de arquivo, não como montagem.
- Branco estourado na metade de baixo do quadro. É onde a copy mora e onde está o piso de contraste — céu queimado, parede branca e transição fade-to-white são as três formas de chegar lá.
- Movimento em direção à lente. Um zoom-in que termina em outro enquadramento denuncia a emenda do loop.
- Cotação, extrato, tela de sistema ou marca de instituição real. Ver Compliance.
Como o loop tem de fechar
O salto na emenda é o defeito que mais aparece e o que ninguém percebe na hora de exportar.
- Primeiro quadro igual ao último. Escolha material cuja composição volte ao ponto de partida: panorâmica lenta sobre assunto estático, água, tecido, luz mudando pouco. É a saída mais barata e a que dá o melhor resultado.
- Ou vai-e-volta no export, se o material não fecha:
-filter_complex "[0:v]split[a][b];[b]reverse[r];[a][r]concat". A emenda passa a ser matematicamente invisível, ao custo de dobrar os quadros — para o mesmo peso, use metade da duração. - Nunca termine em fade para preto. No reinício, o loop pisca.
- Confira assistindo a três voltas completas. Se você viu a emenda uma vez, ela está lá.
O poster
- Tem de ser um quadro do próprio vídeo — o quadro zero. Poster que é outra foto produz um salto visível no instante em que o vídeo assume.
- Mínimo 1600×900, até 120 KB em WebP q75. Ele é o LCP da página: tem de ser o arquivo pequeno.
- A headline tem de ser legível sobre ele, e isso se mede. Nesta demo, com
bin/medir.jsmais leitura de pixel: título 10,1:1, apoio 7,4:1, chave 11,8:1, rótulo 5,9:1 no pior pixel de cada caixa. Não confie no olho: em tela boa, texto claro sobre foto escura parece sempre legível.
Compliance
A peça é uma abertura institucional e não devolve número calculado, mas pisa em terreno regulado em três pontos.
- A régua de números é ILUSTRATIVA no markup e está marcada como tal em comentário HTML. Os três itens têm de ser trocados por dado verificável do cliente antes de publicar. E têm de continuar respondendo "por que confiar": modelo de remuneração, praça, governança, autorização, tempo de casa. Rentabilidade, meta de retorno ou comparação com índice num hero de assessoria é problema de compliance, não copy fraca — não existe faixa nem ressalva que salve um número de retorno em corpo grande na primeira dobra.
- O vídeo é uma superfície de compliance. Não pode conter: rosto de cliente real, marca ou logo de instituição financeira, tela de sistema com posição consolidada ou extrato, cotação, gráfico de rentabilidade, nem qualquer plano que insinue resultado. Um take de dez segundos passa por revisão de conteúdo igual a um texto.
- Nada é gravado. Sem
localStorage, sem cookie, sem envio. A escolha de pausar não é persistida de propósito: quem quer o movimento desligado de forma durável já temprefers-reduced-motionno sistema operacional, que esta peça obedece, e a biblioteca reservalocalStoragepara o caso de consentimento (§4 do brief eoverlay/aviso-cookies).
Como usar
<link rel="stylesheet" href="core/tokens.css">
<link rel="stylesheet" href="core/temas/<projeto>.css">
<link rel="stylesheet" href="core/base.css">
<link rel="stylesheet" href="core/motion.css">
<link rel="stylesheet" href="componentes/hero/video-fundo/estilo.css">
...
<script src="core/motion.js"></script>
<script src="componentes/hero/video-fundo/script.js"></script>
Cole o markup.html como primeiro filho do <body>, descomente os dois <source> e aponte o poster para o WebP.
O escopo de tom NÃO se troca aqui, ao contrário do irmão de foto. A peça é escura por construção: o scrim, o véu e a emenda leem --dark-rgb porque o texto tem de sobreviver a qualquer quadro, e trocar .sec-dark por .sec-paper ou .sec-light deixa tinta escura sobre mídia escurecida — ilegível, e nenhum validador acusa. Por isso o realce em itálico e a chave da régua leem --accent direto e não --accent-ink: --accent-ink existe para o accent usado como tinta num escopo claro, e nos dois escopos escuros os dois valores são idênticos. Num projeto claro, a dobra de abertura é hero/centrado-prova-social ou o irmão de foto.
Os quatro atributos do <video> — muted, playsinline, autoplay, loop — andam juntos e nenhum é opcional. Sem muted nenhum navegador toca sem gesto; sem playsinline o iPhone abre o vídeo em tela cheia por cima do site.
preload="metadata" é escolha, e as outras duas não servem: auto põe megabytes na frente do poster, que é o LCP; none é ignorado na presença de autoplay, e escrevê-lo documentaria uma intenção que o navegador descarta.
Ganchos
| Gancho | Papel |
|---|---|
[data-vfd] |
raiz — o script procura por ele, nunca por id, e itera sobre todos |
[data-vfd-video] |
o <video> de fundo |
[data-vfd-toggle] |
o botão de pausa, vestindo as primitivas .btn-disc e .btn-play. Nasce [hidden]; quem o mostra é o LIB.pausavel do core |
data-rot-pausar / data-rot-tocar |
os dois rótulos, no atributo. Nomes do core — é ele quem lê e escreve o aria-label |
[data-pausado] |
no próprio botão, escrito pelo core: espelha video.paused e é de onde o .btn-play tira qual ícone aparece. Substituiu a classe .vfd.is-tocando na raiz |
.vfd.in |
portão da cascata de entrada, posto pelo script |
--vfd-alt |
altura da dobra (100svh, 58vh na variação curta) |
--vfd-video-op |
opacidade do vídeo — o parafuso do contraste, ver a tabela no topo do estilo.css |
--vfd-veu-alt / --vfd-veu-blur |
altura e desfoque do véu. --vfd-veu-blur:0 devolve a GPU |
Variações
- Sem régua — apague o bloco
.vfd-regua. O.vfd-pecontinua servindo o botão de pausa, que não é opcional. - Uma ação só — apague o
<a class="btn btn-ghost">. A cascata não muda. .vfd-curto— banner de página interna: 58vh, véu mais alto, sem régua e sem segundo botão.--vfd-veu-blur: 0— mantém o degradê e devolve a GPU. É o parafuso a girar quando o loop engasga em máquina fraca.- Autoplay pelo script — apague o atributo
autoplay. Fecha o único buraco que a peça não fecha (ver Gotchas) ao custo de o loop começar um pouco depois. É escolha de compliance de acessibilidade, não de desenho. Ainda não exercitada.
Gotchas
- Com
prefers-reduced-motion: reduce, o vídeo não toca — e isso é regra, não gentileza. Vídeo de fundo é movimento que a pessoa não pediu. O script chamapause(), tira oautoplay, põepreload="none"e chamaload(); os quatro passos andam juntos, porquepause()sozinho deixa na tela o último quadro pintado, não o poster. O que não se desliga é o botão: quem aperta play pediu, e pedir continua valendo sobreduce. Medido:paused=true,autoplayremovido,preload=none,readyState=0, poster visível. - O botão de pausa nasce
[hidden], e odisplayque venceria o[hidden]vem da PRIMITIVA, não deste arquivo..btn-discédisplay:gridno core, e qualquer regra de autor comdisplayvence o[hidden]do navegador (§6.5). A armadilha continua de pé — o que mudou em 2026-09-21 é quem a desarma: era.vfd-toggle[hidden]{display:none}noestilo.css, agora é.btn-play[hidden]no core, seletor de classe mais atributo, que vence o.btn-disc{display:grid}em qualquer ordem de carregamento (§6.4). Quem copiar só o.btn-disce esquecer o.btn-playrecebe o botão visível e morto de volta — o atributo está no inspetor, o script ainda não rodou, e nada acusa. - O estado do botão é do BOTÃO, não da seção. Até 2026-09-21 o ícone trocava por
.vfd.is-tocandona raiz, o que custava à peça uma classe de estado global só para desenhar um ícone — e duas instâncias na mesma página disputariam o mesmo seletor. Hoje quem manda é[data-pausado]no próprio botão, escrito pelo core. Se o ícone parar de trocar, o que falta é ocore/base.cssatualizado, não uma classe na raiz. - Não há
aria-pressed, e é decisão, não esquecimento. O rótulo deste botão troca ("Pausar o vídeo de fundo" → "Retomar o vídeo de fundo"), que é o padrão de controle de mídia. Somararia-presseda 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-playnocore/base.css. - O único buraco que a peça não fecha: JavaScript desligado. Ali o vídeo toca (o
autoplayestá no markup) e o botão de pausa nunca aparece, o que contraria a WCAG 2.2.2 para movimento automático acima de cinco segundos. CSS não para um<video>; não há de onde resolver. A saída é a variação autoplay pelo script, uma linha.preload="metadata"ajuda no caso comum: quando o script roda, o vídeo ainda não bufferizou, e o poster nem pisca. @media(scripting:none)noestilo.cssnão é redundância do core. Ocore/motion.csscobre[data-rv],.we.ttl; esta peça não usa nenhum dos três, porque o portão da cascata é a classe.indo próprio componente. Sem o@medialocal, uma página sem JavaScript mostraria vídeo e nada mais — headline, apoio, botões e régua invisíveis para sempre, com o HTML correto no inspetor. O irmãohero/full-bleed-mediatem exatamente esse defeito hoje.- Não há
[data-rv]em nó nenhum, e não é esquecimento. Hero já intersecta na carga: o reveal por interseção dispararia os cinco blocos juntos, e o que se quer é cascata em ordem escrita. Além disso,[data-rv].in{transform:none}vale (0,2,0) e apagaria em silêncio otranslateY(112%)das linhas da headline (§5). - Não há parallax, ao contrário do irmão. Lá o deslocamento existe justamente porque a foto está parada. Aqui a mídia já é movimento, e um segundo eixo faz a dobra nadar. Se alguém "consertar" copiando o
onScrolldo irmão, é isso que volta. - O véu tem de ser irmão da mídia, não filho do bloco de texto. Ele precisa sangrar até a borda igual ao vídeo: parado na coluna do
.wrap, obackdrop-filterdesenha duas linhas verticais duras no meio da imagem. --vfd-video-opé diferente no desktop (.46) e no celular (.38), e os dois números vêm de medição de pixel. No celular a copy ocupa uma fração muito maior da dobra e o apoio sobe até a faixa mais clara do scrim. A tabela completa está no topo doestilo.css. Quem subir o parafuso sem medir devolve o contraste.- No preview, clicar em play deixa o botão dizendo "Pausar" com o poster parado. É artefato de o
<video>não ter<source>: sem nenhuma fonte, o Chrome marcapaused=falsee a promessa deplay()fica pendente para sempre, semerrore sempause. Com arquivo de verdade, um caminho errado disparaerrore oaddEventListener('error')devolve o rótulo. Não persiga isso. navigator.connectionnão existe no Safari nem no Firefox, e a ausência cai no comportamento padrão (tocar), nunca em erro. A linha de corte para no2gde propósito:3gé onde boa parte do acesso móvel brasileiro se reporta, e cortar ali removeria a peça justamente do público para quem ela foi desenhada. Quem quiser ser mais duro troca a expressão por/2g|3g/— está numa linha só.min-heightusa100svhdentro de@supports.100vhno celular ignora a barra do navegador e o pé da dobra fica embaixo dela. O@supportsmantémvhem base antiga, onde é o que existe.
Histórico
| Origem | Quando | O quê |
|---|---|---|
| autoral | 2026-09-06 | Construído do zero. Padrão de domínio comum, sem referência única |
| migração | 2026-09-21 | A fiação do botão de pausa passou ao core: .btn-play (core/base.css) e LIB.pausavel (core/motion.js). Saíram deste componente a troca de ícone por .vfd.is-tocando na raiz, o [hidden] desligado à mão, o tamanho do ícone e a leitura dos rótulos pelo script. Os atributos de copy perderam o prefixo: data-vfd-rot-* virou data-rot-*, que é o par que o core lê. Por quê: este componente e o hero/slideshow-cinematico faziam a mesma coisa 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 |
Origem. O vídeo em loop como superfície da primeira dobra aparece em home de escritório de advocacia, de gestora, de incorporadora e em página institucional de serviço de alto ticket — padrão de domínio comum, sem dono. Nada foi copiado. O registro consolidado vai em projetos/autorais.md.
Por que existe ao lado do irmão. O hero/full-bleed-media tem a mesma anatomia: mídia sangrada, scrim, headline em linhas, régua de números opcional. A divisão não é de desenho, é de asset e de consequência. Foto é um arquivo que qualquer cliente entrega; loop que fecha é produção. Foto não precisa de poster, nem de quatro atributos que só funcionam juntos, nem de um controle de pausa que a WCAG exige, nem de duas razões para não tocar. Foi por isso que a variação "vídeo no lugar da foto" da ficha dele nunca saiu de experimental: trocar a tag não resolve nada do que está nesta ficha. Ele continua sendo a escolha certa na maioria dos casos — e é isso que faz a distinção honesta.
Decisões da tradução para o contrato. O @media(scripting:none) local, porque o portão da cascata é uma classe do próprio componente e o core só protege os ganchos dele. A cascata carregando blur indo a zero, que o §5 exige e o irmão não faz. pause() + preload=none + load() como conjunto, porque só o último devolve o poster. --vfd-video-op calibrado por medição de contraste contra um quadro branco chapado, para a peça ser WCAG AA contra qualquer vídeo e não só contra um vídeo bem-comportado. E o [hidden] do botão, que precisou da linha do §6.5 porque o display vinha da primitiva .btn-disc — linha que em 2026-09-21 passou ao core junto com o resto da fiação.
O que ficou anotado para a contagem do §6. Esta é a segunda cópia de dois padrões do irmão: a máscara de linha da headline (overflow:hidden no bloco, translateY(112%) no span de dentro) e a cascata por delay fixo disparada por uma classe na raiz. Na terceira, promova ao core — a máscara de linha em especial, que é a assinatura de entrada de headline da casa.
Calibragem no primeiro uso. Espere ajustar dois números contra o material real: --vfd-video-op, contra o brilho médio do loop (a tabela do estilo.css diz o que se ganha e o que se perde), e object-position, contra o enquadramento — o padrão 50% 46% sobe o centro de interesse para acima da faixa da copy.