diff --git a/site/assets/css/style.css b/site/assets/css/style.css index 2c88619..244ae28 100644 --- a/site/assets/css/style.css +++ b/site/assets/css/style.css @@ -2248,20 +2248,17 @@ h3.contact-title { opacity: 1; } - *, + /* The team / modalidade detail cards are exempt: reduced-motion is on by + default over Remote Desktop and with Windows "animation effects" off, and + this nuke turned their grow-out-of-the-card move into a jarring pop. They + only move when the visitor asks (hover / tap) and settle in ~0.6s. */ + *:not(.trainer-detail):not(.td-body):not(.mod-detail):not(.md-body), *:before, *:after { animation-duration: .01ms !important; transition-duration: .01ms !important; } - /* Exempt the modalidades marquee: the .01ms nuke turns its infinite loop - into a strobing blur (reduced-motion is on by default over Remote Desktop - and with Windows "animation effects" off). Keep its gentle glide instead; - JS re-asserts the measured duration inline with !important. */ - .mod-track { - animation-duration: 220s !important; - } } /* ── Decorated section title (eyebrow + accent underline + subtitle) ────────── */ @@ -2307,136 +2304,305 @@ h3.contact-title { font-size: 1rem; } -/* ── Modalidades: tap card to reveal its description ───────────────────────── */ -/* No separate icon — the whole card (icon + name) is the trigger. The - description reveals as a clean panel laid over the card footprint, so the - grid never reflows. Click-only (no hover), per request. */ -.mod-card { +/* ── Modalidades: scrollable strip + detail card ───────────────────────────── + .mod-scroll is a real horizontal scroller (touch, trackpad, wheel, mouse + drag, arrow buttons). main.js glides it on its own until someone interacts, + and loops it seamlessly over a cloned second set of cards. */ +/* content-visibility:auto implies paint containment, which would clip the + detail card at the section edge and trap position:fixed on touch. */ +#modalidades.service-section { + content-visibility: visible; + contain: none; + position: relative; + z-index: 2; +} + +#modalidades.has-open-detail { z-index: 1100; } + +.mod-strip { position: relative; } -.mod-card.has-info { - cursor: pointer; +/* Soft fades at both ends, under the arrow buttons. */ +.mod-strip:before, +.mod-strip:after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + z-index: 2; + width: 90px; + pointer-events: none; +} + +.mod-strip:before { left: 0; background: linear-gradient(90deg, var(--fitme-white), rgba(255, 255, 255, 0)); } +.mod-strip:after { right: 0; background: linear-gradient(270deg, var(--fitme-white), rgba(255, 255, 255, 0)); } + +@media (max-width: 767px), (hover: none) { + .mod-strip:before, .mod-strip:after { width: 28px; } +} + +.mod-scroll { + display: flex; + overflow-x: auto; + overflow-y: hidden; + padding: 10px 0 14px; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; + overscroll-behavior-x: contain; + cursor: grab; outline: none; } -.mod-card.has-info .ib-icon, -.mod-card.has-info h4 { - transition: opacity var(--fitme-transition); +.mod-scroll::-webkit-scrollbar { display: none; } +.mod-scroll.is-dragging { cursor: grabbing; user-select: none; } +.mod-scroll.is-dragging .mod-card { pointer-events: none; } + +/* Trailing margin (not flex gap) keeps both copies the same width, so the + loop jump from the clone back to the original is invisible. */ +.mod-scroll .mod-card { + flex: 0 0 auto; + width: 180px; + min-height: 200px; + margin: 0 1rem 0 0; + position: relative; } -.mod-card.has-info:hover h4 { - color: var(--fitme-accent-dark); -} +.mod-scroll .mod-card:first-child { margin-left: 1rem; } + +.mod-card.has-info { cursor: pointer; outline: none; } + +.mod-card.has-info:hover h3 { color: var(--fitme-accent-dark); } .mod-card.has-info:focus-visible { box-shadow: 0 0 0 3px rgba(var(--fitme-accent-rgb), .35); border-radius: var(--fitme-radius); } -/* Subtle "tap for more" affordance under the name */ -.mod-card.has-info h4:after { - content: 'Saber mais'; - display: block; - margin-top: .35rem; - font-size: .68rem; - font-weight: 700; - letter-spacing: .12em; - text-transform: uppercase; - color: var(--fitme-accent-dark); - opacity: .85; -} +/* The card whose detail is open is covered by the panel — hide it so the + panel reads as the card itself. */ +.mod-card.is-source { visibility: hidden; } -.mod-tooltip { +/* Arrow buttons (desktop). Faded edges hint there is more to either side. */ +.mod-nav { position: absolute; - inset: -10px -6px auto -6px; - z-index: 6; - min-height: calc(100% + 20px); + top: 50%; + z-index: 3; + width: 44px; + height: 44px; + margin-top: -30px; + border: 0; + border-radius: 50%; + background: var(--fitme-dark-2); + color: var(--fitme-accent); + box-shadow: var(--fitme-shadow-lg); + cursor: pointer; display: flex; align-items: center; justify-content: center; - background: #fff; - color: var(--fitme-text-soft); - border: 1px solid var(--fitme-surface-2); - border-top: 3px solid var(--fitme-accent); - border-radius: var(--fitme-radius); - box-shadow: var(--fitme-shadow-lg); - padding: 1rem 1.1rem; - font-size: .85rem; - line-height: 1.5; - text-align: center; - opacity: 0; - visibility: hidden; - transform: scale(.96); + opacity: .9; transition: opacity var(--fitme-transition), transform var(--fitme-transition); } -.mod-tooltip span { - display: block; +.mod-nav:hover { opacity: 1; transform: scale(1.06); } +.mod-nav-prev { left: 14px; } +.mod-nav-next { right: 14px; } + +@media (max-width: 767px), (hover: none) { + .mod-nav { display: none; } + .mod-scroll { cursor: auto; } } -.mod-card.is-open .mod-tooltip { +/* ── Detail panel: grows out of the card ─────────────────────────────────── + Same idea as the team card: main.js lays the panel so its left column + (.md-card, a replica of the card) sits exactly on the card, clipped to it; + opening unclips it sideways to reveal the description (.md-flip mirrors it + near the right edge). While open, the mouse drives a small 3D tilt. */ +.mod-detail { + position: absolute; + z-index: 30; + left: var(--md-x, 0px); + top: var(--md-y, 0px); + width: var(--md-w, 540px); + min-height: var(--md-ch, 200px); + display: flex; + text-align: left; + background: #fff; + border-radius: 6px; + box-shadow: 0 24px 60px rgba(var(--fitme-dark-rgb), .28); + overflow: hidden; + clip-path: inset(0px var(--md-cx, 360px) var(--md-cy, 0px) 0px round 2px); + transform-origin: calc(var(--md-cw, 180px) / 2) 50%; + transform: perspective(900px) rotateX(0deg) rotateY(0deg) translate(0px, 0px) scale(1); + opacity: 0; + visibility: hidden; + pointer-events: none; + 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; +} + +.mod-detail.md-flip { + flex-direction: row-reverse; + clip-path: inset(0px 0px var(--md-cy, 0px) var(--md-cx, 360px) round 2px); + transform-origin: calc(100% - var(--md-cw, 180px) / 2) 50%; +} + +.mod-detail.is-open { + clip-path: inset(0px 0px 0px 0px round 6px); + transform: perspective(900px) rotateX(var(--md-rx, 0deg)) rotateY(var(--md-ry, 0deg)) translate(var(--md-mx, 0px), var(--md-my, -6px)) scale(1.02); opacity: 1; visibility: visible; - transform: scale(1); + pointer-events: auto; + transition: + clip-path .55s cubic-bezier(.2, .9, .25, 1), + transform .6s cubic-bezier(.34, 1.35, .64, 1), + opacity 0s, + visibility 0s; } -.mod-card.is-open .ib-icon, -.mod-card.is-open h4 { - opacity: 0; +.mod-detail.is-open.md-tracking { + transition: + clip-path .55s cubic-bezier(.2, .9, .25, 1), + transform .18s ease-out, + opacity 0s, + visibility 0s; } -/* Full-width auto-scrolling modalidade marquee */ -.mod-scroll { - overflow: hidden; - padding: 8px 0 10px; -} - -.mod-track { +/* Left column: looks exactly like the card it came from. */ +.md-card { + flex: 0 0 var(--md-cw, 180px); + min-height: var(--md-ch, 200px); + box-sizing: border-box; display: flex; - width: max-content; - animation: modMarquee 220s linear infinite; - /* slow fallback; JS tunes it to ~18px/s */ - will-change: transform; + flex-direction: column; + align-items: center; + justify-content: flex-start; + padding-top: var(--md-pad, 0px); + text-align: center; + background: #fff; + border-right: 4px solid var(--md-color, var(--fitme-accent)); } -/* Pause while hovering, focusing inside, or reading an open card */ -.mod-scroll:hover .mod-track, -.mod-scroll:focus-within .mod-track, -.mod-scroll.is-paused .mod-track { - animation-play-state: paused; +.md-flip .md-card { border-right: 0; border-left: 4px solid var(--md-color, var(--fitme-accent)); } + +.md-card .ib-icon { + width: 72px; + height: 72px; + border-radius: var(--fitme-radius); + display: flex; + align-items: center; + justify-content: center; + background: var(--fitme-accent); + margin: 0 auto 1.5rem; } -/* Trailing margin (not flex gap) keeps every copy the same width, so a - translateX(-50%) of the duplicated track loops perfectly seamlessly. */ -.mod-scroll .mod-card { - flex: 0 0 auto; - width: 180px; - min-height: 200px; - margin: 0 1rem 0 0; +.md-card .ib-icon img { max-width: 44px; max-height: 44px; } + +.md-card h3 { + font-size: 1.15rem; + margin: 0 .5rem; + color: var(--fitme-accent-dark); } -@keyframes modMarquee { - from { - transform: translateX(0); - } - - to { - transform: translateX(-50%); - } -} - -/* Keep the reveal panel inside the card footprint so it isn't clipped by the - scroller's overflow (which can't be visible on one axis only). Top-align + - scroll so long descriptions don't get their first lines hidden by vertical - centering (you can't scroll back above a centered overflowing flex item). */ -.mod-scroll .mod-tooltip { - inset: 0; - min-height: 0; +.md-body { + flex: 1 1 auto; + padding: 20px 24px 20px 22px; + max-height: 320px; overflow-y: auto; - align-items: flex-start; + opacity: 0; + transform: translateX(-10px); + transition: opacity .12s ease, transform .12s ease; } +.md-flip .md-body { transform: translateX(10px); } + +.mod-detail.is-open .md-body { + opacity: 1; + transform: none; + transition: opacity .3s ease .18s, transform .35s ease .18s; +} + +.md-name { + display: block; + font-size: 1.2rem; + font-weight: 900; + text-transform: uppercase; + color: var(--fitme-dark); + margin-bottom: .5rem; + padding-right: 18px; +} + +.md-text { + font-size: .9rem; + line-height: 1.55; + color: var(--fitme-text-soft); +} + +.md-text p:last-child { margin-bottom: 0; } + +.md-close { + position: absolute; + top: 6px; + right: 10px; + z-index: 2; + background: transparent; + border: 0; + font-size: 1.6rem; + line-height: 1; + color: var(--fitme-text-muted); + cursor: pointer; + display: none; +} + +/* Touch / narrow: a centred fixed card over a dimmed page; main.js flies it + out of the card with a FLIP animation. */ +@media (max-width: 767px), (hover: none) { + .mod-detail, + .mod-detail.md-flip { + position: fixed; + top: var(--md-top, 20vh); + left: 16px; + right: 16px; + width: auto; + max-width: 540px; + min-height: 0; + margin: 0 auto; + 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; + } + .mod-detail.is-open { + clip-path: none; + transform: none; + transition: opacity 0s, visibility 0s; + } + .mod-detail-backdrop { + position: fixed; + inset: 0; + z-index: 29; + background: rgba(0, 0, 0, .55); + } + .md-close { display: block; } + .md-card { flex-basis: 120px; min-height: 0; padding: 18px 6px; justify-content: center; } + .md-flip .md-card { border-left: 0; border-right: 4px solid var(--md-color, var(--fitme-accent)); } + .md-card .ib-icon { width: 60px; height: 60px; margin-bottom: .8rem; } + .md-card h3 { font-size: .95rem; } + .md-body, + .md-flip .md-body { max-height: calc(100vh - 48px); padding: 16px 16px 16px 14px; transform: none; } +} + +@media (max-width: 339px) { + .mod-detail, .mod-detail.md-flip { flex-direction: column; } + .md-card { flex-basis: auto; border-right: 0; border-bottom: 4px solid var(--md-color, var(--fitme-accent)); } +} + + /* ── Horário "save as JPG" button + capture-clone export board ─────────────── */ .schedule-dl-btn { display: inline-flex; @@ -3110,7 +3276,8 @@ h3.contact-title { position:fixed descendants — which would pin the mobile detail card inside its own tile. Reveal the team with opacity only. */ #equipa.revealed, -#equipa .trainer-item.revealed { +#equipa .trainer-item.revealed, +#modalidades.revealed { animation-name: fitmeFadeOnly; } @@ -3395,10 +3562,6 @@ h3.contact-title { .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 c6c8b33..44f120d 100644 --- a/site/assets/js/main.js +++ b/site/assets/js/main.js @@ -80,75 +80,295 @@ document.addEventListener('DOMContentLoaded', function () { } }); -/* ── Modalidades: clickable cards + CSS-animated marquee ──────────────────── - Cards reveal their description on click (one open at a time; Esc / outside - click closes; Enter/Space for keyboard users). For the auto-scroll we move - the cards into an inner .mod-track, duplicate the set once, and let a CSS - transform animation glide it from 0 to -50% forever — a GPU-driven loop that - actually moves (the old scrollLeft approach was defeated by sub-pixel - rounding). It pauses on hover / focus and while a card is open. */ +/* ── Modalidades: scrollable strip + detail card ──────────────────────────── + .mod-scroll is a native horizontal scroller, so touch, trackpad and + shift+wheel just work; on top of that: mouse drag, arrow buttons, plain + wheel → sideways, and a slow auto-glide (~20px/s) that pauses while anyone + interacts and picks up again a moment later. The card set is cloned once so + the glide loops: past the first copy we jump back by exactly its width. + (The position is kept in a float — assigning scrollLeft += 0.3 each frame + rounds to nothing — and written back every frame.) + Cards with a description open .mod-detail, one shared panel outside the + scroller (so overflow doesn't clip it) that grows out of the card like the + team cards do, with the same 3D tilt toward the mouse; on touch it flies to + the centre of the screen instead. */ document.addEventListener('DOMContentLoaded', function () { - var scroller = document.querySelector('.mod-scroll'); + var section = document.getElementById('modalidades'); + var scroller = section && section.querySelector('.mod-scroll'); + if (!scroller) return; + var panel = section.querySelector('.mod-detail'); + var floating = window.matchMedia('(max-width: 767px), (hover: none)'); + var reduced = window.matchMedia('(prefers-reduced-motion: reduce)'); - // Build the animated track: move originals in, then append a hidden clone set. - if (scroller && !scroller.querySelector('.mod-track')) { - var track = document.createElement('div'); - track.className = 'mod-track'; - Array.prototype.slice.call(scroller.querySelectorAll('.mod-card')) - .forEach(function (c) { track.appendChild(c); }); - Array.prototype.slice.call(track.children).forEach(function (c) { - var clone = c.cloneNode(true); - clone.setAttribute('aria-hidden', 'true'); - clone.setAttribute('tabindex', '-1'); - clone.removeAttribute('id'); - track.appendChild(clone); - }); - scroller.appendChild(track); - // Gentle, constant ~18px/s regardless of how many modalidades there are. - // Measure after a frame so layout (card widths) is settled. Set with - // !important so the reduced-motion .01ms!important override (active - // over Remote Desktop / Windows "animation effects" off) can't shrink - // the duration and turn the loop into a strobing blur. - requestAnimationFrame(function () { - var oneCopy = track.scrollWidth / 2; - var dur = oneCopy > 0 ? Math.max(60, Math.round(oneCopy / 18)) : 220; - track.style.setProperty('animation-duration', dur + 's', 'important'); - }); + // ── Loop: clone the set once ────────────────────────────────────────── + var originals = Array.prototype.slice.call(scroller.querySelectorAll('.mod-card')); + originals.forEach(function (c) { + var clone = c.cloneNode(true); + clone.setAttribute('aria-hidden', 'true'); + clone.setAttribute('tabindex', '-1'); + clone.classList.add('is-clone'); + scroller.appendChild(clone); + }); + function loopWidth() { + // Distance from the first original to the first clone = one full set. + var first = scroller.querySelector('.mod-card'); + var firstClone = scroller.querySelector('.mod-card.is-clone'); + return first && firstClone ? firstClone.offsetLeft - first.offsetLeft : 0; + } + function wrap() { + var w = loopWidth(); + if (!w) return; + if (scroller.scrollLeft >= w) { scroller.scrollLeft -= w; pos = scroller.scrollLeft; } + else if (scroller.scrollLeft <= 0 && (performance.now() - lastUser) < IDLE) { scroller.scrollLeft += w; pos = scroller.scrollLeft; } } - var cards = document.querySelectorAll('.mod-card.has-info'); - if (!cards.length) return; - - function anyOpen() { return !!document.querySelector('.mod-card.is-open'); } - function syncPause() { if (scroller) scroller.classList.toggle('is-paused', anyOpen()); } - function closeAll(except) { - cards.forEach(function (card) { - if (card === except) return; - card.classList.remove('is-open'); - card.setAttribute('aria-expanded', 'false'); - }); + // ── Auto-glide ──────────────────────────────────────────────────────── + var SPEED = 20; // px per second + var IDLE = 2500; // ms after the last interaction before gliding again + var pos = 0, last = 0, lastUser = -1e9, hovering = false; // -1e9: glide from the start + function busy() { + return hovering || dragging || panelOpen() || document.hidden || + (performance.now() - lastUser) < IDLE; } + function tick(t) { + var dt = last ? Math.min(64, t - last) : 0; + last = t; + if (!busy()) { + pos += SPEED * dt / 1000; + scroller.scrollLeft = pos; + wrap(); + } else { + pos = scroller.scrollLeft; + } + requestAnimationFrame(tick); + } + requestAnimationFrame(tick); + function touched() { lastUser = performance.now(); } - cards.forEach(function (card) { - card.addEventListener('click', function (e) { + scroller.addEventListener('mouseenter', function () { hovering = true; }); + scroller.addEventListener('mouseleave', function () { hovering = false; touched(); }); + scroller.addEventListener('touchstart', touched, { passive: true }); + scroller.addEventListener('touchmove', touched, { passive: true }); + scroller.addEventListener('focusin', touched); + scroller.addEventListener('scroll', function () { + if (dragging || (performance.now() - lastUser) < IDLE) { touched(); wrap(); } + if (panelOpen() && !opening) closePanel(); + }, { passive: true }); + + // Plain vertical wheel over the strip scrolls it sideways — but only while + // there is room, so the page keeps scrolling past it at the ends… which a + // looping strip never has, so: only when the pointer is actually on it. + scroller.addEventListener('wheel', function (e) { + if (floating.matches || Math.abs(e.deltaX) > Math.abs(e.deltaY) || e.shiftKey) { touched(); return; } + e.preventDefault(); + touched(); + scroller.scrollLeft += e.deltaY; + wrap(); + }, { passive: false }); + + // ── Mouse drag ──────────────────────────────────────────────────────── + var dragging = false, dragStartX = 0, dragStartLeft = 0, dragMoved = 0, suppressClick = false; + scroller.addEventListener('pointerdown', function (e) { + if (e.pointerType !== 'mouse' || e.button !== 0) return; + dragging = true; dragMoved = 0; + dragStartX = e.clientX; dragStartLeft = scroller.scrollLeft; + touched(); + }); + window.addEventListener('pointermove', function (e) { + if (!dragging) return; + var dx = e.clientX - dragStartX; + dragMoved = Math.max(dragMoved, Math.abs(dx)); + if (dragMoved > 5) { + scroller.classList.add('is-dragging'); + scroller.scrollLeft = dragStartLeft - dx; + wrap(); + touched(); + } + }); + window.addEventListener('pointerup', function () { + if (!dragging) return; + dragging = false; + suppressClick = dragMoved > 5; + scroller.classList.remove('is-dragging'); + touched(); + }); + // A drag must not also count as a click on the card under the pointer. + scroller.addEventListener('click', function (e) { + if (suppressClick) { e.stopPropagation(); e.preventDefault(); suppressClick = false; } + }, true); + + // ── Arrow buttons ───────────────────────────────────────────────────── + function step(dir) { + var card = scroller.querySelector('.mod-card'); + var by = card ? (card.offsetWidth + 16) * Math.max(1, Math.floor(scroller.clientWidth / (card.offsetWidth + 16)) - 1) : 400; + touched(); + if (dir < 0 && scroller.scrollLeft - by < 0) { scroller.scrollLeft += loopWidth(); } + scroller.scrollBy({ left: dir * by, behavior: reduced.matches ? 'auto' : 'smooth' }); + } + section.querySelectorAll('.mod-nav').forEach(function (b) { + b.addEventListener('click', function (e) { e.stopPropagation(); - var open = card.classList.toggle('is-open'); - card.setAttribute('aria-expanded', open ? 'true' : 'false'); - closeAll(card); - syncPause(); - }); - card.addEventListener('keydown', function (e) { - if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') { - e.preventDefault(); - card.click(); - } else if (e.key === 'Escape') { - card.classList.remove('is-open'); - card.setAttribute('aria-expanded', 'false'); - syncPause(); - } + closePanel(); + step(b.classList.contains('mod-nav-prev') ? -1 : 1); }); }); - document.addEventListener('click', function () { closeAll(null); syncPause(); }); + + // ── Detail panel ────────────────────────────────────────────────────── + if (!panel) return; + var source = null, opening = false, trackTimer = 0, backdrop = null; + var W = 540, GUTTER = 16; + function panelOpen() { return panel.classList.contains('is-open'); } + + function fill(card) { + var img = card.querySelector('.ib-icon img'); + var name = card.querySelector('h3').textContent; + var tpl = card.querySelector('template.mod-desc'); + var pImg = panel.querySelector('.md-card img'); + pImg.hidden = !img; + pImg.src = img ? img.src : ''; + panel.querySelector('.md-card h3').textContent = name; + panel.querySelector('.md-name').textContent = name; + panel.querySelector('.md-text').innerHTML = tpl ? tpl.innerHTML : ''; + panel.style.setProperty('--md-color', card.getAttribute('data-color') || ''); + panel.setAttribute('aria-label', name); + } + + // Desktop: lay the panel's left column exactly over the card. + function place(card) { + var sr = section.getBoundingClientRect(), cr = card.getBoundingClientRect(); + var vw = document.documentElement.clientWidth; + var w = Math.min(W, vw - GUTTER * 2); + var flip = cr.left + w > vw - GUTTER; + var x = (flip ? cr.right - w : cr.left) - sr.left; + var icon = card.querySelector('.ib-icon'); + panel.classList.toggle('md-flip', flip); + panel.style.setProperty('--md-w', w + 'px'); + panel.style.setProperty('--md-cw', cr.width + 'px'); + panel.style.setProperty('--md-ch', cr.height + 'px'); + panel.style.setProperty('--md-pad', (icon ? icon.offsetTop : 0) + 'px'); + panel.style.setProperty('--md-x', x + 'px'); + panel.style.setProperty('--md-y', (cr.top - sr.top) + 'px'); + panel.style.setProperty('--md-cx', Math.max(0, w - cr.width) + 'px'); + panel.style.setProperty('--md-cy', Math.max(0, panel.offsetHeight - cr.height) + 'px'); + } + + function flyFrom(card, opening) { + if (!panel.animate) return null; + var c = card.getBoundingClientRect(), p = panel.getBoundingClientRect(); + if (!p.width || !p.height) return null; + var from = 'translate(' + (c.left - p.left) + 'px,' + (c.top - p.top) + 'px) scale(' + + (c.width / p.width) + ',' + (c.height / p.height) + ')'; + var frames = [{ transform: from, opacity: .4 }, { transform: 'none', opacity: 1 }]; + return panel.animate(opening ? frames : frames.reverse(), { + duration: opening ? 420 : 260, + easing: opening ? 'cubic-bezier(.2,.9,.25,1)' : 'cubic-bezier(.4,0,1,1)' + }); + } + + function openPanel(card) { + if (source === card && panelOpen()) return; + closePanel(true); + source = card; + fill(card); + opening = true; + if (floating.matches) { + backdrop = document.createElement('div'); + backdrop.className = 'mod-detail-backdrop'; + backdrop.addEventListener('click', function () { closePanel(); }); + section.appendChild(backdrop); + panel.classList.add('is-open'); + panel.style.setProperty('--md-top', Math.max(24, (window.innerHeight - panel.offsetHeight) / 2) + 'px'); + flyFrom(card, true); + } else { + panel.style.transition = 'none'; + place(card); + void panel.offsetWidth; + panel.style.transition = ''; + void panel.offsetWidth; + panel.classList.add('is-open'); + card.classList.add('is-source'); + trackTimer = setTimeout(function () { panel.classList.add('md-tracking'); }, 600); + } + panel.setAttribute('aria-hidden', 'false'); + section.classList.add('has-open-detail'); + setTimeout(function () { opening = false; }, 450); + } + + function closePanel(instant) { + if (!panelOpen() && !source) return; + var card = source; + clearTimeout(trackTimer); + ['--md-rx', '--md-ry', '--md-mx', '--md-my'].forEach(function (p) { panel.style.removeProperty(p); }); + panel.classList.remove('md-tracking'); + var done = function () { + panel.classList.remove('is-open'); + if (card) card.classList.remove('is-source'); + section.classList.remove('has-open-detail'); + if (backdrop) { backdrop.remove(); backdrop = null; } + }; + var anim = (!instant && floating.matches && card) ? flyFrom(card, false) : null; + if (anim) anim.onfinish = done; + else { + panel.classList.remove('is-open'); + // Desktop: the card reappears once the panel has folded back onto it. + if (card) setTimeout(function () { if (source !== card) card.classList.remove('is-source'); }, instant ? 0 : 380); + section.classList.remove('has-open-detail'); + if (backdrop) { backdrop.remove(); backdrop = null; } + } + panel.setAttribute('aria-hidden', 'true'); + source = null; + touched(); + } + + // Open: hover (desktop, short intent delay) or click / tap / Enter. + var hoverTimer = 0; + scroller.addEventListener('mouseover', function (e) { + if (floating.matches || dragging) return; + var card = e.target.closest('.mod-card.has-info'); + if (!card || card === source) return; + clearTimeout(hoverTimer); + hoverTimer = setTimeout(function () { if (!dragging) openPanel(card); }, 120); + }); + scroller.addEventListener('mouseout', function (e) { + if (floating.matches) return; + var card = e.target.closest('.mod-card.has-info'); + if (card && !card.contains(e.relatedTarget)) clearTimeout(hoverTimer); + }); + // The panel sits over the card; leaving the panel closes it. + panel.addEventListener('mouseleave', function (e) { + if (floating.matches) return; + if (source && source.contains(e.relatedTarget)) return; + closePanel(); + }); + panel.addEventListener('mousemove', function (e) { + if (floating.matches || !panelOpen()) return; + var r = panel.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))); + panel.style.setProperty('--md-ry', (dx * 5).toFixed(2) + 'deg'); + panel.style.setProperty('--md-rx', (-dy * 5).toFixed(2) + 'deg'); + panel.style.setProperty('--md-mx', (dx * 6).toFixed(1) + 'px'); + panel.style.setProperty('--md-my', (dy * 6 - 6).toFixed(1) + 'px'); + }); + + scroller.addEventListener('click', function (e) { + var card = e.target.closest('.mod-card.has-info'); + if (!card) return; + e.stopPropagation(); + openPanel(card); + }); + scroller.addEventListener('keydown', function (e) { + var card = e.target.closest('.mod-card.has-info'); + if (card && (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar')) { e.preventDefault(); openPanel(card); } + }); + panel.addEventListener('click', function (e) { + e.stopPropagation(); + if (e.target.closest('.md-close')) closePanel(); + }); + document.addEventListener('click', function () { if (panelOpen()) closePanel(); }); + document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && panelOpen()) closePanel(); }); + window.addEventListener('resize', function () { if (panelOpen()) closePanel(true); }, { passive: true }); + window.addEventListener('scroll', function () { if (panelOpen() && !floating.matches && !opening) closePanel(); }, { passive: true }); }); /* ── Schedule day filter ───────────────────────────────────────────────────── @@ -307,7 +527,7 @@ document.addEventListener('DOMContentLoaded', function () { 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; + if (!card || !pic || !card.animate) 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(' + @@ -376,7 +596,7 @@ document.addEventListener('DOMContentLoaded', function () { // 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; + if (floating.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))); diff --git a/site/index.php b/site/index.php index b43880c..4a2745d 100644 --- a/site/index.php +++ b/site/index.php @@ -170,32 +170,51 @@ if ($team): // ── Modalidades ───────────────────────────────────────────────────────────────── if ($modalidades): ?> -
+
Treinos -

Modalidades

+

Modalidades

Encontra a modalidade certa para ti.

- -
- -
diff --git a/uploads/modalidades/icons/fitme-crossfit-teen-s.svg b/uploads/modalidades/icons/fitme-crossfit-teen-s.svg new file mode 100644 index 0000000..c354b23 --- /dev/null +++ b/uploads/modalidades/icons/fitme-crossfit-teen-s.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-crossfit.svg b/uploads/modalidades/icons/fitme-crossfit.svg new file mode 100644 index 0000000..c62240d --- /dev/null +++ b/uploads/modalidades/icons/fitme-crossfit.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-cycling.svg b/uploads/modalidades/icons/fitme-cycling.svg new file mode 100644 index 0000000..6876aea --- /dev/null +++ b/uploads/modalidades/icons/fitme-cycling.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-fbb.svg b/uploads/modalidades/icons/fitme-fbb.svg new file mode 100644 index 0000000..9a8b6e4 --- /dev/null +++ b/uploads/modalidades/icons/fitme-fbb.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-fitburn.svg b/uploads/modalidades/icons/fitme-fitburn.svg new file mode 100644 index 0000000..8d769f7 --- /dev/null +++ b/uploads/modalidades/icons/fitme-fitburn.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-hybrid.svg b/uploads/modalidades/icons/fitme-hybrid.svg new file mode 100644 index 0000000..417866f --- /dev/null +++ b/uploads/modalidades/icons/fitme-hybrid.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-localizada.svg b/uploads/modalidades/icons/fitme-localizada.svg new file mode 100644 index 0000000..dc24a8d --- /dev/null +++ b/uploads/modalidades/icons/fitme-localizada.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-pilates.svg b/uploads/modalidades/icons/fitme-pilates.svg new file mode 100644 index 0000000..5c87ce6 --- /dev/null +++ b/uploads/modalidades/icons/fitme-pilates.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-yoga.svg b/uploads/modalidades/icons/fitme-yoga.svg new file mode 100644 index 0000000..1a41510 --- /dev/null +++ b/uploads/modalidades/icons/fitme-yoga.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-yourfit-fight.svg b/uploads/modalidades/icons/fitme-yourfit-fight.svg new file mode 100644 index 0000000..bb9d111 --- /dev/null +++ b/uploads/modalidades/icons/fitme-yourfit-fight.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-yourfit-mind.svg b/uploads/modalidades/icons/fitme-yourfit-mind.svg new file mode 100644 index 0000000..70cb1b8 --- /dev/null +++ b/uploads/modalidades/icons/fitme-yourfit-mind.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-yourfit-pulse.svg b/uploads/modalidades/icons/fitme-yourfit-pulse.svg new file mode 100644 index 0000000..e9bc9cf --- /dev/null +++ b/uploads/modalidades/icons/fitme-yourfit-pulse.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-yourfit-pump.svg b/uploads/modalidades/icons/fitme-yourfit-pump.svg new file mode 100644 index 0000000..4ad2794 --- /dev/null +++ b/uploads/modalidades/icons/fitme-yourfit-pump.svg @@ -0,0 +1 @@ + diff --git a/uploads/modalidades/icons/fitme-zumba.svg b/uploads/modalidades/icons/fitme-zumba.svg new file mode 100644 index 0000000..0bd52b9 --- /dev/null +++ b/uploads/modalidades/icons/fitme-zumba.svg @@ -0,0 +1 @@ +