markup.html · 147 linhas · 7,9 KB
<!-- ==========================================================================
CASE COM FICHA FLUTUANTE — um caso por linha, a ficha pousada na mídia
Prefixo: cff-
Requer: core (tokens, base, motion.css, motion.js) + este estilo.css.
O script.js só é preciso na variação com vídeo em loop.
A IDEIA. Cada caso ocupa a coluna inteira: uma mídia larga — foto ou um
loop de vídeo mudo — e, pousada no canto de baixo dela, uma ficha clara
com o tema, UMA frase sobre o trabalho, quem era o cliente e as frentes
envolvidas. A frase é a peça: ela diz o que mudou, e o resto da ficha
só situa. É a grade de cases (midia/grade-cases-foto) com um caso de
cada vez e espaço para dizer alguma coisa sobre ele.
A FICHA DECLARA O PRÓPRIO TOM no markup (.sec-light), em vez de o
componente escrever cor: é um bloco claro dentro de uma dobra escura, a
exceção nomeada do §3. Trocar por .sec-paper ou .sec-deep troca a ficha
inteira, sem tocar no CSS.
O CASO INTEIRO É CLICÁVEL, MAS O LINK É O TÍTULO. O <a> mora dentro do
<h3> (o sumário de cabeçalhos do leitor de tela continua com os casos),
e um ::after dele se estica sobre o <article>. É o padrão do
prova/manchetes-imprensa, e é o que permite o botão de pausa da
variação com vídeo existir: um <button> dentro de um <a> é HTML
inválido, e aqui o botão é irmão do link, acima do ::after.
O CONTEÚDO É DE ASSESSORIA, e por isso a frase fala do TRABALHO, nunca
de rentabilidade, e o cliente não tem nome — tem um retrato em cinco
palavras. A .nota no fim é parte da peça. Ver Compliance na ficha.
========================================================================== -->
<section class="sec-dark sec-pad cff" data-cff>
<div class="wrap">
<div class="cff-cabeca">
<span class="eyebrow" data-rv>Casos</span>
<h2 class="sec-title ttl">O trabalho, <em>caso a caso</em></h2>
<p class="sec-lead" data-rv data-d="1">Três situações que chegaram à primeira conversa e o que foi feito com cada uma. Os clientes autorizaram a descrição, sem identificação.</p>
</div>
<!-- <ol>: a ordem é escolha editorial (o caso mais forte primeiro), e
role="list" devolve a semântica que o reset do core apaga no Safari
(MANUAL §6, armadilha 4). O data-rv vai no <li>, e não no <article>:
o reveal termina em filter:blur(0), que continua sendo filtro, e o
<article> é o contexto do link esticado e do botão de pausa — fica
limpo de qualquer coisa que o core escreva (MANUAL §5). -->
<ol class="cff-lista" role="list">
<li class="cff-item" data-rv>
<article class="cff-caso">
<div class="cff-midia">
<img class="cff-img" src="../../../showroom/demo/ph-wide-2.webp"
alt="" width="1600" height="900" loading="lazy" decoding="async">
</div>
<div class="cff-ficha sec-light">
<p class="cff-olho">Sucessão familiar</p>
<h3 class="cff-titulo">
<a class="cff-link" href="#caso-sucessao">Três empresas e duas gerações passaram a decidir <em>pelo mesmo protocolo</em></a>
</h3>
<p class="cff-quem">Família do agronegócio, terceira geração</p>
<ul class="cff-tags" role="list">
<li class="pill">Holding</li>
<li class="pill">Acordo de sócios</li>
<li class="pill">Política de investimento</li>
</ul>
<span class="cff-ver" aria-hidden="true">
Ver o caso
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 7H12M8 3L12 7L8 11"/></svg>
</span>
</div>
</article>
</li>
<li class="cff-item" data-rv>
<article class="cff-caso">
<div class="cff-midia">
<img class="cff-img" src="../../../showroom/demo/ph-wide-5.webp"
alt="" width="1600" height="900" loading="lazy" decoding="async">
</div>
<div class="cff-ficha sec-light">
<p class="cff-olho">Liquidez</p>
<h3 class="cff-titulo">
<a class="cff-link" href="#caso-liquidez">A venda de uma participação virou <em>renda para vinte anos</em>, com regra escrita</a>
</h3>
<p class="cff-quem">Empresária, saída de sociedade</p>
<ul class="cff-tags" role="list">
<li class="pill">Planejamento de liquidez</li>
<li class="pill">Tributação</li>
</ul>
<span class="cff-ver" aria-hidden="true">
Ver o caso
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 7H12M8 3L12 7L8 11"/></svg>
</span>
</div>
</article>
</li>
<li class="cff-item" data-rv>
<!-- VARIAÇÃO COM VÍDEO EM LOOP: troque o <img> por este bloco. O
vídeo só toca quando está na tela, mudo e em loop, e o botão de
pausa (WCAG 2.2.2) nasce [hidden] — quem o mostra é o script, e
só se houver arquivo para tocar. Sem `src` nem <source>, fica o
pôster, que é o estado deste preview.
<div class="cff-midia">
<video class="cff-video" data-cff-video muted loop playsinline
preload="none" poster="/midia/caso-internacional.webp">
<source src="/midia/caso-internacional.mp4" type="video/mp4">
</video>
</div>
<button class="btn-disc btn-play cff-pausa" type="button" data-cff-pausa hidden
data-rot-pausar="Pausar o vídeo do caso"
data-rot-tocar="Tocar o vídeo do caso"> …os dois ícones do .btn-play… </button>
-->
<article class="cff-caso">
<div class="cff-midia">
<video class="cff-video" data-cff-video muted loop playsinline preload="none"
poster="../../../showroom/demo/ph-wide-1.webp"></video>
</div>
<button class="btn-disc btn-play cff-pausa" type="button" data-cff-pausa hidden
data-rot-pausar="Pausar o vídeo do caso" data-rot-tocar="Tocar o vídeo do caso"
aria-label="Pausar o vídeo do caso">
<svg class="btn-play-pausa" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5h3v14H8zM13 5h3v14h-3z" fill="currentColor"/></svg>
<svg class="btn-play-tocar" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.5v13l10-6.5z" fill="currentColor"/></svg>
</button>
<div class="cff-ficha sec-light">
<p class="cff-olho">Patrimônio em dois países</p>
<h3 class="cff-titulo">
<a class="cff-link" href="#caso-internacional">Contas, imóveis e herdeiros em dois países <em>num só relatório</em></a>
</h3>
<p class="cff-quem">Casal de médicos, Brasil e Portugal</p>
<ul class="cff-tags" role="list">
<li class="pill">Estrutura internacional</li>
<li class="pill">Sucessão</li>
</ul>
<span class="cff-ver" aria-hidden="true">
Ver o caso
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 7H12M8 3L12 7L8 11"/></svg>
</span>
</div>
</article>
</li>
</ol>
<p class="nota cff-nota" data-rv>Casos descritos com autorização dos clientes e sem identificá-los. Descrevem o trabalho feito, não resultado financeiro, e não constituem promessa de resultado.</p>
</div>
</section>
estilo.css · 254 linhas · 9,3 KB
/* ==========================================================================
CASE COM FICHA FLUTUANTE — prefixo cff-
Um caso por linha: mídia larga e, pousada no canto de baixo dela, uma
ficha clara com tema, frase, cliente e frentes de trabalho.
Tokens usados:
--display --sans a frase e o resto da ficha
--bg --fg --fg-2 --fg-muted --rule da ficha (escopo .sec-light no
markup) e da dobra
--accent-ink o realce da frase e a seta, sobre o fundo da FICHA
--radius-media canto da mídia e da ficha
--ease a curva das respostas ao ponteiro
--dark-2 fundo da mídia enquanto a imagem não pinta
A FICHA FICA DENTRO DA IMAGEM (contrato §6, texto sobre imagem), e não
abaixo dela — em toda largura, inclusive no celular. Não é véu sobre a
foto: é um bloco sólido, de fundo do escopo dela, pousado no canto. Por
isso não tem backdrop-filter: a regra do véu (§6) existe para a caixa de
vidro sobre imagem, e uma caixa de vidro reta aqui leria "chapado". Um
bloco opaco de canto não tem borda a endurecer.
PARAFUSOS (custom properties do próprio componente, §3):
--cff-ratio proporção da mídia no desktop (16/9)
--cff-ficha-w largura da ficha
--cff-inset distância da ficha às bordas da mídia
--cff-gap vão entre um caso e o seguinte
--cff-tempo duração das respostas ao ponteiro
========================================================================== */
.cff{
--cff-ratio:16 / 9;
--cff-ficha-w:min(46%,520px);
--cff-inset:clamp(16px,2.4vw,32px);
--cff-gap:clamp(28px,5vh,56px);
--cff-tempo:.5s;
}
.cff-cabeca{margin-block-end:clamp(36px,6vh,64px)}
.cff-lista{
display:grid;
/* minmax(0,1fr), e não a coluna automática: o mínimo automático de um item
de grade é o CONTEÚDO dele, e o conteúdo aqui é um <img> de 1600px de
largura intrínseca. Medido sem isto: caso de 1307px numa coluna de
1096, e a página estourando 32px para o lado. */
grid-template-columns:minmax(0,1fr);
gap:var(--cff-gap);
}
/* O <article> é o contexto do link esticado: o ::after do .cff-link se
estende sobre ele inteiro. Mídia e ficha moram na MESMA célula de grade,
uma sobre a outra — ver o comentário da ficha, logo abaixo, para o porquê
de não ser position:absolute. */
.cff-caso{
position:relative;
display:grid;
grid-template-columns:minmax(0,1fr); /* mesma razão do .cff-lista */
border-radius:var(--radius-media);
overflow:hidden;
background:var(--dark-2);
}
/* ------------------------------------------------------------- a mídia */
.cff-midia{
grid-area:1 / 1;
aspect-ratio:var(--cff-ratio);
}
/* `block-size` em 100% e não auto, e a diferença é a do MANUAL §6
(armadilha 2): o <img> declara width e height para reservar a caixa, e o
atributo height viraria altura definida de 900px. Aqui quem manda na
altura é o .cff-midia, pelo aspect-ratio; a mídia só preenche. */
.cff-img,
.cff-video{
display:block;
inline-size:100%;
block-size:100%;
object-fit:cover;
/* Um quase nada abaixo do pleno no repouso, para o hover ter para onde
ir sem mexer na geometria (§6: o retorno mora no brilho). */
filter:brightness(.92);
transition:filter var(--cff-tempo) var(--ease);
}
.cff-caso:hover .cff-img,
.cff-caso:hover .cff-video,
.cff-caso:focus-within .cff-img,
.cff-caso:focus-within .cff-video{filter:brightness(1.04)}
/* ------------------------------------------------------------- a ficha */
/* Bloco sólido no canto de baixo. O escopo de tom vem do markup
(.sec-light), e o fundo dela é o --bg desse escopo — o componente não
escreve cor.
EMPILHADA POR GRADE, NÃO POR position:absolute, e é isso que faz o caso
inteiro ser clicável. O link mora dentro da ficha e estica um ::after
absoluto sobre o caso; um ::after absoluto se mede contra o ancestral
POSICIONADO mais próximo. Com a ficha em position:absolute, esse
ancestral seria ela, e o alvo do clique encolheria para o tamanho da
ficha — a foto deixaria de ser link sem nada acusar. Na mesma célula de
grade, a ficha não é posicionada e o ::after chega ao <article>.
O z-index funciona sem position porque item de grade aceita z-index; e
precisa dele, porque a mídia tem `filter`, e elemento com filtro é
pintado na camada dos posicionados — por cima de um bloco comum. */
.cff-ficha{
grid-area:1 / 1;
align-self:end;
justify-self:start;
margin:var(--cff-inset);
inline-size:var(--cff-ficha-w);
z-index:2;
display:grid;
gap:14px;
justify-items:start;
padding:clamp(20px,2.2vw,30px);
background:var(--bg);
border-radius:var(--radius-media);
}
.cff-olho{
font-size:11px;
letter-spacing:.16em;
text-transform:uppercase;
color:var(--fg-muted);
}
.cff-titulo{
font-family:var(--display);
font-weight:600;
font-size:clamp(19px,1.9vw,27px);
line-height:1.22;
letter-spacing:-.018em;
color:var(--fg);
}
/* O realce lê a tinta de realce DO ESCOPO DA FICHA: o fundo atrás do
glifo é o da ficha, claro, e não a foto. É a regra do MANUAL §3 — fundo
do escopo, --accent-ink; foto com véu, --accent. */
.cff-titulo em{
font-style:italic;
color:var(--accent-ink,var(--accent));
}
.cff-link{
color:inherit;
/* O filete do link, como no .ltr-cta do hero/letreiro-marca: decoração
que cresce sob o texto, sem mexer em caixa nenhuma. Aqui é sublinhado
de verdade, porque o link tem várias linhas. */
text-decoration-line:underline;
text-decoration-color:transparent;
text-decoration-thickness:1px;
text-underline-offset:.18em;
transition:text-decoration-color var(--cff-tempo) var(--ease);
}
/* O CASO INTEIRO É ALVO: o ::after estica o link sobre o <article>. É ele
que recebe o clique na foto. O anel de foco do core desenha em volta do
<a> (o texto), e o :focus-within do caso acende a foto junto — quem chega
de teclado vê o mesmo retorno de quem chega de mouse. */
.cff-link::after{
content:"";
position:absolute;
inset:0;
z-index:1;
}
.cff-caso:hover .cff-link,
.cff-link:focus-visible{text-decoration-color:currentColor}
.cff-quem{
font-size:14px;
line-height:1.5;
color:var(--fg-2);
}
.cff-tags{
display:flex;
flex-wrap:wrap;
gap:6px;
}
/* A .pill do core tem tracking largo e caixa alta; três delas numa ficha de
460px quebravam em duas linhas cada. Corpo e tracking menores aqui, sem
tocar na primitiva. */
.cff-tags .pill{font-size:10px;letter-spacing:.12em;padding:4px 10px}
.cff-ver{
display:inline-flex;
align-items:center;
gap:9px;
font-family:var(--display);
font-weight:700;
font-size:11px;
letter-spacing:.14em;
text-transform:uppercase;
color:var(--fg);
}
.cff-ver svg{color:var(--accent-ink,var(--accent))}
/* ------------------------------------------------- o botão de pausa */
/* Só na variação com vídeo. Fica ACIMA da ficha e do link esticado dela
(z-index 3 contra 2), no canto de cima à direita, longe da ficha. A cor
é de ilha escura: está sobre a mídia, não sobre o fundo da dobra.
Duas classes no seletor de propósito: .btn-disc, do core, também pinta
cor e borda com uma classe só, e empatar especificidade deixaria a cor
na mão da ordem de carregamento (§6.4). */
.cff-caso .cff-pausa{
position:absolute;
inset-block-start:var(--cff-inset);
inset-inline-end:var(--cff-inset);
z-index:3;
color:var(--light);
border-color:color-mix(in srgb,var(--light) 50%,transparent);
background:color-mix(in srgb,var(--dark) 40%,transparent);
}
.cff-caso .cff-pausa:hover{color:var(--accent);border-color:var(--accent)}
/* O .btn-play do core já desliga o [hidden] dele; esta linha é a do §6.5
para o display que a .btn-disc dá — redundante hoje, e aqui porque a
ordem de carregamento não é garantida (§6.4). */
.cff-pausa[hidden]{display:none}
.cff-nota{margin-block-start:clamp(24px,4vh,36px)}
@media (prefers-reduced-motion:reduce){
.cff-img,
.cff-video,
.cff-link{transition:none}
}
/* ---------------------------------------------------------------- mobile */
/* A FICHA CONTINUA DENTRO DA MÍDIA em toda largura, e é a MÍDIA que muda de
proporção para caber nela. Pôr a ficha embaixo da foto seria a saída
fácil e é o que o §6 proíbe: uma peça gráfica viraria duas. O
enquadramento da mídia tem de prever os recortes — ver a especificação
dos assets na ficha.
Os degraus saíram de medição, com a ficha mais alta do preview:
760px em 16:9 mídia 376, ficha 301 — a ficha cobria 80% da foto
760px em 4:3 mídia 513, ficha ~250
320px em 4:5 mídia 331, ficha 323 — a ficha passava da mídia e a
célula crescia
320px em 3:4 sem as pílulas, a ficha cabe com a foto à vista */
@media (max-width:980px){
.cff{--cff-ratio:4 / 3;--cff-ficha-w:min(62%,460px)}
}
@media (max-width:560px){
.cff{--cff-ratio:3 / 4;--cff-inset:12px}
.cff-ficha{
justify-self:stretch;
inline-size:auto;
gap:10px;
padding:18px;
}
.cff-titulo{font-size:clamp(17px,4.8vw,20px)}
.cff-quem{font-size:13px}
/* As frentes saem no celular: são o slot mais secundário da ficha, e
três pílulas em duas linhas eram a diferença entre a ficha caber ou
não na foto a 320px. O caso aberto continua dizendo quais foram. */
.cff-tags{display:none}
}
script.js · 91 linhas · 4,0 KB
/* ==========================================================================
CASE COM FICHA FLUTUANTE — cff-
Só serve à variação com vídeo em loop. Sem nenhum [data-cff-video], ou
com vídeos sem arquivo, este script não faz nada — o componente com foto
é CSS puro.
O QUE ELE FAZ, POR VÍDEO:
1. confere que há o que tocar (src ou <source>). Sem arquivo, fica o
pôster e o botão de pausa segue [hidden], como nasceu;
2. entrega o botão ao LIB.pausavel do core (WCAG 2.2.2: loop que passa
de cinco segundos pede um jeito de parar);
3. toca SÓ quando o caso está na tela e a aba está à vista — três
vídeos tocando fora da tela são três downloads e três decodificações
que ninguém vê;
4. com movimento reduzido ou economia de dados, NÃO toca sozinho: o
botão aparece no estado "tocar", e quem apertar pediu. Movimento
reduzido desliga o que anda sozinho, nunca o que a pessoa pede
(MANUAL §5).
A FONTE DA VERDADE É O PEDIDO DA PESSOA, E NÃO O <video>. Diferente do
hero/video-fundo (que acompanha os eventos play/pause do elemento), aqui
o vídeo é pausado e retomado pelo script o tempo todo — ao sair da tela,
ao trocar de aba —, e esses pauses não são a pessoa pedindo nada. O
botão só reflete o clique dela.
ESTA É A SEGUNDA PEÇA DA CASA COM LOOP DE VÍDEO E PAUSA (a primeira é o
hero/video-fundo). O gatilho de promoção do §6 é três; a contagem está na
fila do MANUAL.
========================================================================== */
(function () {
var raizes = document.querySelectorAll('[data-cff]');
if (!raizes.length) return; // guard: saída silenciosa
if (!window.LIB) return; // sem o core, fica o pôster
var economia = !!(navigator.connection && navigator.connection.saveData);
var podeSozinho = !LIB.reduce && !economia;
[].forEach.call(raizes, function (raiz) {
[].forEach.call(raiz.querySelectorAll('[data-cff-video]'), function (video) {
var caso = video.closest('.cff-caso');
var botao = caso && caso.querySelector('[data-cff-pausa]');
if (!video.getAttribute('src') && !video.querySelector('source')) return;
var pausadoPelaPessoa = !podeSozinho; // reduce: nasce parado
var naTela = false;
var quebrou = false;
function aplicar() {
if (quebrou) return;
if (naTela && !pausadoPelaPessoa && !document.hidden) {
if (video.preload !== 'auto') video.preload = 'auto';
var p = video.play();
/* Play recusado (política de autoplay, aba sem foco) devolve
promessa rejeitada; sem o catch vira erro vermelho no console. */
if (p && p.catch) p.catch(function () {});
} else if (!video.paused) {
video.pause();
}
}
var ctl = LIB.pausavel(botao, function (pausado) {
pausadoPelaPessoa = pausado;
aplicar();
});
if (ctl && !podeSozinho) ctl.trocar(true); // ícone e rótulo de "tocar"
/* Arquivo que não carrega: o <source> dispara `error` nele mesmo, e
não no <video> — daí a escuta em captura. O botão sai de cena,
porque comanda algo que não existe, e o pôster fica. */
video.addEventListener('error', function () {
quebrou = true;
if (botao) botao.hidden = true;
}, true);
/* Observer de VISIBILIDADE, não de entrada: a entrada do caso é do
data-rv do core. Este só liga e desliga o vídeo. 35% porque abaixo
disso o caso está saindo da tela e tocar é desperdício. */
if ('IntersectionObserver' in window) {
new IntersectionObserver(function (es) {
naTela = es[0].isIntersecting;
aplicar();
}, { threshold: 0.35 }).observe(video);
} else {
naTela = true;
aplicar();
}
document.addEventListener('visibilitychange', aplicar);
});
});
})();