← Biblioteca componentes/prova/depoimento-video/ficha.md CódigoTela cheia
prova/depoimento-video

Depoimento em vídeo

🟢 estavel complexidade média prefixo dvid- depoimento prova-social video grade dialogo dialog flip legenda captions acessibilidade controles-nativos compliance

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

<link rel="stylesheet" href="componentes/prova/depoimento-video/estilo.css">
<script src="componentes/prova/depoimento-video/script.js"></script>

markup.html · 285 linhas · 15,0 KB

<!-- ==========================================================================
     DEPOIMENTO EM VÍDEO — grade de retratos, e a citação legível ao lado
     Prefixo: dvid-
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css + script.js

     A IDEIA. Dois a quatro depoimentos em grade. Cada um é um retrato em
     corpo grande, com o nome e a praça da pessoa DENTRO da foto, e a citação
     em texto logo abaixo — visível, sem clique. O vídeo abre num <dialog>
     nativo que cresce do lugar exato da capa.

     A CITAÇÃO NÃO ESTÁ ESCONDIDA, e essa é a decisão que manda no resto.
     Depoimento em vídeo sem versão legível é prova que metade das pessoas
     não recebe: quem não ouve, quem está no transporte com o som desligado,
     quem tem a conexão ruim, e o buscador. Então a citação é markup estático,
     e o vídeo é o reforço dela — não o contrário. Sem JavaScript esta dobra
     continua inteira: retrato, nome, praça e fala.

     LEGENDA É OBRIGATÓRIA. Cada <video> traz o <track kind="captions">
     declarado, com o `src` comentado enquanto não houver arquivo .vtt. Ver a
     ficha, seção Compliance: publicar depoimento sem legenda é publicar prova
     que uma parte do público não recebe — e o .vtt é também o único jeito de
     o texto falado existir para o buscador.

     COMPLIANCE, e aqui é o terreno mais delicado da casa. Nenhuma das falas
     de exemplo menciona retorno, ganho, percentual ou comparação com o
     mercado — depoimento que fala de resultado é promessa por interposta
     pessoa, e soa pior que a promessa da própria casa, porque parece
     verificada. Todas falam de SERVIÇO: atendimento, clareza, tempo de
     resposta, sensação de estar acompanhado. Nomes, praças e retratos são
     FICTÍCIOS. Pessoa real com rosto e nome exige autorização de uso de
     imagem e voz assinada, com escopo e prazo — pré-requisito de publicação,
     não detalhe. Ver ficha.md, seção Compliance.

     ESTRUTURA DE CADA ITEM, e por que ela é assim:

       <li class="dvid-item">
         <div class="dvid-slot" data-rv>   ← invólucro do reveal (§5)
            <button class="dvid-abrir">    ← a capa inteira é o botão
            <blockquote class="dvid-quote">
         </div>
         <dialog class="dialog-casca dvid-dialog">   ← irmão do invólucro
       </li>

     O <dialog> mora DENTRO do <li> e FORA do [data-rv], e as duas coisas são
     de propósito:
       · dentro do <li>, porque o script acha o diálogo de cada capa por
         `closest('.dvid-item')`. Não há id, não há índice e não há chave para
         alguém errar: acrescentar um depoimento é duplicar UM <li> com o
         diálogo dele dentro, e trocar a ordem dos itens não desemparelha
         nada. O overlay/modal-dialogo casa por id porque lá o diálogo mora no
         fim do <body>, longe do gatilho; aqui o par é vizinho.
       · fora do [data-rv], por PRECAUÇÃO — e a medição merece ser lida antes
         de alguém "simplificar" isto. O §5 manda pôr tudo que é `fixed` fora
         do reveal, porque [data-rv] termina em filter:blur(0) e elemento com
         filtro é bloco de contenção para descendente position:fixed. Um
         <dialog> modal É `fixed`, mas vive na TOP LAYER — e a top layer
         escapa da contenção: medido no Chrome, com o diálogo dentro do
         invólucro e o invólucro com um blur(10px) de verdade, a caixa saiu
         [0,0,1425,900] nos dois arranjos e o diálogo pintou NÍTIDO. É a
         primeira coisa `fixed` da casa que escapa da armadilha do §5, e
         escapa por comportamento de especificação recente. Manter o diálogo
         fora do reveal custa uma <div> e continua valendo em qualquer
         navegador que trate a top layer como filho comum.

     A CAPA É UM <button>, não um <div> com onclick: teclado, Enter, Espaço e
     o anúncio de "botão" vêm de graça. E o nome acessível dele é o próprio
     texto da capa, precedido de um trecho só para leitor de tela — por isso
     o nome da pessoa não é escrito duas vezes em nenhum lugar deste arquivo.
     O aria-label do <dialog> também sai daí, escrito pelo script.

     O POSTER DO <video> NÃO É O MESMO ARQUIVO DA CAPA, e no uso real é. Em
     produção os dois saem do mesmo frame: a capa é o recorte em retrato, e o
     poster do vídeo é o frame inteiro, em 16/9. Aqui são dois arquivos porque
     showroom/demo/ guarda retrato (ph-*) e paisagem (ph-wide-*) separados, e
     o preview tem de mostrar a geometria real dos dois quadros — com um
     retrato no palco de 16/9 o quadro apareceria com tarja preta dos dois
     lados e pareceria erro de componente.

     O <video> NÃO TRAZ <source> NESTE PREVIEW. A biblioteca não versiona
     vídeo em showroom/demo/, então o que se vê é o poster real e os controles
     nativos, sem arquivo por trás. Abertura, foco, Esc e fechamento funcionam
     e podem ser conferidos; só a reprodução não.
     ========================================================================== -->

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

    <div class="sec-head">
      <span class="eyebrow" data-rv>Depoimentos</span>
      <h2 class="sec-title ttl">Ouça de quem <em>já decidiu</em></h2>
      <p class="sec-lead dvid-lead" data-rv data-d="1">Clientes falando do atendimento que recebem, sem roteiro nosso. A citação abaixo de cada retrato é o que a pessoa diz no vídeo — que abre no clique, com legenda.</p>
    </div>

    <ul class="dvid-grid">

      <!-- ------------------------------------------------------------ 01 -->
      <li class="dvid-item">
        <div class="dvid-slot" data-rv data-d="1">

          <button class="dvid-abrir" type="button" data-dvid-abrir aria-haspopup="dialog">
            <!-- Este trecho é o começo do nome acessível do botão: o leitor de
                 tela anuncia "Assistir ao depoimento em vídeo de Helena
                 Fontoura, empresária…". O resto vem do texto visível da capa,
                 que é o mesmo que o olho lê. -->
            <span class="visually-hidden">Assistir ao depoimento em vídeo de</span>

            <!-- alt vazio de propósito: o nome da pessoa está no texto do
                 próprio botão, logo abaixo. Um alt descritivo aqui faria o
                 leitor de tela anunciar a mesma pessoa duas vezes.
                 O recorte é por foto, no style, como no
                 prova/carrossel-depoimentos — retrato de estúdio tem o rosto
                 no terço de cima, foto de ambiente tem em outro lugar. -->
            <img class="dvid-poster" src="../../../showroom/demo/ph-1.webp" alt=""
                 style="--dvid-poster-pos:72% 30%" loading="lazy" decoding="async">
            <span class="grain" aria-hidden="true"></span>
            <span class="dvid-veu" aria-hidden="true"></span>

            <span class="dvid-play" aria-hidden="true">
              <svg viewBox="0 0 24 24" focusable="false">
                <path d="M8 5.5v13l11-6.5z" fill="currentColor"/>
              </svg>
            </span>

            <!-- A ORDEM DO DOM NÃO É A ORDEM VISUAL: a duração vem depois do
                 perfil aqui, e a grade do CSS a devolve para a linha do nome.
                 O motivo é o nome acessível do botão, que é o texto dele lido
                 em ordem de documento. Medido no texto do botão: com a
                 duração no meio saía "Helena Fontoura 1 min 12 s Empresária —
                 embalagens Joinville, SC"; nesta ordem sai a pessoa inteira e
                 o tempo no fim, que é como se fala. -->
            <span class="dvid-quem">
              <span class="dvid-name">Helena Fontoura</span>
              <span class="dvid-perfil">
                <span class="dvid-role">Empresária — embalagens</span>
                <span class="dvid-onde">Joinville, SC</span>
              </span>
              <span class="dvid-dur">1 min 12 s</span>
            </span>
          </button>

          <!-- A fala é o <blockquote>; a atribuição fica FORA dele, na capa.
               As duas coisas seguem a especificação do elemento — quem é
               citado não vai dentro da citação — e aqui a atribuição ganhou
               o lugar mais útil, colada ao rosto de quem fala. -->
          <blockquote class="dvid-quote">
            <p>Antes eu tinha quatro gerentes ligando para oferecer produto. Hoje tenho um assessor que me liga para explicar o que mudou e por quê. A conversa passou a ser sobre a minha empresa, não sobre a prateleira do banco.</p>
          </blockquote>

        </div>

        <dialog class="dialog-casca dvid-dialog" data-dvid-dialog aria-label="Depoimento em vídeo">
          <div class="dvid-frame" data-dvid-frame>
            <video class="dvid-video" data-dvid-video controls playsinline preload="none"
                   poster="../../../showroom/demo/ph-wide-1.webp">
              <!-- <source src="/midia/depoimento-01.mp4" type="video/mp4"> -->
              <!-- LEGENDA: o <track> fica declarado mesmo sem arquivo, para
                   que a falta apareça a quem for publicar. Descomente o src
                   junto com o <source> acima. -->
              <track kind="captions" srclang="pt-BR" label="Português" default>
              <!-- src="/midia/depoimento-01.pt-BR.vtt" -->
            </video>

            <button class="dvid-fechar" type="button" data-dvid-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>
          </div>
        </dialog>
      </li>

      <!-- ------------------------------------------------------------ 02 -->
      <li class="dvid-item">
        <div class="dvid-slot" data-rv data-d="2">

          <button class="dvid-abrir" type="button" data-dvid-abrir aria-haspopup="dialog">
            <span class="visually-hidden">Assistir ao depoimento em vídeo de</span>

            <img class="dvid-poster" src="../../../showroom/demo/ph-5.webp" alt=""
                 style="--dvid-poster-pos:34% 24%" loading="lazy" decoding="async">
            <span class="grain" aria-hidden="true"></span>
            <span class="dvid-veu" aria-hidden="true"></span>

            <span class="dvid-play" aria-hidden="true">
              <svg viewBox="0 0 24 24" focusable="false">
                <path d="M8 5.5v13l11-6.5z" fill="currentColor"/>
              </svg>
            </span>

            <span class="dvid-quem">
              <span class="dvid-name">Rui Vasconcelos</span>
              <span class="dvid-perfil">
                <span class="dvid-role">Sócio — agronegócio</span>
                <span class="dvid-onde">Uberaba, MG</span>
              </span>
              <span class="dvid-dur">1 min 40 s</span>
            </span>
          </button>

          <blockquote class="dvid-quote">
            <p>Levei três meses de dúvida até a primeira reunião e saí dela com a política de investimentos escrita. O que me convenceu não foi promessa nenhuma: foi ver o custo da assessoria na mesa antes de eu decidir qualquer coisa.</p>
          </blockquote>

        </div>

        <dialog class="dialog-casca dvid-dialog" data-dvid-dialog aria-label="Depoimento em vídeo">
          <div class="dvid-frame" data-dvid-frame>
            <video class="dvid-video" data-dvid-video controls playsinline preload="none"
                   poster="../../../showroom/demo/ph-wide-2.webp">
              <!-- <source src="/midia/depoimento-02.mp4" type="video/mp4"> -->
              <track kind="captions" srclang="pt-BR" label="Português" default>
              <!-- src="/midia/depoimento-02.pt-BR.vtt" -->
            </video>

            <button class="dvid-fechar" type="button" data-dvid-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>
          </div>
        </dialog>
      </li>

      <!-- ------------------------------------------------------------ 03 -->
      <li class="dvid-item">
        <div class="dvid-slot" data-rv data-d="3">

          <button class="dvid-abrir" type="button" data-dvid-abrir aria-haspopup="dialog">
            <span class="visually-hidden">Assistir ao depoimento em vídeo de</span>

            <img class="dvid-poster" src="../../../showroom/demo/ph-4.webp" alt=""
                 style="--dvid-poster-pos:76% 24%" loading="lazy" decoding="async">
            <span class="grain" aria-hidden="true"></span>
            <span class="dvid-veu" aria-hidden="true"></span>

            <span class="dvid-play" aria-hidden="true">
              <svg viewBox="0 0 24 24" focusable="false">
                <path d="M8 5.5v13l11-6.5z" fill="currentColor"/>
              </svg>
            </span>

            <span class="dvid-quem">
              <span class="dvid-name">Marina Bueno</span>
              <span class="dvid-perfil">
                <span class="dvid-role">Família — segunda geração</span>
                <span class="dvid-onde">Recife, PE</span>
              </span>
              <span class="dvid-dur">58 s</span>
            </span>
          </button>

          <blockquote class="dvid-quote">
            <p>Somos seis herdeiros e cada um tinha uma opinião. O comitê trimestral acabou com a conversa de bastidor: todos leem o mesmo relatório consolidado e ouvem a mesma pessoa responder, na frente uns dos outros.</p>
          </blockquote>

        </div>

        <dialog class="dialog-casca dvid-dialog" data-dvid-dialog aria-label="Depoimento em vídeo">
          <div class="dvid-frame" data-dvid-frame>
            <video class="dvid-video" data-dvid-video controls playsinline preload="none"
                   poster="../../../showroom/demo/ph-wide-5.webp">
              <!-- <source src="/midia/depoimento-03.mp4" type="video/mp4"> -->
              <track kind="captions" srclang="pt-BR" label="Português" default>
              <!-- src="/midia/depoimento-03.pt-BR.vtt" -->
            </video>

            <button class="dvid-fechar" type="button" data-dvid-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>
          </div>
        </dialog>
      </li>

    </ul>

    <!-- A ressalva fica ONDE A PESSOA LÊ A PROVA, e não no rodapé da página.
         Ela não é enfeite jurídico: é o que informa que estas pessoas
         autorizaram o uso da imagem e que nenhuma delas está falando de
         rentabilidade. Reescreva com o compliance da casa antes de publicar. -->
    <p class="nota dvid-nota" data-rv>Depoimentos sobre a experiência de atendimento, gravados com autorização de uso de imagem e voz. Não tratam de rentabilidade e não são recomendação de investimento.</p>

  </div>
</section>

estilo.css · 400 linhas · 15,9 KB

/* ==========================================================================
   DEPOIMENTO EM VIDEO — prefixo dvid-
   Grade de dois a quatro retratos. Nome e praca DENTRO da foto, citacao em
   texto abaixo dela, e o video num <dialog> que cresce do lugar da capa.

   Tokens usados:
     --dark --dark-2 --light   as superficies e a tinta sobre foto
     --fg-2 --fg-muted        citacao e ressalva, pelo escopo da secao
     --veil                   redeclarado no dialogo, para o ::backdrop da casca
     --accent                  realce do play no hover e no foco
     --display --ease          face do nome e a curva do projeto
     --radius-media            canto da capa e do quadro
     --radius-pill             o disco de play e o de fechar
     --veil                    veu do ::backdrop, pintado pela .dialog-casca

   Parafusos (custom properties deste componente):
     --dvid-cols        colunas da grade. 3 no padrao
     --dvid-gap         vao entre os cards
     --dvid-ratio       proporcao da capa. 4/5 (retrato)
     --dvid-frame-ratio proporcao do quadro do video no dialogo. 16/9
     --dvid-play        diametro do disco de play
     --dvid-dur         tempo da abertura do quadro
     --dvid-veil-mix    grau do veu do ::backdrop
     --dvid-poster-pos  recorte da foto, POR CARD, no style do <img>
     --dvid-dx --dvid-dy --dvid-s   a diferenca capa/quadro, ESCRITAS PELO SCRIPT

   O QUE NAO ESTA AQUI, porque vem do core: a coluna (.wrap), o ritmo
   (.sec-pad), o cabecalho centrado (.sec-head), o olho, o titulo, o apoio,
   o grao, o texto so' para leitor de tela (.visually-hidden) e a casca do
   <dialog> (.dialog-casca — padding, borda, fundo, limites de tamanho e o
   ::backdrop com --veil).
   ========================================================================== */

.dvid{
  --dvid-cols:3;
  --dvid-gap:clamp(20px,2.4vw,34px);
  --dvid-ratio:4 / 5;
  --dvid-frame-ratio:16 / 9;
  --dvid-play:64px;
  --dvid-dur:.52s;
  /* Veu MAIS ESCURO que os 78% da casa, pela mesma razao registrada no
     midia/showreel-expansivel: video pede sala escura. O numero e' o dele, e
     a medicao tambem — 88% contra 78% valem 11 pontos de L* sobre uma dobra
     clara e ZERO sobre dobra escura, porque o grau do veu so' se ve quando
     ha claro atras. Nao remedi: reaproveitei. */
  --dvid-veil-mix:88%;
}

/* O apoio do cabecalho e' um bloco de 60ch dentro de um .sec-head de 760px:
   sem isto ele encosta a esquerda com o texto centrado, e a dobra fica
   torta por dois pixels que ninguem sabe explicar. */
.dvid-lead{margin-inline:auto}

/* ---------------------------------------------------------------- a grade */
/* minmax(0,1fr) e nao 1fr: com 1fr o minimo automatico e' o conteudo, e uma
   citacao com uma palavra longa alarga a coluna e estoura a grade. */
.dvid-grid{
  display:grid;
  grid-template-columns:repeat(var(--dvid-cols),minmax(0,1fr));
  gap:var(--dvid-gap);
  margin-top:clamp(36px,5vh,56px);
}

/* O depoimento e' o unico conteudo da biblioteca que pode PRECISAR sair do ar
   de um dia para o outro: autorizacao de imagem vence, a pessoa pede para
   tirar. Quem faz isso poe [hidden] no <li> — e sem esta linha o
   display:flex de baixo venceria o [hidden] do navegador e o item
   continuaria na tela, com o atributo la' no inspetor (contrato §6.5). */
.dvid-item{display:flex;flex-direction:column}
.dvid-item[hidden]{display:none}

/* O involucro do reveal: o data-rv vive AQUI, e nao no <li>, para que o
   <dialog> fique IRMAO dele e nao descendente. O §5 do contrato manda isso
   para tudo que e' position:fixed, porque [data-rv] termina em filter:blur(0)
   e elemento com filtro e' bloco de contencao para descendente fixo.

   MEDIDO, e a medicao surpreende: um <dialog> modal escapa dessa contencao,
   porque vive na TOP LAYER. Com o dialogo dentro do involucro e o involucro
   carregando blur(10px) de verdade, a caixa saiu [0,0,1425,900] nos dois
   arranjos e o dialogo pintou nitido. A separacao fica por precaucao — custa
   uma <div> e continua valendo em navegador que trate a top layer como filho
   comum. Ver a ficha, secao Gotchas. */
.dvid-slot{display:flex;flex-direction:column;min-width:0}

/* ---------------------------------------------------------------- a capa */
.dvid-abrir{
  position:relative;
  display:block;
  inline-size:100%;
  padding:0;
  border:0;
  overflow:hidden;
  border-radius:var(--radius-media);
  aspect-ratio:var(--dvid-ratio);
  background:var(--dark-2);
  cursor:pointer;
  /* O <button> alinha o conteudo como texto: sem isto a foto ganha um filete
     de descida embaixo. Mesmo caso da .reel-capa do showreel. */
  line-height:0;
  /* Tinta da capa: e' texto sobre FOTO, e por isso nao le --fg. O escopo de
     tom manda no que esta' fora da imagem — a citacao e a ressalva —, nao no
     que esta' por cima dela, onde a unica referencia e' a propria foto. */
  color:var(--light);
  text-align:start;
}

.dvid-poster{
  inline-size:100%;
  block-size:100%;
  object-fit:cover;
  /* O recorte e' por pessoa, no style do <img>: retrato de estudio tem o
     rosto no terco de cima, frame de entrevista tem em outro lugar. Mesma
     receita do --depo-face-pos do prova/carrossel-depoimentos. */
  object-position:var(--dvid-poster-pos,50% 28%);
  display:block;
  transition:filter .5s var(--ease);
}

/* 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. */
.dvid-abrir:hover .dvid-poster,
.dvid-abrir:focus-visible .dvid-poster{filter:brightness(.84)}

/* Veu mascarado em gradiente, como manda o §6: caixa reta com corte seco
   produz borda dura e le "chapado". A altura e' generosa porque o que ele
   sustenta sao duas linhas de texto, nao uma. */
.dvid-veu{
  position:absolute;
  inset-block-end:0;
  inset-inline:0;
  block-size:56%;
  background:linear-gradient(to top,color-mix(in srgb,var(--dark) 92%,transparent),transparent);
  mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.9) 46%,transparent 100%);
  -webkit-mask-image:linear-gradient(to top,#000 0%,rgba(0,0,0,.9) 46%,transparent 100%);
  pointer-events:none;
}

/* O disco de play. Fica no centro optico da capa, um pouco acima do meio
   geometrico: embaixo mora o nome, e um disco centrado de verdade parece
   colado nele. */
.dvid-play{
  position:absolute;
  inset-block-start:44%;
  inset-inline-start:50%;
  transform:translate(-50%,-50%);
  display:grid;
  place-items:center;
  inline-size:var(--dvid-play);
  block-size:var(--dvid-play);
  border-radius:var(--radius-pill);
  border:1px solid color-mix(in srgb,var(--light) 48%,transparent);
  background:color-mix(in srgb,var(--dark) 34%,transparent);
  /* backdrop-filter sem mascara e' proibido em CAIXA RETA (§6) — o motivo da
     regra e' a borda dura. Num disco nao ha' borda reta para endurecer, e o
     precedente e' a .reel-play do showreel. */
  backdrop-filter:blur(10px);
  -webkit-backdrop-filter:blur(10px);
  transition:transform .5s var(--ease),background-color .5s var(--ease),border-color .5s var(--ease);
}

/* 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. */
.dvid-abrir:hover .dvid-play,
.dvid-abrir:focus-visible .dvid-play{
  background:color-mix(in srgb,var(--accent) 30%,transparent);
  border-color:color-mix(in srgb,var(--accent) 70%,transparent);
}

.dvid-play svg{
  inline-size:34%;
  /* O triangulo de play e' optico, nao geometrico: centrado pelo calculo ele
     parece deslocado para a esquerda. */
  margin-inline-start:8%;
}

/* ---------------------------------------------------------------- a identidade */
/* Nome e praca vao DENTRO da imagem, ancorados embaixo (§6: legenda de card
   de midia nao vira paragrafo abaixo da foto). Aqui isso vale duas vezes: o
   nome pertence ao rosto, e e' ele que responde "de quem e' o video que eu
   vou abrir" sem o olho sair do retrato.

   GRADE, e nao flex em duas linhas: a ordem do DOM e' nome, perfil, duracao,
   porque o nome acessivel do botao sai do texto dele lido em ordem de
   documento — e a duracao no meio da identidade atrapalha o anuncio. A grade
   devolve a duracao para a primeira linha, a direita, sem que o markup
   precise de um involucro por linha. */
.dvid-quem{
  position:absolute;
  inset-block-end:0;
  inset-inline:0;
  display:grid;
  grid-template-columns:minmax(0,1fr) auto;
  column-gap:10px;
  row-gap:4px;
  padding:clamp(16px,1.6vw,22px);
  line-height:1.3;
}

.dvid-name{
  grid-area:1 / 1;
  font-family:var(--display);
  font-weight:700;
  font-size:clamp(1rem,1.35vw,1.16rem);
  letter-spacing:-.01em;
  color:var(--light);
}

/* A duracao nao e' enfeite: e' a informacao que decide o clique. "Assistir"
   sem duracao e' convite para o desconhecido. */
.dvid-dur{
  grid-area:1 / 2;
  align-self:baseline;
  font-size:.72rem;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:color-mix(in srgb,var(--light) 64%,transparent);
}

.dvid-perfil{
  grid-area:2 / 1 / 3 / 3;
  display:flex;
  flex-wrap:wrap;
  column-gap:8px;
  font-size:.82rem;
  color:color-mix(in srgb,var(--light) 72%,transparent);
}

/* O ponto separador e' do CSS e nao do markup: escrito la', ele viraria um
   no de texto que a skill de montagem teria de preservar entre dois slots. */
.dvid-onde::before{content:"·";margin-inline-end:8px}

/* ---------------------------------------------------------------- a citacao */
/* A VERSAO LEGIVEL DO DEPOIMENTO, e a razao de forma da peca inteira: ela e'
   markup estatico, aparece sem clique e sobra quando o video nao toca. Nao
   ha' regra de altura nem corte com reticencias — a fala tem o tamanho que
   tem, e o padrao editorial da ficha e' que mantem os cards parelhos. */
.dvid-quote{
  margin:clamp(16px,1.8vw,22px) 0 0;
  padding:0;
}

/* Seletor de elemento como descendente do prefixo, em conteudo de prosa: o
   §6.3 permite exatamente isto, e o texto de um depoimento e' de quem
   escreve conteudo, nao de quem escreve markup. */
.dvid-quote p{
  font-size:clamp(15px,1.1vw,16.5px);
  line-height:1.62;
  color:var(--fg-2);
}

/* ---------------------------------------------------------------- ressalva */
/* Tipografia e medida vem da primitiva .nota do core — os 74ch que esta peca
   ja' escrevia a mao sao exatamente o padrao dela. Fica so' a posicao. */
.dvid-nota{
  margin:clamp(30px,3.8vh,46px) auto 0;
  text-align:center;
}

/* ---------------------------------------------------------------- o dialogo */
/* A casca vem da primitiva .dialog-casca do core, declarada ANTES desta
   classe no markup: e' ela que zera padding, borda, fundo e os limites de
   tamanho que o navegador da' de graca ao <dialog>, e que pinta o
   ::backdrop com --veil. Aqui fica so' o que e' FORMA desta peca.

   O <dialog> nasce display:none na folha do agente do usuario, e QUALQUER
   regra de autor com display vence essa. Escrever `.dvid-dialog{display:grid}`
   sem o [open] deixaria os tres dialogos visiveis o tempo todo, empilhados
   sobre a pagina: e' a armadilha do §6.5 na versao do elemento nativo. */
.dvid-dialog{
  inline-size:100%;
  block-size:100%;
  /* hidden, e nao o overflow:visible da casca: o dialogo e' o PALCO, e um
     quadro maior que a janela (a variacao de video vertical) tem de ficar
     dentro da tela em vez de pintar por fora dela. */
  overflow:hidden;
  /* O ::backdrop herda custom property do elemento que o originou: declarar
     --veil aqui basta para a primitiva do core pintar o veu deste
     componente, sem uma regra ::backdrop propria. */
  --veil:color-mix(in srgb,var(--dark) var(--dvid-veil-mix),transparent);
}

.dvid-dialog[open]{
  display:grid;
  place-items:center;
}

.dvid-frame{
  position:relative;
  inline-size:min(94vw,1240px);
  aspect-ratio:var(--dvid-frame-ratio);
  max-block-size:86vh;
  border-radius:var(--radius-media);
  overflow:hidden;
  background:var(--dark);
  /* Os tres valores sao escritos pelo script: a diferenca entre onde a capa
     esta' e onde o quadro vai ficar. O transform sai para a identidade no
     quadro seguinte, e e' isso que faz o video CRESCER do lugar do retrato
     em vez de aparecer no meio da tela.
     A escala e' UNIFORME, calculada pela largura: capa em 4/5 e quadro em
     16/9 nao tem a mesma proporcao, e escalar cada eixo por conta propria
     esticaria a imagem. O que se ve e' uma faixa da largura do retrato
     abrindo no centro dele — a leitura certa para video. */
  transform:translate(var(--dvid-dx,0px),var(--dvid-dy,0px)) scale(var(--dvid-s,1));
  transform-origin:50% 50%;
  will-change:transform;
}

.dvid-frame.is-abrindo{
  transition:transform var(--dvid-dur) var(--ease);
  transform:none;
}

.dvid-video{
  inline-size:100%;
  block-size:100%;
  /* contain, e nao cover: cortar rosto de quem esta' dando depoimento e' a
     pior troca possivel. Video vertical entra com tarja e legivel. */
  object-fit:contain;
  display:block;
  background:var(--dark);
}

/* Nao e' o .btn-disc do core, e o base.css ja' registra por que: aquele e'
   um disco de contorno que le --fg-2 e --rule-2, tokens do escopo de tom.
   Este botao mora sobre VIDEO, cujo quadro pode ser qualquer coisa — um
   contorno tenue desaparece num frame claro. Dai as cores de --light/--dark
   e o desfoque, como no .reel-fechar do showreel. */
.dvid-fechar{
  position:absolute;
  inset-block-start:14px;
  inset-inline-end:14px;
  display:grid;
  place-items:center;
  inline-size:44px;
  block-size:44px;
  border-radius:var(--radius-pill);
  border:1px solid color-mix(in srgb,var(--light) 34%,transparent);
  background:color-mix(in srgb,var(--dark) 56%,transparent);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  color:var(--light);
  cursor:pointer;
  /* Acima dos controles nativos do video, que o navegador desenha por cima
     do proprio elemento. */
  z-index:2;
  transition:border-color .3s var(--ease),background-color .3s var(--ease);
}

.dvid-fechar:hover{
  border-color:color-mix(in srgb,var(--light) 70%,transparent);
  background:color-mix(in srgb,var(--dark) 74%,transparent);
}

.dvid-fechar svg{
  inline-size:44%;
  stroke:currentColor;
  stroke-width:1.7;
}

/* ---------------------------------------------------------------- movimento reduzido */
/* O dialogo continua ABRINDO — ele foi pedido. Some so' o que anda sozinho:
   o quadro aparece no lugar final sem crescer, e a capa para de escalar.
   O video tambem nao toca sozinho, e isso e' no script. */
@media(prefers-reduced-motion:reduce){
  .dvid-frame{transform:none;will-change:auto}
  .dvid-frame.is-abrindo{transition:none}
  .dvid-poster,.dvid-play,.dvid-fechar{transition:none}
  .dvid-abrir:hover .dvid-poster,
  .dvid-abrir:focus-visible .dvid-poster{filter:none}
}

/* ---------------------------------------------------------------- toque */
/* Toque nao tem hover: o disco de play e a identidade JA' nascem visiveis
   nesta peca, e nao ha' nada que apareca so' no ponteiro. O que muda e' o
   tamanho do alvo — 44px e' o minimo de area de toque. */
@media(hover:none){
  .dvid-play{--dvid-play:56px}
}

/* ---------------------------------------------------------------- mobile */
@media(max-width:1080px){
  /* Duas colunas, e a capa DEIXA de ser 4/5 junto. Com duas colunas ela dobra
     de largura, e em 4/5 passaria de 580px de altura: o par retrato + fala
     nao cabe mais no mesmo campo de visao, que e' justamente o que esta peca
     existe para garantir. Medido a 1024x800: em 4/5 a dobra tem 1924px, em
     1/1 tem 1704px, e o par retrato + fala cabe em 581px. E' a mesma conta da variacao "dois depoimentos". */
  .dvid{--dvid-cols:2;--dvid-ratio:1 / 1}
}

@media(max-width:680px){
  /* Uma coluna. A capa deixa de ser 4/5 porque em coluna cheia um retrato
     de 4/5 passa de 400px de altura e empurra a citacao para fora da tela:
     o par retrato + fala tem de caber junto no mesmo campo de visao. */
  .dvid{--dvid-cols:1;--dvid-ratio:4 / 3;--dvid-play:56px}
  .dvid-grid{gap:clamp(28px,7vw,40px)}
  .dvid-nota{text-align:left}
}

script.js · 147 linhas · 7,1 KB

/* ==========================================================================
   DEPOIMENTO EM VIDEO — dvid-
   Abre o <dialog> nativo de cada capa e faz o quadro do video CRESCER do
   lugar exato do retrato.

   O QUE NAO ESTA' AQUI, porque o <dialog> nativo ja' entrega: prender o foco
   dentro do dialogo, fechar no Esc, devolver o foco ao botao que abriu (por
   isso nao ha' nenhuma variavel `opener` neste arquivo) e tornar o resto da
   pagina inerte para tecnologia assistiva. Mesmo criterio do
   midia/showreel-expansivel e do overlay/modal-dialogo.

   O QUE O NATIVO NAO FAZ, e por isso esta' aqui:
     1. crescer do lugar da capa (FLIP);
     2. fechar ao clique no fundo;
     3. tocar ao abrir e PAUSAR ao fechar;
     4. travar a rolagem da pagina atras (LIB.travarRolagem, do core);
     5. dizer de quem e' o depoimento no nome acessivel do dialogo.

   COMO O PAR CAPA/DIALOGO E' RESOLVIDO, e por que nao por id nem por indice:
   cada <li class="dvid-item"> carrega a sua capa e o seu <dialog>, e o
   dialogo de uma capa e' `closest('.dvid-item')` dela. Sem id, dois destes
   componentes convivem na mesma pagina (§7.6) sem colisao; sem indice,
   trocar a ordem dos itens ou apagar um nao desemparelha ninguem — e
   desemparelhar aqui seria mostrar o video da pessoa errada, um erro que
   nada acusa. O overlay/modal-dialogo casa por id porque la' o dialogo mora
   no fim do <body>, longe do gatilho; aqui o par e' vizinho.
   ========================================================================== */
(function () {
  var capas = document.querySelectorAll('[data-dvid-abrir]');
  if (!capas.length) return;                     // guard: saida silenciosa

  var reduz = !!(window.LIB && window.LIB.reduce);

  /* A trava vem do core desde 2026-09-06: LIB.travarRolagem escreve
     overflow:hidden no <html>, guarda o valor anterior e CONTA as travas —
     inclusive as de outra sobreposicao da mesma pagina. O guard nao e' zelo:
     um consumidor pode colar markup, estilo e script sem o motion.js. Sem a
     trava a pagina rola atras do video, o que e' feio; sem o guard o video
     nao abre, o que e' bug. */
  function travar() {
    if (window.LIB && window.LIB.travarRolagem) window.LIB.travarRolagem();
  }
  function soltar() {
    if (window.LIB && window.LIB.soltarRolagem) window.LIB.soltarRolagem();
  }

  /* O play e' consequencia de um clique, entao o navegador COSTUMA permitir.
     "Costuma": politica de autoplay muda por navegador e por configuracao, e
     uma promessa rejeitada sem catch vira erro vermelho no console de quem
     so' abriu um depoimento. */
  function tocar(video) {
    if (!video || !video.play) return;
    var p = video.play();
    if (p && p.catch) p.catch(function () {});
  }

  for (var i = 0; i < capas.length; i++) ligar(capas[i]);

  function ligar(capa) {
    var item = capa.closest ? capa.closest('.dvid-item') : null;
    if (!item) return;                           // par incompleto: silencio
    var dlg = item.querySelector('[data-dvid-dialog]');
    if (!dlg || !dlg.showModal) return;          // idem
    var quadro = dlg.querySelector('[data-dvid-frame]');
    var video = dlg.querySelector('[data-dvid-video]');
    var fechar = dlg.querySelector('[data-dvid-fechar]');
    if (!quadro) return;

    /* O nome acessivel do dialogo sai DA CAPA, e nao de um aria-label escrito
       a mao. Assim o nome da pessoa existe num lugar so' no markup: quem
       monta a pagina com a copy de um cliente troca o .dvid-name e o dialogo
       acompanha. O markup ja' traz "Depoimento em vídeo" como base, que e' o
       que vale se o script nao rodar. */
    var nome = capa.querySelector('.dvid-name');
    if (nome && nome.textContent) {
      dlg.setAttribute('aria-label', 'Depoimento em vídeo de ' + nome.textContent.trim());
    }

    capa.addEventListener('click', function () {
      if (dlg.open) return;
      dlg.showModal();          // showModal, nunca show: e' ele que traz o
      travar();                 // foco preso, o ::backdrop e a pagina inerte

      /* Com movimento reduzido nao ha' coreografia (o CSS zera o transform do
         quadro) e nao ha' o que medir. E O VIDEO NAO TOCA SOZINHO: quem pediu
         menos movimento nao pediu um filme comecando na cara. Os controles
         nativos estao la', e o play e' um gesto. E' a divergencia deliberada
         em relacao ao showreel, que toca sempre — ver a ficha. */
      if (reduz) return;

      /* FLIP: mede onde a capa esta' e onde o quadro ficou, e escreve a
         DIFERENCA como transform. O quadro comeca desenhado sobre o retrato.
         A escala e' UNIFORME, tirada da largura: a capa e' 4/5 e o quadro
         16/9, e escalar cada eixo por conta propria esticaria a imagem. */
      var a = capa.getBoundingClientRect();
      var b = quadro.getBoundingClientRect();
      var escala = b.width ? a.width / b.width : 1;

      quadro.classList.remove('is-abrindo');
      quadro.style.setProperty('--dvid-dx', ((a.left + a.width / 2) - (b.left + b.width / 2)).toFixed(1) + 'px');
      quadro.style.setProperty('--dvid-dy', ((a.top + a.height / 2) - (b.top + b.height / 2)).toFixed(1) + 'px');
      quadro.style.setProperty('--dvid-s', escala.toFixed(4));

      /* Dois quadros de espera, nao um. Com um so', o navegador as vezes
         agrupa a escrita do transform inicial com a remocao dele e a
         transicao nao acontece — o quadro simplesmente aparece no lugar
         final. Medido no showreel, e vale igual aqui. */
      requestAnimationFrame(function () {
        requestAnimationFrame(function () {
          quadro.classList.add('is-abrindo');
          tocar(video);
        });
      });
    });

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

    /* Clique no fundo fecha — a unica coisa que falta ao <dialog>. O
       ::backdrop nao recebe evento: quem recebe e' o proprio <dialog>, que o
       CSS faz cobrir a janela. Alvo igual ao dialogo significa que o clique
       caiu FORA do quadro; dentro, o alvo seria o quadro, o video ou o botao.

       O mousedown tambem e' conferido, e nao e' excesso: quem arrasta a barra
       de progresso do video e solta o mouse fora do quadro produz um clique
       cujo alvo e' o dialogo — e o video fecharia no meio de uma busca de
       trecho. Mesma protecao do overlay/modal-dialogo. */
    var alvoDown = null;
    dlg.addEventListener('mousedown', function (e) { alvoDown = e.target; });
    dlg.addEventListener('click', function (e) {
      if (e.target === dlg && alvoDown === dlg) dlg.close();
      alvoDown = null;
    });

    /* 'close' dispara pelo botao, pelo Esc e pelo clique no fundo: um lugar
       so' para pausar o video, soltar a rolagem e desfazer o FLIP, em vez de
       tres caminhos que podem divergir. Video tocando atras de um dialogo
       fechado e' um som sem origem visivel. */
    dlg.addEventListener('close', function () {
      if (video && video.pause) video.pause();
      soltar();
      quadro.classList.remove('is-abrindo');
      quadro.style.removeProperty('--dvid-dx');
      quadro.style.removeProperty('--dvid-dy');
      quadro.style.removeProperty('--dvid-s');
    });
  }
})();

Dois a quatro depoimentos em grade. Cada um é um retrato em corpo grande com o nome e a praça da pessoa dentro da foto, um disco de play no centro e a citação em texto logo abaixo — visível, sem clique. No clique, o vídeo cresce do lugar exato do retrato dentro de um <dialog> nativo, com legenda declarada e os controles do navegador.

O que ele resolve: servir a prova mais forte que existe — uma pessoa falando — sem torná-la refém do vídeo. A citação é markup estático e é a versão legível do depoimento: é ela que sobra quando o som está desligado, quando a conexão não segura o arquivo, quando a pessoa não ouve, e é ela que o buscador lê. O vídeo é o reforço da citação, não o contrário.


Quando usar

Quando não usar

Qual dos três de prova em citação

prova/depoimento-video prova/carrossel-depoimentos midia/showreel-expansivel
Conteúdo vídeo e citação em texto citação em texto vídeo
Itens 2 a 4, em grade 3 a 6, em esteira 1
Atribuição nome, perfil e praça nome e perfil não tem
Movimento em repouso nenhum a esteira anda sozinha nenhum
JavaScript precisa (o diálogo) não usa precisa
Legenda obrigatória não se aplica obrigatória se há fala

Padrão editorial

Dois a quatro itens. Um só não é grade — é o midia/showreel-expansivel. Cinco começa a virar catálogo de depoimentos, e aí a esteira do prova/carrossel-depoimentos lê melhor.

Slot Onde Faixa Regra
Olho .eyebrow 1 a 2 palavras o tipo de prova: Depoimentos, Na voz deles
Título h2.sec-title 4 a 7 palavras uma ou duas em <em>. Não cite a quantidade — o mesmo bloco roda com 2, 3 ou 4 itens, e "três clientes contam" mente na primeira troca
Apoio .sec-lead 1 a 2 frases enquadra o conjunto: quem são estas pessoas e o que foram convidadas a falar. Se disser que a fala é livre, ela tem de ser livre
Nome .dvid-name 2 a 3 palavras por item. Nome e sobrenome, sem título acadêmico e sem empresa colada
Perfil .dvid-role 12 a 28 caracteres por item. É o perfil, não o cargo: Empresária — embalagens, Família — segunda geração
Praça .dvid-onde 8 a 22 caracteres por item. Cidade e UF — de onde a pessoa é
Duração .dvid-dur 4 a 12 caracteres por item. A duração real do arquivo: 1 min 12 s, 58 s
Citação .dvid-quote p 180 a 250 caracteres por item. O trecho mais forte da fala, sem aspas
Ressalva .dvid-nota 14 a 28 palavras uma só, no pé da dobra

A citação é o slot mais importante da peça, e não é resumo do vídeo: é transcrição do trecho mais forte da fala. Escreva sem aspas — o retrato e o <blockquote> já dizem que é fala — e sem corrigir a pessoa. Mantenha as citações a menos de 20% de diferença de comprimento entre si: em grade, uma fala muito mais longa que as vizinhas desalinha o pé dos cards.

A duração não é enfeite. É a informação que decide o clique: "assistir" sem duração é um convite para o desconhecido. É também a única coisa desta peça que o visitante pode conferir na hora — confira depois da edição final.

Nome e praça ficam no texto, e isso é regra, não estilo. Depoimento anônimo não é prova, e fala sem endereço é frase solta. Os dois vão dentro da foto, ancorados embaixo (§6 do contrato: legenda de card de mídia não vira parágrafo abaixo da imagem), e o nome fica onde ele é mais útil — colado ao rosto de quem fala.

O nome da pessoa é escrito uma vez só. O nome acessível do botão da capa sai do texto visível dela, precedido de "Assistir ao depoimento em vídeo de" só para leitor de tela; e o aria-label do <dialog> é escrito pelo script a partir do .dvid-name. Medido — o texto do botão em ordem de documento, que é de onde o nome é computado: Assistir ao depoimento em vídeo de Helena Fontoura Empresária — embalagens Joinville, SC 1 min 12 s; e no diálogo, Depoimento em vídeo de Helena Fontoura. Quem monta a página com a copy de um cliente troca um nó por pessoa.


Especificação dos assets

Asset Especificação
Vídeo um por depoimento. mp4 com H.264, 1920×1080. Fala captada em microfone de lapela — depoimento com áudio de câmera não se salva na edição
Capa (retrato) 1200×1500 ou maior, recorte em retrato de um frame do próprio vídeo: a pessoa olhando para a câmera, sem legenda queimada, sem logo, sem terço inferior de televisão. Não use o primeiro frame — ele costuma ser preto ou o meio de uma piscada
Poster do <video> 1600×900, o mesmo frame da capa sem o recorte. É o que segura o palco até o arquivo chegar
Legenda um .vtt por vídeo, em <track kind="captions">. Obrigatório — ver Compliance
Autorização uso de imagem e voz assinado por pessoa, com escopo e prazo. Pré-requisito de publicação

Neste preview a capa e o poster são arquivos diferentes, e no uso real são o mesmo frame. showroom/demo/ guarda retrato (ph-*) e paisagem (ph-wide-*) em arquivos separados, e o preview precisa mostrar a geometria real dos dois quadros: com um retrato no palco de 16/9 o vídeo apareceria com tarja preta dos dois lados e pareceria erro de componente.

Nenhum vídeo é versionado na biblioteca, então o <source> está comentado. O que se vê é o poster e os controles nativos: abertura, crescimento, foco, Esc e fechamento funcionam e podem ser conferidos; só a reprodução não.


Compliance

Depoimento de cliente em página de assessoria de investimentos é o terreno mais delicado da biblioteca. O que tem de ser revisto antes de publicar:

  1. Nenhum depoimento fala de retorno, ganho, percentual ou comparação com o mercado. "Ganhei X%" é promessa de resultado por interposta pessoa, e é pior do que a casa dizer, porque parece verificado. O depoimento fala de serviço: atendimento, clareza, tempo de resposta, sensação de estar acompanhado. Isso vale para a citação em texto e para a fala gravada — a legenda torna o áudio pesquisável, e um número dito no minuto 1 não fica escondido.
  2. Autorização de uso de imagem e voz assinada, por pessoa, com escopo (site, campanha, redes) e prazo. Rosto e nome de pessoa real sem isso não vão ao ar. Guarde também o consentimento para a transcrição: a citação é publicação de fala.
  3. Legenda é obrigatória. Publicar depoimento sem .vtt é publicar prova que uma parte do público não recebe — e é o único caminho para o conteúdo mais persuasivo da página existir para o buscador. O <track kind="captions"> já está declarado no markup, com o src comentado: a falta aparece a quem for publicar em vez de passar em branco.
  4. A ressalva do pé da dobra é ponto de partida, não parecer. Ela diz duas coisas: que houve autorização, e que os depoimentos não tratam de rentabilidade nem são recomendação. Reescreva com o compliance da casa.
  5. Confirme se o seu registro permite depoimento de cliente. O regime de assessoria, consultoria e gestão não é o mesmo, e em alguns deles a divulgação de declaração de cliente é restrita. A peça não decide isso — quem decide é o jurídico da casa, antes de haver gravação.
  6. Sem nome de instituição real, sem logo de banco e sem marca de terceiro na fala, na legenda ou na imagem.
  7. Nada é gravado no navegador. A peça não escreve localStorage, não tem formulário e não mede nada de quem assiste. Se o consumidor plugar analytics no clique de play, o dado é dele e o aviso de privacidade dele.

Os nomes, praças, retratos e falas de exemplo são fictícios, escritos para a voz da Liberta Investimentos. Nenhum é cliente real.


Como usar

<link rel="stylesheet" href="componentes/prova/depoimento-video/estilo.css">
<script src="componentes/prova/depoimento-video/script.js"></script>

Cole a <section> inteira. O <dialog> de cada depoimento mora dentro do <li> dele — não o mova para o fim do <body>: é a vizinhança que dispensa id, e é ela que faz "acrescentar um depoimento" ser duplicar um <li>.

O <dialog> leva duas classes, na ordem class="dialog-casca dvid-dialog". A primeira é a primitiva do core (base.css): zera padding, borda, fundo e os limites de tamanho que o navegador dá de graça ao <dialog>, e pinta o ::backdrop. A segunda é a forma desta peça — o palco de tela cheia, o display de [open] e o grau do véu.

Depois: troque as fotos e o --dvid-poster-pos de cada uma, descomente <source> e o src do <track>, e ajuste nome, perfil, praça, duração e citação.

Ganchos

Gancho O quê
.dvid a seção. Carrega os parafusos, e é o único lugar onde se mexe
.dvid-item um depoimento: o invólucro do reveal e o <dialog> dele, lado a lado
.dvid-slot o invólucro do [data-rv]. O <dialog> é irmão dele, nunca filho
[data-dvid-abrir] a capa inteira, que é um <button>
[data-dvid-dialog] o <dialog> daquele item, achado por closest('.dvid-item')
[data-dvid-frame] o quadro que cresce. Recebe as três custom properties
[data-dvid-video] o <video>, para tocar ao abrir e pausar ao fechar
[data-dvid-fechar] o botão de fechar
.is-abrindo dispara a transição do quadro para a identidade
--dvid-cols colunas da grade. 3; 2 até 1080px, 1 até 680px
--dvid-gap vão entre os cards
--dvid-ratio proporção da capa. 4/5; 1/1 de 1080px para baixo, quando a grade vira duas colunas; 4/3 em coluna única
--dvid-frame-ratio proporção do quadro do vídeo. 16/9
--dvid-play diâmetro do disco de play. 64px; 56px no toque e no celular
--dvid-dur tempo da abertura do quadro. .52s
--dvid-veil-mix grau do véu do ::backdrop. 88% — mais escuro que os 78% da casa, e de propósito
--dvid-poster-pos recorte da foto, por card, no style do <img>
--dvid-dx / --dvid-dy / --dvid-s a diferença entre a capa e o quadro, escritas pelo script

Variações

Variação Como
Dois depoimentos --dvid-cols:2 e --dvid-ratio:1 / 1. Em duas colunas a capa passa de 500px de largura, e em 4/5 ela ficaria com mais de 600px de altura — o retrato empurra a citação para fora do campo de visão, que é o que esta peça existe para evitar
Quatro depoimentos --dvid-cols:4 e --dvid-ratio:1 / 1. A citação cai para o piso da faixa (180 caracteres). O quarto item repete data-d="3": o core só escalona o atraso até três
Vídeo vertical --dvid-frame-ratio:9 / 16, para depoimento gravado no celular. O object-fit:contain já daria conta com tarja; o quadro em retrato aproveita a tela inteira
Em seção clara trocar .sec-dark por .sec-paper. Citação e ressalva leem tokens de escopo e acompanham; a capa não muda, porque a tinta dela é sobre foto e não lê --fg
Vídeo hospedado fora trocar o <video> por um <iframe>. Perde-se a pausa ao fechar, a legenda passa a ser responsabilidade do arquivo lá, e a CSP do site precisa liberar o frame

Gotchas


Movimento reduzido / toque

Com prefers-reduced-motion: reduce, o diálogo continua abrindo — ele foi pedido. Some o que anda sozinho: o quadro aparece no lugar final sem crescer (o script não mede nem escreve transform), a capa para de escalar no hover e as transições vão a 0s.

E o vídeo não toca sozinho — esta é a divergência deliberada em relação ao midia/showreel-expansivel, que toca sempre. Quem pediu menos movimento não pediu um filme começando na cara; os controles nativos estão ali, e o play é um gesto. Medido com --reduce: diálogo aberto, transform: none, --dvid-dx nunca escrita, video.paused continua true.

No toque, a capa é um <button> e responde como qualquer botão. Não há nada nesta peça que apareça só no ponteiro: o disco de play, o nome, a praça, a duração e a citação nascem visíveis. O que muda em @media(hover:none) é o tamanho do disco (56px). Os controles do vídeo são os nativos, inclusive a tela cheia — que no celular é o que a pessoa vai realmente usar, porque um quadro 16/9 na largura de um telefone em pé tem 206px de altura (medido em 390×844).

Por teclado: Tab chega a cada capa, Enter ou Espaço abre, o foco entra no diálogo e fica preso lá (dois nós focáveis: o <video> e o botão de fechar), Esc fecha e o foco volta para a capa que abriu. Tudo isso é do <dialog>.

Sem JavaScript a dobra continua inteira: retrato, nome, perfil, praça, duração, citação e ressalva são markup estático, e o componente não esconde nada por conta própria — o único estado invisível de repouso é o [data-rv] do core, que o motion.css devolve em @media(scripting:none). O que se perde é só o vídeo, que já estava atrás de um clique.


Histórico

Origem Quando O quê
autoral 2026-09-09 Construído do zero. A grade de retratos com play é forma de domínio comum, sem referência única

Origem. A fileira de retratos com botão de play, cada um abrindo um vídeo, aparece em site de SaaS, de escola e de clínica — padrão de domínio comum, sem dono. Nada foi copiado: reescrito do zero dentro do contrato desta biblioteca. O registro consolidado vai em projetos/autorais.md.

Por que existe ao lado dos irmãos. A biblioteca tinha depoimento em texto (prova/carrossel-depoimentos) e vídeo sem atribuição (midia/showreel-expansivel), e nenhum dos dois servia um depoimento gravado: o primeiro não tem vídeo, o segundo não tem nome, praça nem citação — e não tem grade. A lacuna era exatamente a prova mais forte que existe.

O que foi reaproveitado do midia/showreel-expansivel, como método: o <dialog> nativo com showModal() em vez de um <div role="dialog">; o FLIP medindo capa e quadro e escrevendo a diferença em custom properties, com dois requestAnimationFrame de espera; o play() dentro de catch; o display morando em [open]; o clique no fundo conferido por e.target === dlg; e o grau de véu de 88% com a medição dele.

O que é diferente, e por quê:

  1. A citação em texto é visível e é a razão de forma da peça. No showreel o conteúdo é o vídeo, e ponto. Aqui a versão legível vem primeiro na ordem de importância.
  2. N diálogos, um por item, vizinhos da sua capa dentro do <li>. O showreel tem um. O overlay/modal-dialogo casa gatilho e diálogo por id, porque lá o diálogo mora no fim do <body>; aqui o par é vizinho e o script resolve por closest('.dvid-item') — sem id (dois destes componentes convivem na mesma página) e sem índice (trocar a ordem dos itens não desemparelha nada, e desemparelhar aqui seria mostrar o vídeo da pessoa errada, um erro que nada acusa). Medido com duas cópias da seção na mesma página: 6 capas, 1 diálogo aberto, no item certo da cópia certa.
  3. Sob movimento reduzido o vídeo não toca sozinho.
  4. A escala do FLIP é uniforme e tirada da largura, porque capa e quadro têm proporções diferentes — no showreel os dois são 16/9 e a morfose é exata.
  5. A trava de rolagem vem do core (LIB.travarRolagem), que não existia quando o showreel foi escrito.
  6. O nome da pessoa é escrito uma vez só, e o aria-label do diálogo sai dele pelo script.

Verificação (2026-09-09, bin/medir.js). 1440×900: altura da seção 1189px, overflowX ok, console limpo, nenhuma imagem sem pintar; três capas em 343×428 nas colunas 165 / 541 / 918. Diálogo aberto: position:fixed, caixa [0,0,1425,900] mesmo com o <dialog> dentro do .wrap; quadro 1240×698; ::backdrop com 0.88 de alfa e blur(6px). 390×844: uma coluna de 350px, capa 350×263 (4/3), disco de 56px, quadro 367×206 dentro da tela, botão de fechar em 44×44, overflowX ok com o diálogo aberto. --reduce: nada anima e o vídeo não toca. Hover: poster em matrix(1.04) e brightness(0.84), disco em matrix(1.08) — os dois em descendente do [data-rv], que computa transform:none e filter:blur(0px) no invólucro, como o §5 prevê.

Calibragem no primeiro uso. Espere ajustar --dvid-dur (.52s) contra o tempo real de carregamento do vídeo: com arquivo pesado a coreografia termina antes de o primeiro frame existir, e o quadro fica preto por um instante. O poster do <video> cobre essa janela — é para isso que ele é obrigatório.