feat(equipa): o cartão de detalhe nasce da foto, estica e segue o rato

O cartão abre por cima da própria foto (mesmo sítio, tamanho e inclinação),
endireita-se e estica para o lado — para a esquerda nas fotos da ponta
direita — com o texto a entrar por trás. Aberto, inclina-se em 3D e desliza
uns píxeis na direção do rato; ao sair volta a dobrar-se para dentro da foto.
No telemóvel voa da foto para o centro (FLIP) e volta ao fechar.

- clip-path em px calculados no JS: o Chrome não interpola inset() com
  calc(100% - …) e o recorte saltava em vez de esticar.
- O cartão é posicionado sem transição antes de abrir; senão a transição do
  estado fechado arrancava primeiro e a abertura partia de valores antigos.
- Tira o «Saber mais» do cartão da equipa.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Opus 5.5 committed 2026-09-24 21:28:32 +01:00
1 parent 1614bf327b
commit 90ee2a93bc
3 files changed
+237 -84

No files matched your search

+134 -35
View File
@@ -224,36 +224,105 @@ document.addEventListener('DOMContentLoaded', function () {
});
/* ── Equipa: horizontal detail card ─────────────────────────────────────────
Desktop (fine pointer) opens it on hover purely in CSS; this handles tap /
click / keyboard (.is-open, one at a time; Esc, the × and outside clicks
close) and nudges the absolutely-positioned card sideways (--td-shift) so an
edge column's card stays inside the viewport. On small / touch screens the
CSS turns it into a centred fixed card, so no shift is applied there. */
/* ── Equipa: detail card that grows out of the tile ─────────────────────────
Desktop: hover (or keyboard focus) opens. The card is laid exactly over the
tile photo, tilted like it, and the CSS unclips/straightens it sideways —
to the right, or to the left (.td-flip) when there is no room. While open,
the mouse drives a small 3D tilt + drift so it handles like a real card.
Touch / narrow screens: tap opens a centred fixed card; a FLIP animation
makes it fly out of (and back into) the tile photo. Esc, ×, outside tap close. */
document.addEventListener('DOMContentLoaded', function () {
var items = document.querySelectorAll('.trainer-item.has-detail');
if (!items.length) return;
var floating = window.matchMedia('(max-width: 767px), (hover: none)');
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
var section = document.getElementById('equipa');
var CARD_W = 560, GUTTER = 16;
function tileRot(item) {
// #powerRotation tilts odd tiles -3deg and even ones +3deg (style.css).
var col = item.parentElement, i = 0;
while (col && (col = col.previousElementSibling)) i++;
return (i % 2 === 1) ? 3 : -3;
}
// Desktop placement: photo column on top of the tile photo.
function place(item) {
var card = item.querySelector('.trainer-detail');
if (!card) return;
card.style.setProperty('--td-shift', '0px');
if (floating.matches) return;
var r = card.getBoundingClientRect();
var itemR = item.getBoundingClientRect();
// Where the card would sit centred on the item (ignoring its current transform).
var left = itemR.left + itemR.width / 2 - r.width / 2;
var right = left + r.width;
var gutter = 16, vw = document.documentElement.clientWidth, shift = 0;
if (left < gutter) shift = gutter - left;
else if (right > vw - gutter) shift = (vw - gutter) - right;
card.style.setProperty('--td-shift', Math.round(shift) + 'px');
var pic = item.querySelector('.trainer-pic');
if (!card || !pic || floating.matches) return;
var vw = document.documentElement.clientWidth;
var w = Math.min(CARD_W, vw - GUTTER * 2);
var pw = pic.offsetWidth, ph = pic.offsetHeight;
var picLeft = item.getBoundingClientRect().left + pic.offsetLeft; // untransformed
var flip = picLeft + w > vw - GUTTER; // no room on the right
var x = flip ? pic.offsetLeft + pw - w : pic.offsetLeft;
card.classList.toggle('td-flip', flip);
card.style.setProperty('--td-w', w + 'px');
card.style.setProperty('--td-pw', pw + 'px');
card.style.setProperty('--td-ph', ph + 'px');
card.style.setProperty('--td-x', x + 'px');
card.style.setProperty('--td-y', pic.offsetTop + 'px');
card.style.setProperty('--td-rot', tileRot(item) + 'deg');
// Closed clip = just the photo square, in px (see style.css).
card.style.setProperty('--td-cx', Math.max(0, w - pw) + 'px');
card.style.setProperty('--td-cy', Math.max(0, card.offsetHeight - ph) + 'px');
}
var section = document.getElementById('equipa');
function resetTilt(card) {
['--td-rx', '--td-ry', '--td-mx', '--td-my'].forEach(function (p) { card.style.removeProperty(p); });
card.classList.remove('td-tracking');
}
// Touch: fly the fixed card out of / back into the tile photo.
function flip(item, opening) {
var card = item.querySelector('.trainer-detail');
var pic = item.querySelector('.trainer-pic');
if (!card || !pic || !card.animate || reduced.matches) return null;
var p = pic.getBoundingClientRect(), c = card.getBoundingClientRect();
if (!c.width || !c.height) return null;
var from = 'translate(' + (p.left - c.left) + 'px,' + (p.top - c.top) + 'px) scale(' +
(p.width / c.width) + ',' + (p.height / c.height) + ') rotate(' + tileRot(item) + 'deg)';
var frames = [{ transform: from, opacity: .4 }, { transform: 'none', opacity: 1 }];
return card.animate(opening ? frames : frames.reverse(), {
duration: opening ? 420 : 280,
easing: opening ? 'cubic-bezier(.2,.9,.25,1)' : 'cubic-bezier(.4,0,1,1)'
});
}
function setOpen(item, open) {
item.classList.toggle('is-open', open);
var card = item.querySelector('.trainer-detail');
var wasOpen = item.classList.contains('is-open');
if (open === wasOpen) return;
clearTimeout(item._tdTrack);
if (open) {
if (floating.matches) {
item.classList.add('is-open');
card.style.setProperty('--td-top', Math.max(24, (window.innerHeight - card.offsetHeight) / 2) + 'px');
flip(item, true);
} else {
// Jump (no transition) to the closed pose on top of the tile, commit
// it, then open — otherwise the closed state's own transition would
// start moving from the previous pose and the opening would begin
// from that half-way value instead of from the photo.
card.style.transition = 'none';
place(item);
void card.offsetWidth;
card.style.transition = '';
void card.offsetWidth;
item.classList.add('is-open');
item._tdTrack = setTimeout(function () { card.classList.add('td-tracking'); }, 600);
}
} else {
resetTilt(card);
var anim = floating.matches ? flip(item, false) : null;
if (anim) anim.onfinish = function () { item.classList.remove('is-open'); syncSection(); };
else item.classList.remove('is-open');
}
item.setAttribute('aria-expanded', open ? 'true' : 'false');
syncSection();
}
function syncSection() {
if (section) section.classList.toggle('has-open-detail', !!section.querySelector('.trainer-item.is-open'));
}
function closeAll(except) {
@@ -261,35 +330,65 @@ document.addEventListener('DOMContentLoaded', function () {
}
items.forEach(function (item) {
item.addEventListener('mouseenter', function () { place(item); });
item.addEventListener('focus', function () { place(item); });
var card = item.querySelector('.trainer-detail');
// Desktop hover: short intent delay so sweeping across the grid
// doesn't fire every card.
item.addEventListener('mouseenter', function () {
if (floating.matches) return;
clearTimeout(item._tdHover);
item._tdHover = setTimeout(function () { closeAll(item); setOpen(item, true); }, 90);
});
item.addEventListener('mouseleave', function () {
if (floating.matches) return;
clearTimeout(item._tdHover);
setOpen(item, false);
});
// Tilt + drift toward the pointer while open (desktop only).
item.addEventListener('mousemove', function (e) {
if (floating.matches || reduced.matches || !item.classList.contains('is-open')) return;
var r = card.getBoundingClientRect();
var dx = Math.max(-1, Math.min(1, (e.clientX - (r.left + r.width / 2)) / (r.width / 2)));
var dy = Math.max(-1, Math.min(1, (e.clientY - (r.top + r.height / 2)) / (r.height / 2)));
card.style.setProperty('--td-ry', (dx * 5).toFixed(2) + 'deg');
card.style.setProperty('--td-rx', (-dy * 5).toFixed(2) + 'deg');
card.style.setProperty('--td-mx', (dx * 6).toFixed(1) + 'px');
card.style.setProperty('--td-my', (dy * 6).toFixed(1) + 'px');
});
item.addEventListener('focus', function () {
if (!floating.matches) { closeAll(item); setOpen(item, true); }
});
item.addEventListener('blur', function (e) {
if (!floating.matches && !item.contains(e.relatedTarget)) setOpen(item, false);
});
// Tap / click (touch, or a desktop click on a closed tile).
item.addEventListener('click', function (e) {
e.stopPropagation();
if (e.target.closest('.td-close')) { setOpen(item, false); return; }
// Clicks inside an open card (e.g. scrolling / selecting text) keep it open.
if (e.target.closest('.trainer-detail') && item.classList.contains('is-open')) return;
// Tapping the dimmed backdrop (the item's ::after) closes it.
if (floating.matches && item.classList.contains('is-open') && !e.target.closest('.trainer-detail')) {
setOpen(item, false); return;
var inCard = !!e.target.closest('.trainer-detail');
if (item.classList.contains('is-open')) {
// Tapping the dimmed backdrop (the item's ::after) closes it;
// taps inside the card (scroll, select) keep it open.
if (floating.matches && !inCard) setOpen(item, false);
return;
}
closeAll(item);
place(item);
setOpen(item, !item.classList.contains('is-open'));
setOpen(item, true);
});
item.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
if (e.target !== item) return;
if (e.key === 'Escape') { setOpen(item, false); item.blur(); }
else if ((e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') && e.target === item) {
e.preventDefault();
closeAll(item); place(item);
setOpen(item, !item.classList.contains('is-open'));
} else if (e.key === 'Escape') {
setOpen(item, false);
item.blur();
}
});
});
document.addEventListener('click', function () { closeAll(null); });
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeAll(null); });
window.addEventListener('resize', function () { closeAll(null); }, { passive: true });
});
/* ── Mensalidades: pre-select the clicked card in the sign-up modal ───────── */