markup.html · 171 linhas · 10,7 KB
<!-- ==========================================================================
CARTÕES 3D — o tambor de cartões que gira, para campanha institucional
Prefixo: crt-
Requer: core (tokens, base, motion.css, motion.js) + este estilo.css e
este script.js
Seis cartões num cilindro que gira em torno do eixo HORIZONTAL: o da
frente de pé, o de cima e o de baixo inclinados e recuando, como as
câmaras de um tambor. Ao lado, o nome e a descrição do acabamento em
destaque. Para lançamento de cartão, níveis de relacionamento, uma linha
de produtos com "cara de objeto".
O TAMBOR GIRA SÓ QUANDO A PESSOA PEDE: arrasto (vertical no mouse,
horizontal no dedo), as duas setas, os pontos, ↑ ↓ no teclado. Tocar no
cartão da frente o vira e mostra o verso. A roda do mouse continua
rolando a página, e nada se move no hover (CLAUDE.md §6).
PARA A ROLAGEM GIRAR O TAMBOR, acrescente data-crt-rolagem na <section>:
a seção fica alta, a grade gruda e a página gira do primeiro ao último
cartão. Só em tela larga e sem movimento reduzido.
OS ACABAMENTOS SAEM DOS TOKENS (--dark, --light, --light-2, --accent...):
trocar o tema troca os cartões. Nenhuma cor escrita à mão.
SEM JAVASCRIPT o tambor aparece parado, com o primeiro cartão de frente,
e os controles não aparecem.
========================================================================== -->
<section class="sec-dark sec-pad crt" data-crt>
<div class="wrap crt-grade">
<div class="crt-texto">
<span class="eyebrow" data-rv>Cartão Private</span>
<h2 class="sec-title ttl">Seis acabamentos, <em>um cartão só</em></h2>
<p class="sec-lead" data-rv data-d="1">Metal escovado, numeração no verso e o seu nome gravado. Você escolhe o acabamento ao abrir o relacionamento.</p>
<a class="btn btn-primary crt-cta" href="#conhecer" data-rv data-d="2">
Quero conhecer
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 7H12M8 3L12 7L8 11"/></svg>
</a>
<div class="crt-rotulo" data-rv data-d="3">
<p class="crt-conta" data-crt-conta aria-hidden="true">01 — 06</p>
<p class="crt-nome" data-crt-nome>Grafite</p>
<p class="crt-desc" data-crt-desc>Metal escovado, acabamento fosco</p>
</div>
<p class="crt-dica" data-crt-dica hidden>Arraste, use as setas ou toque no cartão para ver o verso.</p>
<p class="visually-hidden" data-crt-vivo aria-live="polite"></p>
</div>
<div class="crt-palco-slot" data-rv data-d="1">
<div class="crt-palco" data-crt-palco tabindex="0" role="region" aria-roledescription="carrossel" aria-label="Acabamentos do cartão">
<div class="crt-tambor">
<div class="crt-cartao crt-acab--grafite" style="--i:0" data-nome="Grafite" data-desc="Metal escovado, acabamento fosco" role="group" aria-roledescription="slide" aria-label="1 de 6: Grafite">
<div class="crt-giro">
<div class="crt-face crt-frente">
<span class="crt-marca">Meridiano <b>Private</b></span>
<span class="crt-chip" aria-hidden="true"></span>
<svg class="crt-onda" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M8.5 7.5a6 6 0 0 1 0 9M12 5a9.5 9.5 0 0 1 0 14M15.5 2.5a13 13 0 0 1 0 19"/></svg>
<span class="crt-numero">•••• 6769</span>
<span class="crt-titular">Ricardo Couto</span>
</div>
<div class="crt-face crt-verso" aria-hidden="true">
<span class="crt-tarja"></span>
<span class="crt-assina">•••</span>
<span class="crt-legal">Cartão de demonstração. Emissão sujeita a análise de crédito e às condições do contrato.</span>
</div>
</div>
</div>
<div class="crt-cartao crt-acab--marfim" style="--i:1" data-nome="Marfim" data-desc="Cerâmica clara, toque acetinado" role="group" aria-roledescription="slide" aria-label="2 de 6: Marfim">
<div class="crt-giro">
<div class="crt-face crt-frente">
<span class="crt-marca">Meridiano <b>Private</b></span>
<span class="crt-chip" aria-hidden="true"></span>
<svg class="crt-onda" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M8.5 7.5a6 6 0 0 1 0 9M12 5a9.5 9.5 0 0 1 0 14M15.5 2.5a13 13 0 0 1 0 19"/></svg>
<span class="crt-numero">•••• 6769</span>
<span class="crt-titular">Ricardo Couto</span>
</div>
<div class="crt-face crt-verso" aria-hidden="true">
<span class="crt-tarja"></span>
<span class="crt-assina">•••</span>
<span class="crt-legal">Cartão de demonstração. Emissão sujeita a análise de crédito e às condições do contrato.</span>
</div>
</div>
</div>
<div class="crt-cartao crt-acab--casa" style="--i:2" data-nome="Assinatura" data-desc="Na cor da casa, anodizado" role="group" aria-roledescription="slide" aria-label="3 de 6: Assinatura">
<div class="crt-giro">
<div class="crt-face crt-frente">
<span class="crt-marca">Meridiano <b>Private</b></span>
<span class="crt-chip" aria-hidden="true"></span>
<svg class="crt-onda" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M8.5 7.5a6 6 0 0 1 0 9M12 5a9.5 9.5 0 0 1 0 14M15.5 2.5a13 13 0 0 1 0 19"/></svg>
<span class="crt-numero">•••• 6769</span>
<span class="crt-titular">Ricardo Couto</span>
</div>
<div class="crt-face crt-verso" aria-hidden="true">
<span class="crt-tarja"></span>
<span class="crt-assina">•••</span>
<span class="crt-legal">Cartão de demonstração. Emissão sujeita a análise de crédito e às condições do contrato.</span>
</div>
</div>
</div>
<div class="crt-cartao crt-acab--areia" style="--i:3" data-nome="Areia" data-desc="A cor da casa lavada em papel, fosca" role="group" aria-roledescription="slide" aria-label="4 de 6: Areia">
<div class="crt-giro">
<div class="crt-face crt-frente">
<span class="crt-marca">Meridiano <b>Private</b></span>
<span class="crt-chip" aria-hidden="true"></span>
<svg class="crt-onda" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M8.5 7.5a6 6 0 0 1 0 9M12 5a9.5 9.5 0 0 1 0 14M15.5 2.5a13 13 0 0 1 0 19"/></svg>
<span class="crt-numero">•••• 6769</span>
<span class="crt-titular">Ricardo Couto</span>
</div>
<div class="crt-face crt-verso" aria-hidden="true">
<span class="crt-tarja"></span>
<span class="crt-assina">•••</span>
<span class="crt-legal">Cartão de demonstração. Emissão sujeita a análise de crédito e às condições do contrato.</span>
</div>
</div>
</div>
<div class="crt-cartao crt-acab--noite" style="--i:4" data-nome="Noite" data-desc="Preto profundo, filete na cor da casa" role="group" aria-roledescription="slide" aria-label="5 de 6: Noite">
<div class="crt-giro">
<div class="crt-face crt-frente">
<span class="crt-marca">Meridiano <b>Private</b></span>
<span class="crt-chip" aria-hidden="true"></span>
<svg class="crt-onda" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M8.5 7.5a6 6 0 0 1 0 9M12 5a9.5 9.5 0 0 1 0 14M15.5 2.5a13 13 0 0 1 0 19"/></svg>
<span class="crt-numero">•••• 6769</span>
<span class="crt-titular">Ricardo Couto</span>
</div>
<div class="crt-face crt-verso" aria-hidden="true">
<span class="crt-tarja"></span>
<span class="crt-assina">•••</span>
<span class="crt-legal">Cartão de demonstração. Emissão sujeita a análise de crédito e às condições do contrato.</span>
</div>
</div>
</div>
<div class="crt-cartao crt-acab--prata" style="--i:5" data-nome="Prata" data-desc="Titânio polido, espelhado" role="group" aria-roledescription="slide" aria-label="6 de 6: Prata">
<div class="crt-giro">
<div class="crt-face crt-frente">
<span class="crt-marca">Meridiano <b>Private</b></span>
<span class="crt-chip" aria-hidden="true"></span>
<svg class="crt-onda" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M8.5 7.5a6 6 0 0 1 0 9M12 5a9.5 9.5 0 0 1 0 14M15.5 2.5a13 13 0 0 1 0 19"/></svg>
<span class="crt-numero">•••• 6769</span>
<span class="crt-titular">Ricardo Couto</span>
</div>
<div class="crt-face crt-verso" aria-hidden="true">
<span class="crt-tarja"></span>
<span class="crt-assina">•••</span>
<span class="crt-legal">Cartão de demonstração. Emissão sujeita a análise de crédito e às condições do contrato.</span>
</div>
</div>
</div>
</div>
</div>
<div class="crt-controles" data-crt-controles hidden>
<button class="btn-disc crt-seta" type="button" data-crt-anterior aria-label="Acabamento anterior">
<svg width="16" height="16" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 12V2M3 6l4-4 4 4"/></svg>
</button>
<div class="crt-pontos" data-crt-pontos></div>
<button class="btn-disc crt-seta" type="button" data-crt-proximo aria-label="Próximo acabamento">
<svg width="16" height="16" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 2v10M3 8l4 4 4-4"/></svg>
</button>
</div>
</div>
</div>
</section>
estilo.css · 285 linhas · 13,3 KB
/* ==========================================================================
CARTÕES 3D — prefixo crt-
Seis cartões num tambor que gira em torno do eixo horizontal. O da frente
de pé; os vizinhos inclinados, recuando e escurecendo, como as câmaras de
um tambor de revólver visto de lado. Tocar no da frente o vira.
Tokens usados:
--display --sans tipografia
--ease curva do giro e do virar
--dark --dark-2 acabamentos escuros, sombra e véu de fundo
--light --light-2 acabamentos claros e tinta dos escuros
--accent --accent-2 acabamento "da casa", filete e chip
--fg --fg-2 --fg-muted rótulo e contador
--rule --rule-2 pontos
--accent-ink ponto ativo e contador sobre o escopo
--radius-pill pontos
--nav-h topo do modo por rolagem
TRÊS DECISÕES QUE EXPLICAM O ARQUIVO:
1. A POSE É UMA CONTA SOBRE UM NÚMERO. Cada cartão recebe do script
--crt-d, a distância (com fração) até a frente: 0 é o da frente, 1 o
de baixo, -1 o de cima. O CSS faz o resto: gira --crt-passo por casa em
torno do eixo do tambor. Sem script, --crt-d cai no --i do markup e o
tambor aparece parado no primeiro cartão.
2. NENHUM GRUPO NO CAMINHO DO 3D. opacity, filter e overflow num elemento
preserve-3d o achatam (grouping property) — e o cartão precisa de 3D
por dentro para virar. Por isso o escurecer dos vizinhos mora nas
FACES, que são folhas, e o recorte mora na seção, fora da perspectiva.
3. CADA CARTÃO TEM A SUA PERSPECTIVA, e o tambor é plano. Num contexto
3D só, dois cartões vizinhos se ATRAVESSAM no meio do giro — as bordas
de um cortam a face do outro por uns vinte quadros. Com perspective()
dentro do transform de cada um (todos partem do mesmo centro, então a
projeção é a mesma) e o tambor achatado, quem decide quem cobre quem é
o z-index, que o script escreve pela distância até a frente. A troca
acontece no meio do caminho, num quadro só.
4. O CARTÃO É UMA ILHA, NÃO UM ESCOPO. O acabamento escolhe fundo e tinta
com --dark/--light direto, como texto sobre foto. Ele não lê --fg,
porque um cartão marfim numa dobra escura continua marfim.
========================================================================== */
.crt{
--crt-w:clamp(260px,30vw,420px); /* largura do cartão */
--crt-h:calc(var(--crt-w) / 1.586); /* proporção ISO 7810 */
--crt-dy:calc(var(--crt-h) * .58); /* o vizinho: quanto sobe ou desce */
--crt-dy2:calc(var(--crt-h) * .3); /* dali em diante, por casa */
--crt-dz:calc(var(--crt-h) * .55); /* quanto recua por casa */
--crt-a:50deg; /* o vizinho: quanto deita */
--crt-a2:20deg; /* dali em diante, por casa */
--crt-persp:1300px; /* distância do olho */
--crt-inclina:-7deg; /* o tambor todo, no plano */
--crt-guinada:-16deg; /* o tambor todo, de lado */
--crt-dur:.8s; /* o virar */
/* O tambor sangra para cima e para baixo; quem corta é a seção. clip e
não hidden: hidden faria da seção um contêiner de rolagem, e o modo por
rolagem precisa do sticky preso à janela. */
overflow:clip;
}
.crt-grade{
display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);
gap:clamp(24px,4vw,64px);align-items:center;
}
/* ---------------------------------------------------------------- texto */
.crt-texto{display:grid;justify-items:start;gap:18px}
.crt-texto .sec-lead{max-inline-size:40ch}
.crt-cta{margin-top:6px}
.crt-cta svg{flex:none}
.crt-rotulo{
margin-top:clamp(20px,4vh,40px);padding-top:18px;
border-top:1px solid var(--rule);min-inline-size:min(100%,320px);
}
.crt-conta{
font-size:11px;letter-spacing:.16em;color:var(--accent-ink,var(--accent));
font-variant-numeric:tabular-nums;
}
.crt-nome{
margin-top:8px;
font-family:var(--display);font-weight:700;text-transform:uppercase;
font-size:clamp(20px,1.8vw,26px);letter-spacing:.04em;color:var(--fg);
}
.crt-desc{margin-top:4px;font-size:14px;color:var(--fg-2)}
.crt-dica{font-size:12.5px;line-height:1.5;color:var(--fg-muted);max-inline-size:36ch}
.crt-dica[hidden]{display:none}
/* ---------------------------------------------------------------- palco */
/* O data-rv mora no slot, e não no palco: o filtro do reveal é grouping
property e achataria a perspectiva se estivesse no mesmo nó. */
.crt-palco-slot{
position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;
align-items:center;gap:clamp(12px,2vw,28px);
}
.crt-palco{
position:relative;height:calc(var(--crt-h) * 2.1);
touch-action:pan-y; /* a página ainda rola no dedo */
cursor:grab;user-select:none;-webkit-user-select:none;
border-radius:24px;outline:none;
}
.crt-palco:focus-visible{box-shadow:0 0 0 2px var(--accent)}
.crt-palco.is-arrastando{cursor:grabbing}
.crt-tambor{position:absolute;inset:0}
.crt-cartao{
--crt-d:var(--i,0);
--crt-p:var(--i,0);
/* A curva por trechos: a primeira casa anda --crt-dy e deita --crt-a; as
seguintes, só --crt-dy2 e --crt-a2. É o que junta os de trás num maço
em vez de espalhá-los pela tela. */
--crt-d1:clamp(-1,var(--crt-d),1);
--crt-dr:calc(var(--crt-d) - var(--crt-d1));
position:absolute;left:calc(50% - var(--crt-w) / 2);top:calc(50% - var(--crt-h) / 2);
width:var(--crt-w);height:var(--crt-h);
z-index:calc(10 - var(--i,0)); /* o script reescreve pela distância */
transform-style:preserve-3d;
transform:
perspective(var(--crt-persp))
rotateZ(var(--crt-inclina))
rotateY(var(--crt-guinada))
translateY(calc(var(--crt-d1) * var(--crt-dy) + var(--crt-dr) * var(--crt-dy2)))
translateZ(calc(var(--crt-p) * var(--crt-dz) * -1))
rotateX(calc((var(--crt-d1) * var(--crt-a) + var(--crt-dr) * var(--crt-a2)) * -1));
}
/* Sem script o tambor não dobra a volta: do quarto em diante os cartões
ficariam de costas, atrás do da frente. Com script, quem some é o que
passou de duas casas e meia (.is-longe). */
.crt:not(.is-vivo) .crt-cartao:nth-child(n+4),
.crt-cartao.is-longe{visibility:hidden}
.crt-giro{
position:absolute;inset:0;transform-style:preserve-3d;
transition:transform var(--crt-dur) var(--ease);
}
.crt-cartao.is-virado .crt-giro{transform:rotateY(180deg)}
/* ---------------------------------------------------------------- faces */
.crt-face{
--crt-fundo:var(--dark-2);
--crt-tinta:var(--light);
position:absolute;inset:0;overflow:hidden;
border-radius:calc(var(--crt-w) * .045);
backface-visibility:hidden;-webkit-backface-visibility:hidden;
background:var(--crt-fundo);color:var(--crt-tinta);
font-family:var(--sans);font-size:calc(var(--crt-w) * .036);
box-shadow:
0 calc(var(--crt-w) * .05) calc(var(--crt-w) * .1) color-mix(in srgb,var(--dark) 55%,transparent),
inset 0 0 0 1px color-mix(in srgb,var(--light) 14%,transparent);
}
/* O brilho: uma faixa diagonal fixa na face, que gira junto com o cartão.
É o giro que a faz passar pela luz, e não o cursor (CLAUDE.md §6). */
.crt-face::before{
content:"";position:absolute;inset:0;pointer-events:none;
background:
linear-gradient(115deg,transparent 30%,color-mix(in srgb,var(--light) 16%,transparent) 46%,transparent 60%),
repeating-linear-gradient(90deg,color-mix(in srgb,var(--light) 3%,transparent) 0 1px,transparent 1px 3px);
}
/* O véu dos vizinhos: escurece conforme recuam. Véu opaco por cima, e não
opacity na face — face translúcida deixaria ver o cartão de trás através
do da frente no meio do giro. E na face, não no cartão (decisão 2). */
.crt-face::after{
content:"";position:absolute;inset:0;pointer-events:none;
background:var(--dark);opacity:calc(var(--crt-p) * .3);
}
.crt-verso{transform:rotateY(180deg)}
/* Frente: marca no alto, chip e aproximação à esquerda, número e titular
embaixo. Tudo em proporção da largura, para o cartão escalar inteiro. */
.crt-frente{
display:grid;grid-template-columns:auto auto 1fr;grid-template-rows:auto 1fr auto auto;
column-gap:calc(var(--crt-w) * .03);
padding:calc(var(--crt-w) * .065) calc(var(--crt-w) * .07);
}
.crt-marca{
grid-column:1 / -1;
font-family:var(--display);font-weight:600;letter-spacing:.02em;
font-size:calc(var(--crt-w) * .048);
}
.crt-marca b{font-weight:400;opacity:.7}
.crt-chip{
grid-row:2;align-self:center;
width:calc(var(--crt-w) * .12);height:calc(var(--crt-w) * .09);
border-radius:calc(var(--crt-w) * .016);
background:
linear-gradient(90deg,transparent 32%,color-mix(in srgb,var(--dark) 22%,transparent) 32% 34%,transparent 34% 66%,color-mix(in srgb,var(--dark) 22%,transparent) 66% 68%,transparent 68%),
linear-gradient(0deg,transparent 46%,color-mix(in srgb,var(--dark) 22%,transparent) 46% 54%,transparent 54%),
linear-gradient(135deg,color-mix(in srgb,var(--light-2) 70%,var(--accent)),color-mix(in srgb,var(--light-2) 40%,var(--accent-2)));
}
.crt-onda{grid-row:2;align-self:center;width:calc(var(--crt-w) * .06);height:auto;opacity:.75}
.crt-numero{
grid-column:1 / -1;
font-size:calc(var(--crt-w) * .05);letter-spacing:.14em;
font-variant-numeric:tabular-nums;
}
.crt-titular{
grid-column:1 / -1;margin-top:calc(var(--crt-w) * .02);
font-size:calc(var(--crt-w) * .03);letter-spacing:.16em;text-transform:uppercase;opacity:.75;
}
.crt-verso{display:grid;grid-template-rows:auto auto 1fr;align-content:start;padding-block:calc(var(--crt-w) * .07)}
.crt-tarja{height:calc(var(--crt-w) * .13);background:color-mix(in srgb,var(--dark) 86%,transparent)}
.crt-assina{
margin:calc(var(--crt-w) * .05) calc(var(--crt-w) * .07) 0;
padding:calc(var(--crt-w) * .012) calc(var(--crt-w) * .025);
justify-self:start;min-inline-size:55%;
font-size:calc(var(--crt-w) * .03);letter-spacing:.2em;text-align:end;
background:color-mix(in srgb,var(--crt-tinta) 12%,transparent);
}
.crt-legal{
align-self:end;margin-inline:calc(var(--crt-w) * .07);
font-size:calc(var(--crt-w) * .024);line-height:1.5;opacity:.6;max-inline-size:36ch;
}
/* ---------------------------------------------------------------- acabamentos */
.crt-acab--grafite .crt-face{
--crt-fundo:linear-gradient(140deg,color-mix(in srgb,var(--dark-2) 78%,var(--light)),var(--dark) 70%);
}
.crt-acab--marfim .crt-face{
--crt-fundo:linear-gradient(140deg,var(--light),color-mix(in srgb,var(--light-2) 88%,var(--dark)));
--crt-tinta:var(--dark);
}
.crt-acab--casa .crt-face{
--crt-fundo:linear-gradient(140deg,var(--accent),var(--accent-2));
--crt-tinta:var(--dark);
}
.crt-acab--areia .crt-face{
--crt-fundo:linear-gradient(140deg,color-mix(in srgb,var(--light-2) 78%,var(--accent)),color-mix(in srgb,var(--light-2) 58%,var(--accent-2)));
--crt-tinta:var(--dark);
}
.crt-acab--noite .crt-face{
--crt-fundo:var(--dark);
box-shadow:
0 calc(var(--crt-w) * .05) calc(var(--crt-w) * .1) color-mix(in srgb,var(--dark) 55%,transparent),
inset 0 0 0 1px var(--accent);
}
.crt-acab--noite .crt-marca b{color:var(--accent);opacity:1}
.crt-acab--prata .crt-face{
--crt-fundo:linear-gradient(120deg,color-mix(in srgb,var(--light-2) 70%,var(--dark)),var(--light) 45%,color-mix(in srgb,var(--light-2) 76%,var(--dark)));
--crt-tinta:var(--dark);
}
/* ---------------------------------------------------------------- controles */
.crt-controles{display:grid;justify-items:center;gap:14px}
.crt-controles[hidden]{display:none}
.crt-pontos{display:grid;justify-items:center;gap:4px}
/* O alvo é o botão inteiro (24px); o ponto que se vê é o ::before. */
.crt-ponto{
display:grid;place-items:center;width:24px;height:24px;padding:0;
background:none;border:0;cursor:pointer;
}
.crt-ponto::before{
content:"";width:6px;height:6px;border-radius:var(--radius-pill);
background:var(--rule-2);
transition:width .4s var(--ease),height .4s var(--ease),background-color .4s var(--ease);
}
.crt-ponto[aria-current="true"]::before{height:18px;background:var(--accent-ink,var(--accent))}
.crt-ponto:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--radius-pill)}
/* ---------------------------------------------------------------- modo por rolagem
Variação data-crt-rolagem: a seção fica alta e a grade gruda; a rolagem da
página gira o tambor. Só em tela larga e sem "reduzir movimento" — quem o
script não liga (.is-pinada) é a versão de sempre, com controles. */
.crt.is-pinada{height:calc(100svh + (var(--crt-n,6) - 1) * var(--crt-trecho,42svh))}
.crt.is-pinada .crt-grade{position:sticky;top:var(--nav-h);min-height:calc(100svh - var(--nav-h))}
.crt.is-pinada .crt-palco{cursor:default}
@media(max-width:860px){
.crt{--crt-w:min(78vw,360px);--crt-guinada:-8deg;--crt-inclina:-5deg}
/* No celular o rótulo desce para BAIXO do tambor: em cima, ele ficaria
uma tela acima do cartão que ele nomeia. display:contents solta os
filhos do bloco de texto na grade, e a ordem os rearruma. */
.crt-grade{grid-template-columns:1fr;gap:18px;justify-items:start}
.crt-texto{display:contents}
.crt-palco-slot{order:1;justify-self:stretch;grid-template-columns:1fr;margin-top:8px}
.crt-rotulo,.crt-dica{order:2}
.crt-rotulo{margin-top:0}
.crt-palco{height:calc(var(--crt-h) * 2.2)}
.crt-controles{grid-auto-flow:column;align-items:center}
.crt-pontos{grid-auto-flow:column}
.crt-ponto[aria-current="true"]::before{height:6px;width:18px}
}
script.js · 361 linhas · 14,2 KB
/* ==========================================================================
CARTÕES 3D — crt-
O 3D é CSS. Este script escreve, quadro a quadro, só três coisas em cada
cartão: --crt-d (a distância até a frente, com fração), --crt-p (o
quanto ele recuou, de 0 a 2,5) e o z-index que sai dele. E cuida da
interação:
arrasto vertical no mouse; horizontal no dedo — o vertical do dedo
é da página (touch-action:pan-y), e roubá-lo prenderia quem
só está rolando
setas ↑ ↓ ← → com o foco no palco, Home e End
botões as duas setas e um ponto por cartão
virar clique ou toque no cartão da frente, Enter ou Espaço
vizinho clique num cartão de cima ou de baixo o traz para a frente
O tambor é um CICLO: depois do último vem o primeiro, e o caminho é
sempre o curto. A posição é um número contínuo (pos); cada cartão lê a
diferença até ela dobrada para [-n/2, n/2). O salto da dobra acontece
atrás, onde o cartão já está invisível (.is-longe).
NADA ANDA SOZINHO. Não há giro automático — e por isso não há .btn-play:
o tambor só se move quando a pessoa pede.
MODO POR ROLAGEM (data-crt-rolagem): a seção fica alta, a grade gruda e a
rolagem da página vira a posição, do primeiro ao último sem ciclo. As
setas e os pontos rolam a página até o cartão. Só em tela larga e sem
movimento reduzido; fora disso, o modo de sempre.
MOVIMENTO REDUZIDO: o giro que a pessoa pede continua — é gesto dela, não
movimento imposto —, mas o encaixe resolve em poucos quadros e o arrasto
não arremessa ao soltar.
========================================================================== */
(function () {
var raizes = document.querySelectorAll('[data-crt]');
if (!raizes.length) return; // guard
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;
}
function dois(v) { return (v < 10 ? '0' : '') + v; }
function iniciar(raiz) {
var palco = raiz.querySelector('[data-crt-palco]');
if (!palco) return;
var cartoes = [].slice.call(palco.querySelectorAll('.crt-cartao'));
var n = cartoes.length;
if (n < 2) return;
var reduz = reduzido();
var elConta = raiz.querySelector('[data-crt-conta]');
var elNome = raiz.querySelector('[data-crt-nome]');
var elDesc = raiz.querySelector('[data-crt-desc]');
var elVivo = raiz.querySelector('[data-crt-vivo]');
var elDica = raiz.querySelector('[data-crt-dica]');
var elCtl = raiz.querySelector('[data-crt-controles]');
var elPontos = raiz.querySelector('[data-crt-pontos]');
var btAntes = raiz.querySelector('[data-crt-anterior]');
var btDepois = raiz.querySelector('[data-crt-proximo]');
var pos = 0; /* posição contínua do tambor, em cartões */
var alvo = null; /* destino do encaixe */
var vel = 0; /* arremesso do arrasto, em cartões por quadro */
var frente = -1; /* índice na frente, já publicado */
var laco = 0;
var dAntes = [], pAntes = [], longeAntes = [];
var pontos = [];
var pinada = false;
var dicaGesto = elDica ? elDica.textContent : '';
var dicaRolagem = 'Role a página, use as setas ou toque no cartão para ver o verso.';
var estreito = matchMedia('(max-width: 860px)');
function indice(k) { return ((k % n) + n) % n; }
/* A casa de onde o próximo comando parte: o destino em curso, se houver.
Sem isto, dois cliques rápidos pedem o mesmo destino e andam um só. */
function base() { return Math.round(alvo !== null ? alvo : pos); }
/* ------------------------------------------------------------ rótulo */
function publicar(idx) {
var c = cartoes[idx];
if (elConta) elConta.textContent = dois(idx + 1) + ' — ' + dois(n);
if (elNome) elNome.textContent = c.getAttribute('data-nome') || '';
if (elDesc) elDesc.textContent = c.getAttribute('data-desc') || '';
for (var j = 0; j < n; j++) {
if (j === idx) cartoes[j].removeAttribute('aria-hidden');
else cartoes[j].setAttribute('aria-hidden', 'true');
if (pontos[j]) {
if (j === idx) pontos[j].setAttribute('aria-current', 'true');
else pontos[j].removeAttribute('aria-current');
}
}
}
/* A região viva fala quando o tambor PARA, e não a cada cartão que passa
pela frente no meio de um arrasto. */
function anunciar() {
if (!elVivo) return;
var c = cartoes[indice(Math.round(pos))];
elVivo.textContent = (c.getAttribute('data-nome') || '') + '. ' +
(c.getAttribute('data-desc') || '') + '. ' +
(indice(Math.round(pos)) + 1) + ' de ' + n + '.';
}
/* ------------------------------------------------------------ pintura */
function pintar() {
for (var j = 0; j < n; j++) {
var d = j - pos;
if (!pinada) { /* no ciclo, o caminho curto */
d = ((d % n) + n) % n;
if (d >= n / 2) d -= n;
}
d = Math.round(d * 1000) / 1000;
var p = d < 0 ? -d : d;
var longe = p > 2.5;
if (p > 2.5) p = 2.5;
if (dAntes[j] !== d) { cartoes[j].style.setProperty('--crt-d', d); dAntes[j] = d; }
if (pAntes[j] !== p) {
cartoes[j].style.setProperty('--crt-p', p);
/* Quem está mais perto da frente fica por cima (estilo.css, 3). */
cartoes[j].style.zIndex = 100 - Math.round(p * 20);
pAntes[j] = p;
}
if (longeAntes[j] !== longe) {
cartoes[j].classList.toggle('is-longe', longe);
longeAntes[j] = longe;
}
}
var idx = indice(Math.round(pos));
if (idx !== frente) {
/* O cartão que sai da frente desvira: ninguém quer reencontrar o
verso ao dar a volta. */
if (frente > -1) cartoes[frente].classList.remove('is-virado');
frente = idx;
publicar(idx);
}
}
function quadro() {
laco = 0;
if (arrastando) { pintar(); return; }
if (vel) {
pos += vel;
vel *= .9;
if ((vel < 0 ? -vel : vel) < .01) { vel = 0; alvo = Math.round(pos); }
} else if (alvo !== null) {
var d = alvo - pos;
if ((d < 0 ? -d : d) < .001) {
pos = alvo; alvo = null;
/* Normaliza o ciclo em repouso: o número fica pequeno e o índice
não muda, porque n cartões fecham exatamente uma volta. */
if (!pinada) pos = indice(pos);
anunciar();
} else {
pos += d * (reduz ? .5 : .14);
}
}
pintar();
if (vel || alvo !== null) ligar();
}
function ligar() { if (!laco) laco = requestAnimationFrame(quadro); }
/* ------------------------------------------------------------ destinos */
function irPara(i) {
if (pinada) { rolarAte(i); return; }
var k = base();
var d = i - indice(k);
while (d > n / 2) d -= n;
while (d < -n / 2) d += n;
vel = 0;
alvo = k + d;
ligar();
}
function passo(dir) {
if (pinada) { rolarAte(Math.max(0, Math.min(n - 1, base() + dir))); return; }
vel = 0;
alvo = base() + dir;
ligar();
}
function virar() {
var c = cartoes[indice(Math.round(pos))];
c.classList.toggle('is-virado');
}
/* ------------------------------------------------------------ arrasto */
var arrastando = false, ponteiro = -1, xAnt = 0, yAnt = 0, ultimo = 0, andou = 0, eixo = '';
palco.addEventListener('dragstart', function (e) { e.preventDefault(); });
palco.addEventListener('pointerdown', function (e) {
if (pinada) return;
if (typeof e.button === 'number' && e.button !== 0) return;
arrastando = true;
ponteiro = e.pointerId;
xAnt = e.clientX; yAnt = e.clientY;
ultimo = 0; andou = 0; eixo = '';
vel = 0; alvo = null;
palco.classList.add('is-arrastando');
try { palco.setPointerCapture(ponteiro); } catch (err) {}
});
palco.addEventListener('pointermove', function (e) {
if (!arrastando || e.pointerId !== ponteiro) return;
var dx = e.clientX - xAnt, dy = e.clientY - yAnt;
xAnt = e.clientX; yAnt = e.clientY;
andou += (dx < 0 ? -dx : dx) + (dy < 0 ? -dy : dy);
/* O eixo se decide nos primeiros 6px e não muda mais: um arrasto
diagonal não soma os dois. No dedo só existe o horizontal. */
if (!eixo && andou > 6) {
eixo = (e.pointerType === 'mouse' && (dy < 0 ? -dy : dy) > (dx < 0 ? -dx : dx)) ? 'y' : 'x';
}
if (!eixo) return;
var alto = palco.offsetHeight || 400, largo = palco.offsetWidth || 400;
/* Puxar para baixo traz o de cima; puxar para a esquerda traz o próximo. */
var delta = eixo === 'y' ? -dy / (alto * .4) : -dx / (largo * .6);
pos += delta;
ultimo = delta;
ligar();
});
function soltar(e) {
if (!arrastando) return;
if (e && e.pointerId !== undefined && e.pointerId !== ponteiro) return;
arrastando = false;
palco.classList.remove('is-arrastando');
try { palco.releasePointerCapture(ponteiro); } catch (err) {}
/* Soma da progressão a .9: o arremesso anda 10 vezes o último passo.
Teto de .05 por quadro, meio cartão de escorrego: com .08, um
arrasto de 220px andava dois cartões em vez de um. */
vel = reduz ? 0 : Math.max(-.05, Math.min(.05, ultimo));
if (!vel) alvo = Math.round(pos);
ligar();
}
palco.addEventListener('pointerup', soltar);
palco.addEventListener('pointercancel', soltar);
/* Clique: no da frente, vira; num vizinho, traz para a frente. 6px de
tolerância separam o clique do arrasto. */
palco.addEventListener('click', function (e) {
if (andou > 6) return;
var no = e.target;
while (no && no !== palco) {
var i = cartoes.indexOf(no);
if (i > -1) {
if (i === indice(Math.round(pos)) && alvo === null) virar();
else irPara(i);
return;
}
no = no.parentNode;
}
});
/* ------------------------------------------------------------ teclado */
/* O eixo do tambor é o vertical, então ↑ ↓ giram — mas só com o foco no
palco, que é um gesto deliberado. Fora dele as setas rolam a página. */
palco.addEventListener('keydown', function (e) {
var k = e.key;
if (k === 'ArrowDown' || k === 'ArrowRight') { e.preventDefault(); passo(1); }
else if (k === 'ArrowUp' || k === 'ArrowLeft') { e.preventDefault(); passo(-1); }
else if (k === 'Home') { e.preventDefault(); irPara(0); }
else if (k === 'End') { e.preventDefault(); irPara(n - 1); }
else if (k === 'Enter' || k === ' ') { e.preventDefault(); virar(); }
});
/* ------------------------------------------------------------ controles */
if (elPontos) {
for (var j = 0; j < n; j++) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'crt-ponto';
b.setAttribute('aria-label', cartoes[j].getAttribute('data-nome') || String(j + 1));
(function (i) { b.addEventListener('click', function () { irPara(i); }); })(j);
elPontos.appendChild(b);
pontos.push(b);
}
}
if (btAntes) btAntes.addEventListener('click', function () { passo(-1); });
if (btDepois) btDepois.addEventListener('click', function () { passo(1); });
/* ------------------------------------------------------------ rolagem */
var desliga = null;
var grade = raiz.querySelector('.crt-grade');
var pt = 0, curso = 0, gruda = 0;
/* O curso é o trecho em que a grade está GRUDADA, e não a altura da
seção: o respiro de cima e o de baixo (.sec-pad) passam antes e depois
dele. Medido pela seção inteira, o primeiro cartão já saía girado e o
último chegava com a grade saindo da tela. Lido uma vez e a cada
resize — getComputedStyle a cada quadro de rolagem é desperdício. */
function medirCurso() {
var cs = getComputedStyle(raiz);
pt = parseFloat(cs.paddingTop) || 0;
gruda = parseFloat(getComputedStyle(grade).top) || 0;
curso = raiz.clientHeight - pt - (parseFloat(cs.paddingBottom) || 0) - grade.offsetHeight;
}
function rolarAte(i) {
var y = raiz.getBoundingClientRect().top + pageYOffset + pt - gruda + curso * (i / (n - 1));
window.scrollTo({ top: Math.round(y) + 1, behavior: reduz ? 'auto' : 'smooth' });
}
function lerRolagem() {
if (curso <= 0) return;
var k = (gruda - pt - raiz.getBoundingClientRect().top) / curso;
if (k < 0) k = 0; else if (k > 1) k = 1;
alvo = k * (n - 1);
ligar();
}
function modo() {
var quer = raiz.hasAttribute('data-crt-rolagem') && !reduz && !estreito.matches;
if (quer === pinada) return;
pinada = quer;
raiz.classList.toggle('is-pinada', pinada);
raiz.style.setProperty('--crt-n', n);
if (elDica) elDica.textContent = pinada ? dicaRolagem : dicaGesto;
if (desliga) { desliga(); desliga = null; }
if (pinada) {
medirCurso();
pos = Math.max(0, Math.min(n - 1, pos));
if (window.LIB && LIB.onScroll) desliga = LIB.onScroll(lerRolagem);
else {
addEventListener('scroll', lerRolagem, { passive: true });
desliga = function () { removeEventListener('scroll', lerRolagem); };
lerRolagem();
}
} else {
alvo = Math.round(pos);
ligar();
}
}
if (estreito.addEventListener) estreito.addEventListener('change', modo);
var pedido = 0;
addEventListener('resize', function () {
if (!pinada || pedido) return;
pedido = requestAnimationFrame(function () { pedido = 0; medirCurso(); lerRolagem(); });
}, { passive: true });
/* ------------------------------------------------------------ partida */
raiz.classList.add('is-vivo');
if (elCtl) elCtl.hidden = false;
if (elDica) elDica.hidden = false;
modo();
pintar();
}
for (var r = 0; r < raizes.length; r++) iniciar(raizes[r]);
})();