← Biblioteca componentes/midia/faixa-videos/ficha.md CódigoTela cheia
midia/faixa-videos

Faixa de vídeos

🟢 estavel complexidade média prefixo fvid- video videos grade 3-colunas duracao episodio serie dialogo dialog capa lazy acessibilidade

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

<link rel="stylesheet" href="componentes/midia/faixa-videos/estilo.css">
<script src="componentes/midia/faixa-videos/script.js"></script>

markup.html · 270 linhas · 14,0 KB

<!-- ==========================================================================
     FAIXA DE VÍDEOS — o índice de uma série de vídeos curtos
     Prefixo: fvid-
     Requer: core (tokens.css, base.css, motion.css, motion.js)
             + este estilo.css e este script.js
             + 1 capa 16:9 por item e 1 arquivo de vídeo por item (ver ficha,
               "Especificação dos assets")

     A IDEIA. Três a seis vídeos curtos em grade de três colunas, cada card
     com capa, duração, título e o rótulo que situa o item na série. É a peça
     de quem PUBLICA vídeo toda semana — corte de live, comentário de mercado,
     aula de dez minutos — e precisa que a pessoa escolha um entre vários. A
     grade existe por isso: num trilho, metade da lista fica fora da tela, e
     índice do qual não se vê o fim não é índice.

     POR QUE NÃO É O midia/galeria-scroll-horizontal COM THUMB DE VÍDEO. O
     irmão é uma faixa de FOTOS cujo destino é outra página: cada card é um
     <a href>, o conteúdo está do outro lado do clique, e não há duração,
     ordem nem gesto de play. Aqui o conteúdo é o vídeo, o destino é um
     reprodutor NESTA página (daí <button> e não <a>), a duração é dado
     obrigatório — é ela que decide o clique — e a ordem é informação, por
     isso a lista é <ol> e não <ul>. Trocar a foto por uma thumb no irmão não
     produziria nada disso. Comparação completa na ficha.

     UM <dialog> PARA A SEÇÃO INTEIRA, e um <video> só dentro dele. Seis
     <video> inline numa dobra são seis elementos de mídia e a chance de dois
     tocarem juntos; seis <dialog> são seis reprodutores para manter iguais. O
     script troca `src`, `poster` e `<track>` do único reprodutor a cada
     abertura, e os SOLTA ao fechar.

     NENHUM VÍDEO TOCA SOZINHO. O <video> nasce sem `src`, sem <source>, sem
     `autoplay` e com `preload="none"`: antes do clique não existe um byte de
     mídia na página. O `play()` acontece DEPOIS do clique, que é outra coisa
     — a pessoa pediu o vídeo.

     AS CAPAS SÃO `loading="lazy"`, e por isso as seis são inertes até
     entrarem em cena. Com `width`/`height` declarados, a caixa de cada uma
     já está reservada antes de a imagem existir.

     O <dialog> MORA DENTRO DA <section> E FORA DO .wrap E DE TODO [data-rv].
     Dentro do .wrap ele não teria por que estar (não é conteúdo de coluna);
     dentro de um [data-rv] ele herdaria um ancestral com `filter`, que é o
     caso do §5 do MANUAL. Filho direto da seção resolve as duas coisas, e
     ainda deixa o script achá-lo A PARTIR DA RAIZ — o que é o que permite
     duas faixas de vídeo na mesma página, cada uma com o seu reprodutor.

     A CAPA É UM <button>, e o que ela contém é PHRASING CONTENT. O título é
     <span class="fvid-titulo">, não <h3>: <button> não aceita cabeçalho
     dentro, e o navegador desmontaria a marcação. Mesmo precedente do
     midia/showreel-expansivel.

     SEM JAVASCRIPT esta dobra continua sendo um índice legível — capa,
     título, duração e episódio são markup estático. O que não existe é o
     reprodutor, e o CSS trata isso: `@media (scripting: none)` apaga o anel
     de play, para o card não prometer um gesto que ninguém vai atender. Quem
     precisa de piso sem script troca o <button> por <a href> para a página do
     vídeo — é a variação "sem reprodutor" da ficha.

     CONTEÚDO. Comentário de mercado semanal, na voz da Liberta. Os títulos
     INFORMAM o que muda, nunca prometem resultado. As durações e as datas
     abaixo são ILUSTRATIVAS: têm de bater com os arquivos reais, senão a
     duração deixa de ser a informação que decide o clique e passa a ser
     enfeite que mente. Ver ficha.md, seção Compliance.
     ========================================================================== -->

<section class="sec-deep sec-pad fvid" data-fvid>
  <div class="wrap">

    <div class="sec-head fvid-head">
      <span class="eyebrow" data-rv>Toda quinta</span>
      <h2 class="sec-title ttl">Comentário de <em>mercado</em></h2>
      <p class="sec-lead" data-rv data-d="1">Um corte curto sobre o que mexeu nas carteiras na semana. Comentário, não recomendação.</p>
    </div>

    <!-- <ol> e não <ul>: a ordem é informação — o episódio 12 é o mais novo, e
         é por ele que se começa. role="list" porque `list-style:none` (que vem
         do reset do core) apaga a semântica de lista no VoiceOver do Safari.

         O escalonamento reinicia a cada FILEIRA de três: data-d escalona três
         irmãos, e um quarto delay não existe. Da segunda fileira em diante a
         entrada acontece quando a rolagem traz a fileira para a tela, então o
         relógio recomeça ali mesmo.

         data-rv vai no <li>, nunca no <button>: o card gerencia transform
         próprio no hover, e [data-rv].in{transform:none} vale (0,2,0) — apaga
         em silêncio qualquer transform que more no mesmo nó (§5). -->
    <ol class="fvid-grade" role="list">

      <!-- O endereço do vídeo e o da legenda vivem no próprio card, e é de lá
           que o script os lê ao abrir. No consumidor, o <button> abaixo fica:

             <button class="fvid-card" type="button" data-fvid-card
                     data-fvid-src="/midia/mercado-12.mp4"
                     data-fvid-vtt="/midia/mercado-12.vtt"
                     aria-haspopup="dialog">

           Sem `data-fvid-src` o diálogo abre com o poster e os controles
           nativos, e nada toca — que é o estado deste preview. -->
      <li class="fvid-item" data-rv>
        <button class="fvid-card" type="button" data-fvid-card aria-haspopup="dialog">
          <img class="fvid-capa" src="../../../showroom/demo/ph-wide-1.webp"
               alt="Casal em um vinhedo ao entardecer" width="1600" height="900"
               loading="lazy" decoding="async">
          <span class="fvid-play" aria-hidden="true">
            <svg viewBox="0 0 24 24" focusable="false"><path d="M9 5.5v13l10-6.5z" fill="currentColor"/></svg>
          </span>
          <span class="fvid-legenda">
            <span class="fvid-titulo">O que muda na renda fixa com a Selic em queda</span>
            <span class="fvid-meta">
              <span class="fvid-serie">Ep. 12 · 28 ago</span>
              <span class="fvid-dur-rot visually-hidden">Duração</span>
              <time class="fvid-dur" datetime="PT8M">8 min</time>
            </span>
          </span>
        </button>
      </li>

      <li class="fvid-item" data-rv data-d="1">
        <button class="fvid-card" type="button" data-fvid-card aria-haspopup="dialog">
          <img class="fvid-capa" src="../../../showroom/demo/ph-wide-2.webp"
               alt="Homem observando a cidade pela janela" width="1600" height="900"
               loading="lazy" decoding="async">
          <span class="fvid-play" aria-hidden="true">
            <svg viewBox="0 0 24 24" focusable="false"><path d="M9 5.5v13l10-6.5z" fill="currentColor"/></svg>
          </span>
          <span class="fvid-legenda">
            <span class="fvid-titulo">Por que diversificar não é comprar de tudo um pouco</span>
            <span class="fvid-meta">
              <span class="fvid-serie">Ep. 11 · 21 ago</span>
              <span class="fvid-dur-rot visually-hidden">Duração</span>
              <time class="fvid-dur" datetime="PT6M">6 min</time>
            </span>
          </span>
        </button>
      </li>

      <li class="fvid-item" data-rv data-d="2">
        <button class="fvid-card" type="button" data-fvid-card aria-haspopup="dialog">
          <img class="fvid-capa" src="../../../showroom/demo/ph-wide-3.webp"
               alt="Veleiro em mar aberto" width="1600" height="900"
               loading="lazy" decoding="async">
          <span class="fvid-play" aria-hidden="true">
            <svg viewBox="0 0 24 24" focusable="false"><path d="M9 5.5v13l10-6.5z" fill="currentColor"/></svg>
          </span>
          <span class="fvid-legenda">
            <span class="fvid-titulo">Câmbio no orçamento de quem tem filho estudando fora</span>
            <span class="fvid-meta">
              <span class="fvid-serie">Ep. 10 · 14 ago</span>
              <span class="fvid-dur-rot visually-hidden">Duração</span>
              <time class="fvid-dur" datetime="PT7M">7 min</time>
            </span>
          </span>
        </button>
      </li>

      <li class="fvid-item" data-rv>
        <button class="fvid-card" type="button" data-fvid-card aria-haspopup="dialog">
          <img class="fvid-capa" src="../../../showroom/demo/ph-wide-4.webp"
               alt="Sala de reunião vazia com vista para a cidade" width="1600" height="900"
               loading="lazy" decoding="async">
          <span class="fvid-play" aria-hidden="true">
            <svg viewBox="0 0 24 24" focusable="false"><path d="M9 5.5v13l10-6.5z" fill="currentColor"/></svg>
          </span>
          <span class="fvid-legenda">
            <span class="fvid-titulo">Como ler o relatório consolidado da sua carteira</span>
            <span class="fvid-meta">
              <span class="fvid-serie">Ep. 09 · 07 ago</span>
              <span class="fvid-dur-rot visually-hidden">Duração</span>
              <time class="fvid-dur" datetime="PT9M">9 min</time>
            </span>
          </span>
        </button>
      </li>

      <li class="fvid-item" data-rv data-d="1">
        <button class="fvid-card" type="button" data-fvid-card aria-haspopup="dialog">
          <img class="fvid-capa" src="../../../showroom/demo/ph-wide-5.webp"
               alt="Casal assistindo a uma partida de tênis em quadra de saibro" width="1600" height="900"
               loading="lazy" decoding="async">
          <span class="fvid-play" aria-hidden="true">
            <svg viewBox="0 0 24 24" focusable="false"><path d="M9 5.5v13l10-6.5z" fill="currentColor"/></svg>
          </span>
          <span class="fvid-legenda">
            <span class="fvid-titulo">Previdência: o que o regime tributário decide</span>
            <span class="fvid-meta">
              <span class="fvid-serie">Ep. 08 · 31 jul</span>
              <span class="fvid-dur-rot visually-hidden">Duração</span>
              <time class="fvid-dur" datetime="PT11M">11 min</time>
            </span>
          </span>
        </button>
      </li>

      <!-- A sexta capa repete a paisagem 3 porque showroom/demo/ tem cinco
           `ph-wide-*` e a grade cheia tem seis. No uso do consumidor cada
           card tem a capa do seu próprio vídeo. -->
      <li class="fvid-item" data-rv data-d="2">
        <button class="fvid-card" type="button" data-fvid-card aria-haspopup="dialog">
          <img class="fvid-capa" src="../../../showroom/demo/ph-wide-3.webp"
               alt="Veleiro em mar aberto, ao amanhecer" width="1600" height="900"
               loading="lazy" decoding="async">
          <span class="fvid-play" aria-hidden="true">
            <svg viewBox="0 0 24 24" focusable="false"><path d="M9 5.5v13l10-6.5z" fill="currentColor"/></svg>
          </span>
          <span class="fvid-legenda">
            <span class="fvid-titulo">Liquidez: quanto do patrimônio precisa ficar disponível</span>
            <span class="fvid-meta">
              <span class="fvid-serie">Ep. 07 · 24 jul</span>
              <span class="fvid-dur-rot visually-hidden">Duração</span>
              <time class="fvid-dur" datetime="PT5M">5 min</time>
            </span>
          </span>
        </button>
      </li>

    </ol>
  </div>

  <!-- O REPRODUTOR — um por seção, compartilhado pelos cards.

       .dialog-casca é a primitiva do core: ela zera padding, borda, fundo e
       os limites de tamanho que o navegador dá de graça ao <dialog>, e pinta
       o ::backdrop com --veil. A classe do core vem ANTES da local.

       `sec-deep` DECLARADO AQUI, e não herdado da seção: sem um escopo de tom
       de autor, o <dialog> cai no `color:CanvasText` do navegador e o texto
       da legenda fica preto sobre fundo escuro. A primitiva não declara
       `color` de propósito (ver core/base.css), então o escopo é o contrato —
       a herança seria acidente. É a exceção nomeada do §3.

       O BOTÃO DE FECHAR É O PRIMEIRO FOCÁVEL DO DIÁLOGO, e é por isso que ele
       vem primeiro no DOM: o <dialog> dá foco ao primeiro focável quando
       abre, e sem `autofocus` (que rende aviso de console no showroom por
       file://) o candidato natural seria o <video>, que é focável. O CSS o
       tira do fluxo e o põe no canto.

       O <video> NÃO TRAZ `src` NEM <source>. A biblioteca não versiona vídeo
       em showroom/demo/, e o §11 não deixa asset atravessar: neste preview o
       que se vê é o poster da capa clicada e os controles nativos, sem
       arquivo por trás. Abertura, foco, Esc, fechamento e a troca de poster
       funcionam e podem ser conferidos; só a reprodução não. No uso do
       consumidor, o endereço do vídeo vai em `data-fvid-src` de cada card.

       `preload="none"` fica AQUI e não é redundante com a ausência de `src`:
       ele é a promessa de que, quando o script escrever o `src`, nada será
       buscado antes de o `play()` acontecer. -->
  <dialog class="dialog-casca fvid-dialog sec-deep" data-fvid-dialog aria-label="Reprodutor de vídeo">
    <div class="fvid-palco" data-fvid-palco>

      <button class="fvid-fechar" type="button" data-fvid-fechar aria-label="Fechar o vídeo">
        <svg viewBox="0 0 24 24" focusable="false" aria-hidden="true">
          <path d="M6 6l12 12M18 6L6 18" fill="none" stroke-linecap="round"/>
        </svg>
      </button>

      <!-- Nasce vazio: quem escreve é o script, com o rótulo e o título do
           card que abriu. Com o diálogo fechado ninguém vê, e sem script o
           diálogo não abre. -->
      <p class="fvid-palco-cap">
        <span class="fvid-palco-serie"></span>
        <span class="fvid-palco-titulo"></span>
      </p>

      <div class="fvid-quadro">
        <video class="fvid-video" data-fvid-video controls playsinline preload="none"></video>
      </div>

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

estilo.css · 451 linhas · 19,8 KB

/* ==========================================================================
   FAIXA DE VÍDEOS — prefixo fvid-
   Índice de uma série de vídeos curtos: grade de três colunas, capa 16:9 com
   título e duração DENTRO da imagem, e um <dialog> por seção que recebe o
   reprodutor.

   Tokens usados:
     --pad --max        só via .wrap do core; nada aqui mexe na coluna
     --display --sans   face do título do card e do rótulo de série
     --ease             curva única do projeto
     --accent --dark    anel de play aceso: fundo de marca, glifo escuro
     --dark-rgb         véu em gradiente da legenda sobre a capa
     --dark-2           fundo da caixa da capa até a imagem existir
     --light            tinta do que está sobre a imagem (ver ILHA ESCURA)
     --fg --fg-2        tinta da legenda do palco, dentro do diálogo
     --rule --rule-2    borda do card em repouso e sob o ponteiro
     --radius-media     canto do card e do quadro do vídeo
     --radius-pill      anel de play e botão de fechar
     --veil             véu do ::backdrop, pintado pela .dialog-casca do core

   ILHA ESCURA. O card é uma imagem, e o texto vive dentro dela: o que está
   sobre a capa lê --light, nunca --fg. É o que permite a mesma faixa rodar
   numa dobra .sec-paper sem virar texto escuro sobre véu escuro. Fora da
   imagem (o cabeçalho da dobra, a legenda do palco) o escopo de tom manda.
   Mesma decisão do midia/galeria-scroll-horizontal.

   PARAFUSOS (custom properties do próprio componente, §3):
     --fvid-min         largura mínima da coluna — é ela que decide 3/2/1
     --fvid-gap         vão da grade
     --fvid-tempo       duração das transições de INTERAÇÃO (hover, foco)
     --fvid-play-size   diâmetro do anel de play
     --fvid-entrada     duração da entrada do palco no diálogo
     --fvid-veil-mix    grau do véu do ::backdrop
     --fvid-cromo       altura que a legenda e os respiros comem acima do
                        quadro — entra na conta que dimensiona o palco
     --fvid-pw/--fvid-ph  a proporção da capa e do quadro, em dois números
                        (16 e 9). Dois e não um "16 / 9" porque o palco usa
                        a razão numa conta de calc(), e aspect-ratio aceita
                        os dois números separados por barra
   ========================================================================== */

.fvid{
  /* A GRADE NÃO TEM MEDIA QUERY DE COLUNA, e isso é de propósito: com
     `auto-fill` e este mínimo, a conta do próprio grid decide.
       coluna de 1096px (--max 1240 menos --pad)  ->  3 colunas
       ~660 a 1000px                              ->  2 colunas
       abaixo de ~660px                           ->  1 coluna
     3 * 320 + 2 * 26 = 1012, que cabe em 1096; a quarta não caberia. Mexer
     em --fvid-min move os três limiares de uma vez. */
  --fvid-min:320px;
  --fvid-gap:clamp(16px,2vw,26px);

  /* --dur do core é .9s: certo para entrada de dobra, lento para a capa
     responder ao ponteiro. Duração de interação é local. */
  --fvid-tempo:.55s;
  --fvid-play-size:46px;
  --fvid-entrada:.5s;
  --fvid-veil-mix:88%;
  --fvid-cromo:108px;
  --fvid-pw:16;
  --fvid-ph:9;
}

/* A primitiva .sec-head do core centra a PRÓPRIA caixa; os filhos dela
   continuam com o max-width de 22ch e 60ch encostado à esquerda. Onze
   componentes escrevem estas duas linhas (grid-equipe, acordeao-faq,
   carrossel-anel-3d, planos-honorarios…) — está anotado para promoção. */
.fvid-head .sec-title,
.fvid-head .sec-lead{margin-inline:auto}

.fvid-grade{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(var(--fvid-min),1fr));
  gap:var(--fvid-gap);
  margin-block-start:clamp(34px,5vh,56px);
}
/* Sem isto, um título longo dentro de uma coluna 1fr faz a coluna crescer
   além da fração — o mínimo automático de um item de grade é o conteúdo. */
.fvid-item{min-inline-size:0}

/* ---------------------------------------------------------------- o card */
/* É um <button>, não um <div> com onclick: Tab, Enter, Espaço e o anúncio de
   "botão" vêm de graça. O destino é um reprodutor NESTA página, e é por isso
   que não é <a> — âncora promete navegação. */
.fvid-card{
  position:relative;
  display:block;
  inline-size:100%;
  padding:0;
  border:1px solid var(--rule);
  border-radius:var(--radius-media);
  overflow:hidden;
  text-align:start;
  /* O <button> nativo pinta o cromo do sistema por baixo, e `color-scheme:
     only light` (core/tokens.css) o deixa CINZA-CLARO em toda parte. O fundo
     explícito e o appearance zerado tiram os dois — ver o comentário do .btn
     em core/base.css, onde o mesmo caso já custou um botão invisível. */
  background:var(--dark-2);
  -webkit-appearance:none;appearance:none;
  /* O <button> alinha o conteúdo como texto: sem isto a capa ganha um filete
     de descida embaixo. Os blocos internos declaram a própria entrelinha. */
  line-height:0;
  transition:border-color var(--fvid-tempo) var(--ease);
}
.fvid-card:hover{border-color:var(--rule-2)}

.fvid-capa{
  inline-size:100%;
  /* `block-size:auto` NÃO é redundante, e a falta dele foi medida: o markup
     declara width="1600" height="900" no <img> para a caixa já estar
     reservada antes de a imagem existir, e o atributo `height` vira uma
     ALTURA DEFINIDA de 900px. Com altura definida, `aspect-ratio` não tem o
     que resolver e é ignorado em silêncio — a capa saía 348x900 em vez de
     348x196, e o card com 902px de altura. Quem apagar esta linha reabre o
     buraco; quem apagar os atributos do markup perde a reserva de caixa. */
  block-size:auto;
  aspect-ratio:var(--fvid-pw) / var(--fvid-ph);
  object-fit:cover;
  /* O brilho em repouso deixa a fileira coesa; sob o ponteiro o card acende
     sem virar outra imagem. */
  filter:brightness(.9);
  transition:filter var(--fvid-tempo) var(--ease);
}
/* :focus-visible junto do :hover — quem chega pelo teclado vê o mesmo card
   acender que quem chega pelo mouse. */
/* HOVER NÃO MEXE NA IMAGEM (CLAUDE.md §6, regra de 2026-09-29). O zoom de
   foto sob o ponteiro saiu: escala e deslocamento de mídia no hover mexem
   em recorte e composição, e é o que quebra a página quando o componente
   é colado noutro lugar. O retorno ficou em cor e brilho, que não mudam
   a geometria de nada. */
.fvid-card:hover .fvid-capa,
.fvid-card:focus-visible .fvid-capa{filter:brightness(1)}

/* ---------------------------------------------------------------- play */
/* ÓBVIO SEM SER ADESIVO. O alvo do clique é o CARD INTEIRO, então o anel não
   precisa ser o alvo — ele é só o sinal de que ali tem vídeo. Por isso é
   pequeno, de contorno e no canto, em vez de um disco branco de 84px no
   meio da foto: um triângulo dentro de um anel sobre um quadro parado se lê
   como vídeo à primeira olhada e não cobre o enquadramento.

   Nasce VISÍVEL, e é o que dispensa o `@media(hover:none)` que o irmão
   midia/galeria-scroll-horizontal precisa para o distintivo dele: em toque
   não há hover, e um sinal que só existe no hover nunca apareceria.

   `backdrop-filter` sem máscara em gradiente aqui é legítimo: a regra do §6
   é contra a CAIXA RETA, que produz borda dura e lê "chapado". Num disco de
   contorno não há borda reta a endurecer. Precedentes: .reel-play do
   midia/showreel-expansivel e .hgal-badge do irmão. */
.fvid-play{
  position:absolute;
  inset-block-start:14px;
  inset-inline-end:14px;
  z-index:2;
  display:grid;
  place-items:center;
  inline-size:var(--fvid-play-size);
  block-size:var(--fvid-play-size);
  border-radius:var(--radius-pill);
  border:1px solid color-mix(in srgb,var(--light) 52%,transparent);
  background:color-mix(in srgb,var(--dark) 34%,transparent);
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  color:var(--light);
  transition:background-color var(--fvid-tempo) var(--ease),
             border-color var(--fvid-tempo) var(--ease),
             color var(--fvid-tempo) var(--ease),
             transform var(--fvid-tempo) var(--ease);
}
/* Aceso: fundo de marca e glifo escuro, a mesma dupla do .btn-primary. */
.fvid-card:hover .fvid-play,
.fvid-card:focus-visible .fvid-play{
  background:color-mix(in srgb,var(--accent) 84%,transparent);
  border-color:var(--accent);
  color:var(--dark);
}
/* (Acima:) sem salto no hover (CLAUDE.md §6, 2026-09-29): o disco acende e não
   cresce, a mesma regra do .btn do core — botão não salta nem move. */
.fvid-play svg{
  inline-size:38%;
  /* O centro óptico de um triângulo não é o centro geométrico: centrado pelo
     cálculo ele parece deslocado para a esquerda. */
  margin-inline-start:6%;
}

/* O anel é a promessa de um gesto que só o script atende. Sem JavaScript ele
   sai de cena, e o card volta a ser o que de fato é: uma linha de índice com
   capa, título e duração. `scripting: none` é a mesma consulta que o
   core/motion.css usa para devolver a página sem script — medida, não
   hipótese (Chrome 120, Firefox 113, Safari 17). */
@media (scripting: none){.fvid-play{display:none}}

/* Some o zoom da capa e o salto do anel; ficam cor e opacidade, que não são
   deslocamento — sem elas o card deixaria de responder ao ponteiro. */
@media (prefers-reduced-motion: reduce){
  .fvid-capa{transition-duration:.01ms}
}

/* ---------------------------------------------------------------- legenda */
/* DENTRO da capa, ancorada embaixo, com véu em gradiente MASCARADO — a regra
   de forma do §6. Título e duração abaixo da foto transformariam uma peça
   gráfica em duas, e fariam a linha de base dançar entre um card de título
   curto e um de três linhas. */
.fvid-legenda{
  position:absolute;
  inset-inline:0;
  inset-block-end:0;
  z-index:2;
  display:flex;
  flex-direction:column;
  gap:6px;
  /* O RESPIRO DE CIMA É A MEDIDA CARA DESTA PEÇA, e o número saiu de medição.
     Numa coluna de 348px a capa 16:9 tem 197px de altura, e a legenda ocupa
     o que ocupa: a 54px de respiro superior ela passava de 70% da capa e a
     dobra virava uma parede de véu. A 28px, com título de duas linhas, ela
     fecha em ~100px — metade da capa, dos quais só os 44% de baixo são véu
     opaco (o resto é a máscara em degradê apagando). É a mesma proporção do
     .hgal-cap do irmão, que trabalha numa capa 4:3 e por isso pode ser mais
     generoso. Quem subir este valor devolve a parede. */
  padding:28px 20px 14px;
  /* Duas paradas mais fortes que a receita do §6 (`.9 em 44%`), e o motivo é
     medido: com a legenda em 100px, o título ocupa a faixa entre 55% e 80%
     da caixa — dentro da zona de esmaecimento. Nos 44% originais ele caía
     sobre um véu de alfa baixo, e num quadro claro (o vinhedo ao entardecer
     do preview) a leitura ficava marginal. Empurrar a opacidade para 60% da
     altura resolve sem virar tarja: o topo continua transparente. */
  background:linear-gradient(to top,
    rgba(var(--dark-rgb),.95) 0%,
    rgba(var(--dark-rgb),.72) 56%,
    transparent 100%);
  backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);
  mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.94) 60%,transparent 100%);
  -webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.94) 60%,transparent 100%);
}

/* Sem corte de linha de propósito: título truncado no meio de uma palavra é
   pior que título de três linhas. O que segura o tamanho é o teto de 12
   palavras do padrão editorial — acima disso a legenda come metade da capa,
   e o lugar de resolver isso é a copy, não o CSS. */
.fvid-titulo{
  font-family:var(--display);
  font-weight:700;
  font-size:clamp(14px,1.05vw,16px);
  line-height:1.28;
  letter-spacing:-.015em;
  color:var(--light);
}

.fvid-meta{
  display:flex;
  flex-wrap:wrap;
  align-items:baseline;
  justify-content:space-between;
  gap:4px 14px;
  font-size:10.5px;
  line-height:1.3;
  letter-spacing:.14em;
  text-transform:uppercase;
}
.fvid-serie{color:color-mix(in srgb,var(--light) 60%,transparent)}
/* A duração é a informação que decide o clique, então lê mais forte que o
   rótulo de série. `nowrap` porque "11 min" quebrado em duas linhas dentro de
   uma legenda de 11px é ilegível. */
.fvid-dur{
  color:color-mix(in srgb,var(--light) 88%,transparent);
  white-space:nowrap;
}

/* ---------------------------------------------------------------- diálogo */
/* A casca vem da .dialog-casca do core, declarada ANTES desta classe no
   markup: é ela que zera padding, borda, fundo e os limites de tamanho que o
   navegador dá de graça ao <dialog>, e que pinta o ::backdrop com --veil.
   Aqui fica só o que é FORMA desta peça.

   O <dialog> nasce `display:none` na folha do agente do usuário e só aparece
   com [open]. Escrever `.fvid-dialog{display:grid}` sem o [open] deixaria o
   reprodutor visível o tempo todo, com a página rolando por baixo — é a mesma
   armadilha do §6.5, na versão do elemento nativo. */
.fvid-dialog{
  inline-size:100%;
  block-size:100%;
  /* `hidden`, contra o `visible` da casca: o diálogo é o PALCO, e com um
     palco maior que a janela (--fvid-cromo mal calibrado, legenda em duas
     linhas) o que sobra fica dentro da tela em vez de pintar por fora dela. */
  overflow:hidden;
  /* Véu MAIS ESCURO que os 78% da casa, e isto é desenho: vídeo pede sala
     escura. O grau só se vê quando há dobra clara atrás — sobre fundo escuro
     os dois dão o mesmo pixel. Terceiro componente de vídeo a chegar nos
     mesmos 88% (showreel, depoimento em vídeo): anotado para virar token.
     O ::backdrop herda custom property do elemento que o originou, e é por
     isso que declarar --veil AQUI basta para a primitiva do core pintar o véu
     desta peça — uma regra .fvid-dialog::backdrop só para trocar a cor
     traria de volta a duplicação que a primitiva veio resolver. */
  --veil:color-mix(in srgb,var(--dark) var(--fvid-veil-mix),transparent);
}
.fvid-dialog[open]{display:grid;place-items:center}

/* O PALCO. A largura sai de três limites, e o terceiro é o que faz o quadro
   caber em tela baixa sem `max-block-size` nem letterbox: a largura é capada
   para que a altura 16:9 dela mais o cromo caibam na janela.
     1440x900  ->  min(1354, 1280, 1408) = 1280, quadro de 720
     1440x420  ->  min(1354, 1280,  554) =  554, quadro de 312
     390x844   ->  min( 367, 1280, 1308) =  367, quadro de 206
   A linha em `vh` fica antes da em `dvh` de propósito: navegador que não
   entende `dvh` descarta a segunda e mantém a primeira. */
.fvid-palco{
  position:relative;
  display:flex;
  flex-direction:column;
  gap:14px;
  inline-size:min(94vw,1280px,calc((100vh - var(--fvid-cromo)) * var(--fvid-pw) / var(--fvid-ph)));
  inline-size:min(94vw,1280px,calc((100dvh - var(--fvid-cromo)) * var(--fvid-pw) / var(--fvid-ph)));
  /* Nada entra seco (§5): a entrada carrega blur indo a zero. O transform
     mora AQUI e não num nó com [data-rv] — o diálogo é filho direto da
     seção, fora de todo invólucro de reveal, justamente para o
     `[data-rv].in{transform:none}` do core não apagá-lo em silêncio. */
  opacity:0;
  filter:blur(16px);
  transform:translateY(14px) scale(.985);
}
.fvid-palco.is-entrando{
  opacity:1;
  filter:blur(0);
  transform:none;
  transition:opacity var(--fvid-entrada) var(--ease),
             filter var(--fvid-entrada) var(--ease),
             transform var(--fvid-entrada) var(--ease);
}
/* O diálogo CONTINUA abrindo — ele foi pedido. Some só a coreografia: o palco
   nasce no lugar final, e o script nem chega a pôr a classe. */
@media (prefers-reduced-motion: reduce){
  .fvid-palco{opacity:1;filter:none;transform:none}
  .fvid-palco.is-entrando{transition:none}
}

.fvid-palco-cap{
  display:flex;
  flex-wrap:wrap;
  align-items:baseline;
  gap:4px 12px;
  /* Espaço do botão de fechar, que está fora do fluxo no canto de cima. */
  padding-inline-end:60px;
  min-block-size:44px;
}
.fvid-palco-serie{
  font-size:11px;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--fg-muted);
  white-space:nowrap;
}
.fvid-palco-titulo{
  font-family:var(--display);
  font-weight:700;
  font-size:clamp(15px,1.5vw,20px);
  line-height:1.3;
  letter-spacing:-.02em;
  color:var(--fg);
}

.fvid-quadro{
  position:relative;
  inline-size:100%;
  aspect-ratio:var(--fvid-pw) / var(--fvid-ph);
  border-radius:var(--radius-media);
  overflow:hidden;
  background:var(--dark);
}
.fvid-video{
  inline-size:100%;
  block-size:100%;
  /* `contain` e não `cover`: um vídeo vertical ou 4:3 no quadro 16:9 aparece
     inteiro, com tarja, em vez de ter as pontas cortadas. */
  object-fit:contain;
  display:block;
  background:var(--dark);
}

/* Primeiro focável do diálogo por ordem de DOM, e no canto por CSS — ver o
   comentário do markup. */
.fvid-fechar{
  position:absolute;
  inset-block-start:0;
  inset-inline-end:0;
  z-index:2;
  display:grid;
  place-items:center;
  inline-size:44px;
  block-size:44px;
  padding:0;
  border-radius:var(--radius-pill);
  border:1px solid var(--rule-2);
  background:none;
  -webkit-appearance:none;appearance:none;
  color:var(--fg-2);
  transition:color var(--fvid-tempo) var(--ease),border-color var(--fvid-tempo) var(--ease);
}
.fvid-fechar:hover{color:var(--accent);border-color:var(--accent)}
.fvid-fechar svg{
  inline-size:42%;
  stroke:currentColor;
  stroke-width:1.7;
}

/* ------------------------------------------- variação: vertical 9:16 */
/* .fvid--vertical na seção: a série é de cortes verticais — reels, corte
   de live, depoimento gravado no celular. A capa e o quadro do diálogo
   viram 9:16 pelos dois parafusos de proporção, e a coluna mínima cai para
   a grade acomodar quatro por fileira no lugar de três.
     coluna de 1096px  ->  4 colunas de ~254px (capa de 452px de altura)
     celular (abaixo)  ->  2 colunas, nunca 1: uma capa 9:16 na largura do
                           celular mede 620px e a faixa vira uma rolagem de
                           quatro telas
   O palco do diálogo já sabia lidar: a conta de largura usa a razão, e a
   1440x900 o quadro sai com ~445x792 — a altura da janela manda.
   Referência: os cortes de motion em 9:16 do portfólio do Dudes Becker. */
.fvid--vertical{
  --fvid-pw:9;
  --fvid-ph:16;
  --fvid-min:220px;
}
/* O título de uma capa estreita quebra em mais linhas: 12 palavras numa
   coluna de 254px são cinco linhas. O teto editorial desta variação é 8. */
.fvid--vertical .fvid-titulo{font-size:clamp(13.5px,1vw,15px)}

/* ---------------------------------------------------------------- mobile */
/* A capa fica 16:9 em toda largura, e isso não é esquecimento: a capa É um
   quadro do vídeo, e recortá-la para 3:2 no celular faria a peça mostrar um
   enquadramento que o vídeo não tem. O que muda é o respiro.

   Os dois parafusos são redeclarados em .fvid, e não no elemento que os lê —
   escritos no próprio .fvid-play e .fvid-palco eles venceriam o valor que o
   consumidor põe na seção, e o parafuso pareceria não funcionar. Mesma forma
   do --display-xl do core. Quem sobrescrever um deles sobrescreve os dois. */
@media (max-width: 680px){
  .fvid{--fvid-play-size:40px;--fvid-cromo:92px}
  /* Só o respiro LATERAL muda: 20px de cada lado come 11% de uma capa de
     350px. O de cima segue os 28px do desktop — a coluna do celular tem
     praticamente a mesma largura da coluna de três (350 contra 348), então a
     legenda tem de fechar na mesma altura, e um valor maior aqui devolveria
     a parede de véu exatamente onde a tela é menor. */
  .fvid-legenda{padding:28px 16px 15px;gap:7px}
  .fvid-play{inset-block-start:11px;inset-inline-end:11px}
  .fvid-palco{gap:11px}
}
/* A vertical no celular: duas colunas de ~167px em 390. --fvid-min aqui é
   o que garante as duas (2 × 150 + 16 de vão cabem em 350). O respiro de
   cima da legenda cai junto, porque a capa estreita tem menos largura para
   o mesmo título. */
@media (max-width: 680px){
  .fvid--vertical{--fvid-min:150px}
  .fvid--vertical .fvid-legenda{padding:22px 12px 12px}
}

script.js · 223 linhas · 10,3 KB

/* ==========================================================================
   FAIXA DE VÍDEOS — prefixo fvid-
   Um <dialog> por seção, um <video> dentro dele, e N cards que o alimentam.

   O QUE ESTE SCRIPT FAZ, EM ORDEM:
     1. no clique de um card, MONTA o reprodutor com o que aquele card
        declara: src, poster, legendas, título e rótulo de série;
     2. abre o <dialog> com showModal(), trava a rolagem da página e faz o
        palco entrar com blur indo a zero;
     3. toca — o play é consequência do clique, nunca de `autoplay`;
     4. no fechamento (botão, Esc ou clique no fundo), pausa E SOLTA o vídeo,
        destrava a rolagem e volta o palco ao repouso.

   O QUE NÃO ESTÁ AQUI, porque o <dialog> nativo já entrega: prender o foco
   dentro, fechar no Esc, devolver o foco ao card que abriu, e tornar o resto
   da página inerte para tecnologia assistiva. Mesmo critério do
   midia/showreel-expansivel: usar o elemento nativo em vez de reimplementar
   o comportamento dele.

   UM REPRODUTOR PARA A SEÇÃO, E NÃO UM POR CARD. Seis <video> na dobra são
   seis elementos de mídia, seis posters a gerenciar e a chance real de dois
   tocarem ao mesmo tempo. O preço de compartilhar é que TUDO tem de ser
   trocado a cada abertura — e "tudo" inclui o <track>, que é a parte que
   morde (ver abaixo).

   POR QUE O ENDEREÇO DO VÍDEO MORA NO CARD, e não numa lista aqui: o markup
   é o que o consumidor cola e edita. Uma tabela de URLs dentro do script
   seria um segundo lugar para manter em sincronia com o HTML, e o primeiro a
   ficar velho.

   NADA É GRAVADO. Sem localStorage, sem cookie, sem contagem: o que a pessoa
   assistiu é dado dela.
   ========================================================================== */

(function () {

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

  /* 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;
  }

  /* A trava de rolagem é do core, com contador — dois overlays abertos ao
     mesmo tempo não se destravam mutuamente. Guardada porque o componente
     pode estar colado sem o motion.js. */
  function travar()  { if (window.LIB && window.LIB.travarRolagem) window.LIB.travarRolagem(); }
  function destravar(){ if (window.LIB && window.LIB.soltarRolagem) window.LIB.soltarRolagem(); }

  function iniciar(raiz) {

    /* A BUSCA PARTE DA RAIZ, sempre (§7.6): o <dialog> é filho direto da
       própria <section>, e é isso que permite duas faixas de vídeo na mesma
       página sem que a segunda abra o reprodutor da primeira. */
    var dlg   = raiz.querySelector('[data-fvid-dialog]');
    var cards = [].slice.call(raiz.querySelectorAll('[data-fvid-card]'));
    if (!dlg || !cards.length) return;
    /* Sem <dialog> nativo não há reprodutor. Saída silenciosa: os cards
       continuam legíveis como índice, que é o que o CSS já entrega. */
    if (!dlg.showModal) return;

    var palco  = dlg.querySelector('[data-fvid-palco]');
    var video  = dlg.querySelector('[data-fvid-video]');
    var fechar = dlg.querySelector('[data-fvid-fechar]');
    var capSerie  = dlg.querySelector('.fvid-palco-serie');
    var capTitulo = dlg.querySelector('.fvid-palco-titulo');
    if (!palco || !video) return;

    /* Nome de fábrica do diálogo, para voltar a ele se um card não trouxer
       título. Sem isto o reprodutor ficaria com o nome do vídeo anterior. */
    var rotuloBase = dlg.getAttribute('aria-label') || 'Reprodutor de vídeo';
    var aberto = false;

    function texto(card, seletor) {
      var n = card.querySelector(seletor);
      return n ? (n.textContent || '').replace(/\s+/g, ' ').trim() : '';
    }

    /* ---------------------------------------------------------- soltar */
    /* pause() PARA O SOM E NÃO PARA O DOWNLOAD: sem remover o src e chamar
       load(), o navegador segue puxando o arquivo com o diálogo fechado —
       megabytes de alguém, sem nada na tela para justificá-los.

       removeAttribute('src'), NUNCA src=''. String vazia resolve contra a URL
       da página: o navegador tenta carregar o próprio HTML como mídia e
       dispara um erro de mídia no console. Sem `src` e sem <source>, a
       especificação manda o load() ir para NETWORK_EMPTY e ABORTAR, sem
       evento de erro — que é exatamente o que se quer.

       O load() só é chamado se HAVIA src: numa seção cujos cards ainda não
       apontam para arquivo nenhum (o caso deste preview) não há o que
       cancelar, e chamá-lo à toa é ruído. */
    function soltarVideo() {
      if (video.pause) video.pause();
      var tinha = !!video.getAttribute('src');
      video.removeAttribute('src');
      var faixa = video.querySelector('track');
      if (faixa) video.removeChild(faixa);
      if (tinha && video.load) video.load();
    }

    /* ---------------------------------------------------------- montar */
    function montar(card) {
      var src    = card.getAttribute('data-fvid-src') || '';
      var vtt    = card.getAttribute('data-fvid-vtt') || '';
      var capa   = card.querySelector('.fvid-capa');
      var titulo = texto(card, '.fvid-titulo');
      var serie  = texto(card, '.fvid-serie');
      var dur    = texto(card, '.fvid-dur');

      if (capTitulo) capTitulo.textContent = titulo;
      /* O rótulo do palco junta série e duração: no reprodutor as duas viram
         uma linha de contexto, e não dois dados a alinhar. */
      if (capSerie) capSerie.textContent = serie && dur ? serie + ' · ' + dur : (serie || dur);

      /* O nome acessível do diálogo é o título do vídeo. Sem isto o leitor de
         tela anuncia "Reprodutor de vídeo" nas seis aberturas, e quem navega
         por som não sabe qual dos seis abriu. */
      dlg.setAttribute('aria-label', titulo || rotuloBase);

      /* O POSTER É A PRÓPRIA CAPA DO CARD, lida do <img>. Não existe
         data-fvid-poster de propósito: um segundo endereço para a mesma
         imagem é um lugar a mais para divergir, e o poster certo é por
         definição a capa que foi clicada. currentSrc antes de src porque com
         srcset é ele que diz qual recorte o navegador realmente escolheu. */
      if (capa) video.poster = capa.currentSrc || capa.src;

      /* <track> NOVO a cada abertura, nunca o mesmo com o src trocado:
         trocar o src de um <track> já analisado não faz o navegador reler as
         cues, e o vídeo seguinte apareceria com as falas do anterior.
         Ele é inserido ANTES de o src do vídeo ser escrito, porque é durante
         a seleção de recurso que o `default` é honrado. */
      if (vtt) {
        var t = document.createElement('track');
        t.kind = 'captions';
        t.src = vtt;
        t.srclang = document.documentElement.lang || 'pt-BR';
        t.label = 'Legendas';
        t.default = true;
        video.appendChild(t);
      }

      if (src) video.src = src;
    }

    /* ---------------------------------------------------------- tocar */
    /* NÃO É `autoplay`, e a diferença não é semântica: o play acontece dentro
       do clique da pessoa, num vídeo que ela escolheu. Nenhum <video> desta
       peça toca sem gesto.

       O guard de `src` é o que mantém o console limpo quando o card ainda não
       aponta para arquivo nenhum — o caso deste preview, onde a biblioteca
       não versiona vídeo. Chamar play() num elemento sem mídia rejeita a
       promessa com NotSupportedError, e "capturar para descartar" é pior que
       não chamar: esconde do log um erro que continuaria acontecendo.

       O catch fica de pé mesmo assim, porque a rejeição real existe: política
       de autoplay muda por navegador e por configuração, e uma promessa
       rejeitada sem catch vira erro vermelho no console de quem só abriu um
       vídeo. */
    function tocar() {
      if (!video.play || !video.getAttribute('src')) return;
      var p = video.play();
      if (p && p.catch) p.catch(function () {});
    }

    /* ---------------------------------------------------------- abrir */
    function abrir(card) {
      soltarVideo();                 // o vídeo anterior sai antes de o novo entrar
      montar(card);
      dlg.showModal();
      travar();
      aberto = true;

      if (reduzido()) { tocar(); return; }

      palco.classList.remove('is-entrando');
      /* Dois quadros de espera, não um. Com um só, o navegador às vezes
         agrupa a escrita do estado de repouso com a remoção dele e a
         transição não acontece — o palco simplesmente aparece pronto. Mesma
         medição do midia/showreel-expansivel. */
      requestAnimationFrame(function () {
        requestAnimationFrame(function () {
          palco.classList.add('is-entrando');
          tocar();
        });
      });
    }

    /* ---------------------------------------------------------- fechar */
    /* 'close' dispara pelo botão, pelo Esc e pelo clique no fundo: um lugar
       só para pausar, soltar e destravar, em vez de três caminhos que podem
       divergir. Vídeo tocando atrás de um diálogo fechado é um som sem
       origem visível, e página travada depois de fechar é um bug que ninguém
       associa ao vídeo. */
    dlg.addEventListener('close', function () {
      if (!aberto) return;                    // close sem open não destrava nada
      aberto = false;
      soltarVideo();
      palco.classList.remove('is-entrando');
      destravar();
    });

    /* Clique no fundo fecha. O <dialog> não faz isso sozinho, e é a única
       coisa que falta nele. O alvo ser o próprio diálogo significa que o
       clique caiu FORA do palco — dentro, o alvo seria o palco ou o vídeo. */
    dlg.addEventListener('click', function (e) {
      if (e.target === dlg) dlg.close();
    });

    if (fechar) fechar.addEventListener('click', function () { dlg.close(); });

    for (var i = 0; i < cards.length; i++) {
      cards[i].addEventListener('click', function () { abrir(this); });
    }
  }

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

Três a seis vídeos curtos em grade de três colunas. Cada card é uma capa 16:9 com o título, a duração e o rótulo de episódio dentro da imagem, e um anel de play no canto. No clique, um <dialog> nativo cobre a tela e entrega um <video> com os controles do navegador.

O que ela resolve: publicar vários vídeos curtos — corte de live, comentário de mercado, aula de dez minutos — numa página que não é sobre vídeo. A pessoa precisa escolher um entre vários, e para escolher precisa saber quanto tempo cada um custa e onde ele está na série. É por isso que a duração é dado obrigatório e não enfeite.


Quando usar

Quando não usar

Por que não é o galeria-scroll-horizontal com uma thumb de vídeo

A pergunta é justa, porque as duas peças são cards de mídia numa dobra. Ela se responde com quatro diferenças que não são de aparência.

midia/faixa-videos midia/galeria-scroll-horizontal
Onde está o conteúdo no vídeo, nesta página em outra página, do outro lado do clique
Elemento do card <button> + aria-haspopup="dialog" <a href>
Duração obrigatória, em <time> e lida por leitor de tela não existe
Ordem é informação — <ol>, episódio e data explicitamente fora de ordem (capitulos-navegaveis)
Forma grade de 3 colunas, tudo visível trilho que sangra, com scroll-snap
Gesto de play anel de play, sempre visível não existe

O que decide não é a thumb: é o destino. Um card cujo conteúdo está do outro lado do clique é uma âncora, e uma âncora promete navegação — Enter navega, o meio do mouse abre em outra aba, o histórico ganha uma entrada. Um card cujo conteúdo abre aqui é um botão, e o que ele precisa entregar é o contrato de sobreposição: foco preso, Esc, foco de volta no card que abriu, página inerte. Trocar o <a> do irmão por um <button> já não seria uma variação dele — seria mudar o elemento, o aria, o teclado e o que acontece ao fechar.

Somam-se dois requisitos que uma galeria de fotos não tem e nunca vai ter. Duração: sem ela, uma lista de vídeos é uma lista de compromissos de tamanho desconhecido, e "8 min" muda a decisão de quem tem cinco minutos. Ordem de episódio: capitulos-navegaveis no irmão significa, na letra do vocabulário, "frentes que a pessoa pode querer acessar FORA de ordem" — o oposto de uma série, onde o episódio 12 é o mais novo e é por ele que se começa. Daí <ol> e não <ul>.

E há a diferença de forma que vem do uso: grade, não trilho. Um trilho esconde metade da lista fora da tela, e índice do qual não se vê o fim não é índice. Quem chega numa faixa de vídeos quer varrer a lista com o olho e escolher; quem chega num trilho de cases quer ser conduzido.


Padrão editorial

Slot Regra
Olho 2 a 3 palavras. Diz a cadência da série, não o assunto: toda quinta, aulas curtas. É a informação que o card não tem espaço para repetir seis vezes
Título 3 a 6 palavras. O nome da série, com uma palavra em <em>
Apoio 1 a 2 frases. A segunda é o lugar da ressalva de natureza do conteúdo — comentário, não recomendação
Título do vídeo 6 a 12 palavras, por item. Informa o que muda, nunca promete resultado
Duração 4 a 9 caracteres, por item. A duração real, no formato 8 min
Rótulo de série 8 a 20 caracteres, por item. Episódio, data, ou os dois: Ep. 12 · 28 ago
Alt da capa por item, no atributo alt do <img>. Descreve a cena, não o vídeo

Itens: 3 a 6. Três e seis fecham a fileira; quatro e cinco deixam a última incompleta — o que num índice que cresce toda semana lê como "tem mais vindo", e não como erro de montagem.

O título informa, não promete. O que muda na renda fixa com a Selic em queda serve. Como dobrar seu patrimônio não serve, e não é questão de gosto: é o §4 do briefing e a razão pela qual esta biblioteca não escreve superlativo de mercado.

O título vai dentro da capa, e é isso que limita o tamanho. Acima de 12 palavras ele vira três linhas e a legenda come metade da imagem. Não há corte de linha no CSS de propósito — título truncado no meio de uma palavra é pior que título de três linhas —, então o teto é da copy.

A duração é a informação que decide o clique. Ela não pode ser arredondada para ficar bonita, e não pode ficar velha quando o arquivo é trocado. Duração que não bate com o vídeo é enfeite que mente, e derruba a confiança nas outras cinco.

O rótulo de série tem a mesma forma em todos os cards. Se um traz episódio e o outro só a data, o rótulo deixa de ordenar e passa a ser decoração.

Todos os títulos escritos juntos, na mesma sessão. A legenda é o único texto do card, e comprimentos muito diferentes fazem a linha de base dançar entre um card e o vizinho.

Especificação dos assets

Capas e vídeos não atravessam (§11 do contrato): o preview usa os placeholders de showroom/demo/, e o que segue é a especificação do asset real.

Asset Especificação
Capa 16:9, 1600×900 ou maior, WebP ou JPEG. Uma por vídeo. É um quadro do próprio vídeo — e de preferência não o primeiro, que costuma ser preto ou vinheta
Vídeo mp4 com H.264, 1920×1080. Um webm adicional é bem-vindo e não obrigatório. Áudio normalizado entre os episódios: seis vídeos com volumes diferentes obrigam a mexer no controle a cada clique
Legenda um .vtt por vídeo, em data-fvid-vtt. Não é opcional onde há fala

Peso alvo: capa abaixo de 90 KB. Seis capas são o peso real desta dobra, e são a única coisa que ela baixa antes de alguém clicar.

Composição / enquadramento: o texto do card cobre a faixa inferior da capa e o anel de play ocupa o canto superior direito (46 px). Medido na coluna de 348 px: a capa tem 197 px de altura e a legenda ocupa os 100 px de baixo — metade —, dos quais só os 44% inferiores são véu opaco; o resto é a máscara em degradê apagando. Na prática: o terço de cima da capa é o único espaço limpo, e é onde tem de estar o que interessa no quadro — rosto, gesto, o gráfico de que o vídeo fala. Nada essencial na metade de baixo.

O que a capa não pode ter:


Como usar

<link rel="stylesheet" href="componentes/midia/faixa-videos/estilo.css">
<script src="componentes/midia/faixa-videos/script.js"></script>

Cole a <section> inteira — o <dialog> vem dentro dela, como último filho, e é assim que tem de ficar. Duas razões, e as duas foram medidas: é fora do .wrap e fora de todo [data-rv], então nenhum ancestral com filter o alcança; e é filho da própria seção, o que deixa o script achá-lo a partir da raiz — é isso que permite duas faixas de vídeo na mesma página, cada uma com o seu reprodutor.

O <dialog> leva três classes, nesta ordem: class="dialog-casca fvid-dialog sec-deep".

  1. dialog-casca é a primitiva do core (base.css, já na lista de dependências): zera padding, borda, fundo e os limites de tamanho que o navegador dá de graça ao <dialog>, e pinta o ::backdrop.
  2. fvid-dialog é a forma desta peça: o palco de tela cheia, o display de [open] e o grau do véu.
  3. sec-deep é escopo de tom no markup — a exceção nomeada do §3. Sem ele o diálogo cai no color: CanvasText do navegador e a legenda do palco fica preta sobre fundo escuro.

Depois, por card: troque a capa e o alt, escreva título, rótulo e duração (o texto e o datetime), e aponte data-fvid-src para o arquivo do vídeo. data-fvid-vtt para a legenda.

Ganchos

Gancho Papel
[data-fvid] a <section>. O script parte daqui, sempre
[data-fvid-card] cada <button> de capa
data-fvid-src no card — endereço do vídeo daquele item. Ausente: o diálogo abre com o poster e nada toca
data-fvid-vtt no card — endereço do .vtt daquele item. Opcional no markup, obrigatório onde há fala
[data-fvid-dialog] o <dialog> da seção
[data-fvid-palco] a caixa que entra em cena. Recebe .is-entrando
[data-fvid-video] o <video> compartilhado
[data-fvid-fechar] o botão de fechar
--fvid-min largura mínima da coluna. 320px — é ela que decide 3, 2 ou 1 coluna
--fvid-gap vão da grade
--fvid-tempo duração das transições de interação. .55s
--fvid-play-size diâmetro do anel de play. 46px, 40px no mobile
--fvid-entrada duração da entrada do palco. .5s
--fvid-veil-mix grau do véu do ::backdrop. 88% — mais escuro que os 78% da casa, e de propósito
--fvid-cromo altura que a legenda do palco e os respiros comem acima do quadro. 108px, 92px no mobile
.is-entrando dispara a entrada do palco

Os slots .fvid-palco-serie e .fvid-palco-titulo não são gancho de autoria: nascem vazios e o script os escreve com o rótulo e o título do card que abriu. Escrever neles à mão é trabalho perdido no primeiro clique.

Variações


Gotchas


Compliance

A peça não devolve número, não compara e não captura dado. O terreno regulado aqui é outro: o que ela publica é conteúdo sobre investimento, e a responsabilidade viaja no vídeo, não no card. O que o consumidor tem de trocar antes de publicar:


Movimento reduzido / toque

Com prefers-reduced-motion: reduce o diálogo continua abrindo — ele foi pedido. Some só a coreografia: o palco nasce no lugar final, sem blur nem subida, e o script nem chega a pôr a classe. Na grade, o zoom da capa e o salto do anel param; cor e opacidade ficam, porque sem elas o card deixaria de responder ao ponteiro.

No toque, nada depende de hover: o anel de play nasce visível, e é por isso que este componente não precisa do @media(hover:none) que o midia/galeria-scroll-horizontal escreve para o distintivo dele. Os controles do vídeo são os nativos, que já são os certos para tela sensível — inclusive o botão de tela cheia, que no celular é o que a pessoa realmente vai usar.

Por teclado: Tab alcança cada um dos seis cards, Enter e Espaço abrem, o foco entra no diálogo e fica preso lá, Esc fecha e o foco volta para o card que abriu. Nada disso foi escrito no script — é tudo do <dialog> com showModal().

O ciclo de Tab dentro do diálogo passa pelo <body>, e isso é nativo. Medido: fechar → vídeo → <body> → fechar, indefinidamente. O passo pelo <body> é como o Chrome dá a volta na armadilha de foco de um <dialog> modal; nenhum card e nenhum elemento da página é alcançado, porque a página está inerte. Não há o que consertar aqui, e um "consertador" que escrevesse uma armadilha própria substituiria comportamento nativo correto por código a manter.


Histórico

Projeto Onde Data Diferenças
autoral — 2026-09-09 Construído do zero. Padrão de domínio comum, sem referência única
referência: Dudes Becker https://portfolio.dudesbecker.com/portfolio — "06 / 06 Motion Design", três vídeos 9:16 2026-10-01 variação vertical 9:16; a proporção deixou de ser cravada e virou os parafusos --fvid-pw/--fvid-ph

Origem. O índice de série de vídeos curtos aparece em página de podcast em vídeo, em hub de conteúdo de casa de análise e em área de aulas curtas — padrão sem dono. Nada foi copiado: reescrito do zero dentro do contrato desta biblioteca. O registro consolidado vai em projetos/autorais.md.

Por que existe. A tag videos tinha uma peça, e ela é de um vídeo só. Pior: a ficha do midia/showreel-expansivel mandava quem tem vários vídeos para o overlay/card-expansivel-fullscreen — que é um trilho de cards cujo detalhe é texto, foto e CTA, e não toca vídeo nenhum. O encaminhamento estava errado porque o destino não existia. Agora existe, e a divisão da família de vídeo fica assim:

Peça O vídeo é
hero/video-fundo superfície da primeira dobra — textura, laço, sem som
midia/showreel-expansivel um filme, capa única, peça gráfica da dobra
prova/depoimento-video a fala de uma pessoa, com a citação legível sem clique
midia/faixa-videos um item de uma série, entre três e seis

Decisões da tradução para o contrato. Grade e não trilho, porque índice do qual não se vê o fim não é índice — e porque o trilho já é o irmão galeria-scroll-horizontal, que é de fotos e leva para outra página. <button> e não <a>, porque o destino é um reprodutor nesta página. Um <dialog> por seção com um <video> só, cujo src, poster e <track> o script troca a cada abertura, em vez de seis reprodutores inline ou seis diálogos. O <video> nasce sem src, sem <source> e com preload="none". A duração é <time datetime> com rótulo .visually-hidden na frente. A lista é <ol> com role="list". O <dialog> é filho direto da seção e fora de todo [data-rv].

Primitivas do core que a peça veste em vez de recopiar: .wrap, .sec-pad, .sec-head, .eyebrow, .sec-title, .sec-lead, .ttl, .visually-hidden, .dialog-casca, .sec-deep, e o par LIB.travarRolagem / LIB.soltarRolagem — a trava com contador, para que duas sobreposições abertas não se destravem mutuamente.

Normalizações aplicadas na extração: não se aplica — a peça é autoral, escrita já em token, prefixo e gancho do core.

Divergências conhecidas vs. produção: não se aplica — não há site de origem.

Calibragem no primeiro uso. Duas coisas pedem um olhar com o material real. A primeira é --fvid-cromo contra a legenda do palco: com título longo e fonte de tema mais larga ela vira duas linhas, e o número tem de subir junto. A segunda é a duração da entrada do palco (.5s) contra o tempo real de carregamento do vídeo — com arquivo pesado a coreografia termina antes do primeiro quadro existir, e o quadro fica preto por um instante. O poster, que o script copia da capa clicada, é o que cobre essa janela: é ele que faz o palco abrir mostrando a mesma imagem em que a pessoa clicou.