diff --git a/site/assets/css/style.css b/site/assets/css/style.css index 76fd956..4c3a9e9 100644 --- a/site/assets/css/style.css +++ b/site/assets/css/style.css @@ -2962,7 +2962,7 @@ section[id] { /* The name used to be the tile's last child and got its size from the `.trainer-item>*:last-child` rule; pin it so it stays the same whether or - not a title / "Saber mais" follows it. */ + not a title follows it. */ .trainers-section .trainer-item h4 { font-size: .9rem; } @@ -2987,80 +2987,105 @@ section[id] { outline: none; } -.trainer-more { - display: inline-flex; - align-items: center; - gap: 6px; - margin-top: .55rem; - font-size: .72rem; - font-weight: 700; - letter-spacing: .12em; - text-transform: uppercase; - color: var(--fitme-ash); - transition: color var(--fitme-transition); +/* On tiles with a card, the card IS the hover effect: keep the photo in its + resting tilt (the card starts from exactly that pose) and hide it while the + card covers it, bringing it back only once the card has folded back in. */ +#powerRotation>div:nth-child(odd) .trainer-item.has-detail .trainer-pic:hover { transform: rotate(-3deg); } +#powerRotation>div:nth-child(even) .trainer-item.has-detail .trainer-pic:hover { transform: rotate(3deg); } + +.trainer-item.has-detail .trainer-pic { + transition: transform .4s cubic-bezier(.34, 1.56, .64, 1), box-shadow var(--fitme-transition), visibility 0s linear .38s; } -.trainer-more i { font-size: .65rem; } - -.trainer-item.has-detail:hover .trainer-more, -.trainer-item.has-detail:focus-visible .trainer-more, -.trainer-item.has-detail.is-open .trainer-more { - color: var(--fitme-accent); +.trainer-item.has-detail.is-open .trainer-pic { + visibility: hidden; + transition-delay: 0s; } +@media (max-width: 767px), (hover: none) { + /* Touch: the card flies to the centre, so leave the tile photo in place. */ + .trainer-item.has-detail.is-open .trainer-pic { visibility: visible; } +} + +/* ── Detail card: grows out of the tile's own photo ───────────────────────── + main.js places the card so its photo column sits exactly on the tile photo + (--td-x/--td-y, size --td-pw/--td-ph) with the tile's tilt (--td-rot). Closed, + the clip-path shows only that photo square, so it reads as the tile itself; + opening straightens it and unclips it sideways — the card "stretches" out of + the photo and the text fades in behind the edge. .td-flip mirrors it for + right-hand tiles (photo on the right, card stretching left). While open, the + mouse position feeds a small 3D tilt + drift (--td-rx/--td-ry/--td-mx/--td-my). + Both transform lists have the same functions so they interpolate. */ .trainer-detail { position: absolute; z-index: 30; - top: -10px; - left: 50%; - width: min(560px, calc(100vw - 32px)); - transform: translateX(calc(-50% + var(--td-shift, 0px))) translateY(8px); + left: var(--td-x, 0px); + top: var(--td-y, 0px); + width: var(--td-w, 560px); + min-height: var(--td-ph, 200px); display: flex; text-align: left; background: #fff; color: var(--fitme-text); border-radius: 6px; - border-left: 4px solid var(--fitme-accent); box-shadow: 0 24px 60px rgba(0, 0, 0, .45); overflow: hidden; + /* px from main.js: Chrome won't interpolate inset() with calc(100% - …). */ + clip-path: inset(0px var(--td-cx, 360px) var(--td-cy, 0px) 0px round 2px); + transform-origin: calc(var(--td-pw, 200px) / 2) calc(var(--td-ph, 200px) / 2); + transform: perspective(900px) rotateX(0deg) rotateY(0deg) translate(0px, 0px) rotate(var(--td-rot, 0deg)); opacity: 0; visibility: hidden; pointer-events: none; - transition: opacity .2s ease, transform .2s ease, visibility 0s linear .2s; + transition: + clip-path .38s cubic-bezier(.4, 0, .2, 1), + transform .38s cubic-bezier(.4, 0, .2, 1), + opacity .12s ease .26s, + visibility 0s linear .38s; +} + +.trainer-detail.td-flip { + flex-direction: row-reverse; + clip-path: inset(0px 0px var(--td-cy, 0px) var(--td-cx, 360px) round 2px); + transform-origin: calc(100% - var(--td-pw, 200px) / 2) calc(var(--td-ph, 200px) / 2); } .trainer-item.is-open .trainer-detail { + clip-path: inset(0px 0px 0px 0px round 6px); + transform: perspective(900px) rotateX(var(--td-rx, 0deg)) rotateY(var(--td-ry, 0deg)) translate(var(--td-mx, 0px), var(--td-my, 0px)) rotate(0deg); opacity: 1; visibility: visible; pointer-events: auto; - transform: translateX(calc(-50% + var(--td-shift, 0px))) translateY(0); - transition: opacity .2s ease, transform .2s ease, visibility 0s; + transition: + clip-path .55s cubic-bezier(.2, .9, .25, 1), + transform .6s cubic-bezier(.34, 1.35, .64, 1), + opacity 0s, + visibility 0s; } -@media (hover: hover) and (pointer: fine) { - .trainer-item.has-detail:hover .trainer-detail, - .trainer-item.has-detail:focus-within .trainer-detail { - opacity: 1; - visibility: visible; - pointer-events: auto; - transform: translateX(calc(-50% + var(--td-shift, 0px))) translateY(0); - transition: opacity .2s ease .12s, transform .2s ease .12s, visibility 0s .12s; - } - .trainer-item.has-detail:hover { z-index: 31; } +/* After the opening move, follow the mouse with a short, soft transition. */ +.trainer-item.is-open .trainer-detail.td-tracking { + transition: + clip-path .55s cubic-bezier(.2, .9, .25, 1), + transform .18s ease-out, + opacity 0s, + visibility 0s; } .trainer-item.is-open { z-index: 31; } .td-photo { - flex: 0 0 38%; - max-width: 210px; - background: var(--fitme-dark-2); + flex: 0 0 var(--td-pw, 200px); + box-sizing: border-box; + border: 4px solid var(--fitme-accent); + background: var(--fitme-surface-2); + align-self: stretch; } .td-photo img { width: 100%; height: 100%; - min-height: 220px; + min-height: calc(var(--td-ph, 200px) - 8px); object-fit: cover; object-position: top; display: block; @@ -3071,6 +3096,17 @@ section[id] { padding: 18px 22px 18px 20px; max-height: 340px; overflow-y: auto; + opacity: 0; + transform: translateX(-10px); + transition: opacity .12s ease, transform .12s ease; +} + +.td-flip .td-body { transform: translateX(10px); } + +.trainer-item.is-open .td-body { + opacity: 1; + transform: none; + transition: opacity .3s ease .18s, transform .35s ease .18s; } .td-name { @@ -3148,20 +3184,32 @@ section[id] { display: none; } -/* Touch / small screens: a centred fixed card over a dimmed page. */ +/* Touch / small screens: a centred fixed card over a dimmed page. The "grows + out of the tile" move is done in main.js with a FLIP animation instead of + the clip-path, since the card no longer sits on top of the tile. */ @media (max-width: 767px), (hover: none) { - .trainer-detail { + .trainer-detail, + .trainer-detail.td-flip { position: fixed; - top: 50%; + top: var(--td-top, 20vh); left: 16px; right: 16px; width: auto; max-width: 560px; + min-height: 0; margin: 0 auto; - transform: translateY(-46%); max-height: calc(100vh - 48px); + flex-direction: row; + clip-path: none; + transform: none; + transform-origin: top left; + transition: opacity .15s ease, visibility 0s linear .15s; + } + .trainer-item.is-open .trainer-detail { + clip-path: none; + transform: none; + transition: opacity 0s, visibility 0s; } - .trainer-item.is-open .trainer-detail { transform: translateY(-50%); } .trainer-item.is-open:after { content: ''; position: fixed; @@ -3170,17 +3218,24 @@ section[id] { background: rgba(0, 0, 0, .55); } .td-close { display: block; } - .td-photo { flex-basis: 34%; } + .td-photo { flex-basis: 34%; border-width: 3px; } .td-photo img { min-height: 180px; } - .td-body { max-height: calc(100vh - 48px); padding: 16px 16px 16px 14px; } + .td-body, + .td-flip .td-body { max-height: calc(100vh - 48px); padding: 16px 16px 16px 14px; transform: none; } } @media (max-width: 339px) { - .trainer-detail { flex-direction: column; } + .trainer-detail, + .trainer-detail.td-flip { flex-direction: column; } .td-photo { max-width: none; flex-basis: auto; } .td-photo img { min-height: 0; height: 160px; } } +@media (prefers-reduced-motion: reduce) { + .trainer-detail, + .trainer-item.is-open .trainer-detail { transition-duration: .01ms !important; } +} + /* ══════════════════════════════════════════════════════════════════════════ Mensalidades — cartões de preço ══════════════════════════════════════════════════════════════════════════ */ diff --git a/site/assets/js/main.js b/site/assets/js/main.js index e92b8f6..e7a3e6a 100644 --- a/site/assets/js/main.js +++ b/site/assets/js/main.js @@ -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 ───────── */ diff --git a/site/index.php b/site/index.php index b1005dd..b76fa5c 100644 --- a/site/index.php +++ b/site/index.php @@ -140,7 +140,6 @@ if ($team):

-