'use strict'; /* ── Lazy background images (.set-bg / data-setbg) ────────────────────── Backgrounds are applied as their element approaches the viewport, so below-the-fold images (contactos, newsletter, footer) don't compete with the hero for bandwidth. */ (function () { var els = Array.prototype.slice.call(document.querySelectorAll('.set-bg')); function apply(el) { var bg = el.getAttribute('data-setbg'); if (bg) el.style.backgroundImage = "url('" + bg + "')"; } if ('IntersectionObserver' in window) { var io = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { apply(entry.target); io.unobserve(entry.target); } }); }, { rootMargin: '300px 0px' }); els.forEach(function (el) { io.observe(el); }); } else { els.forEach(apply); } })(); /* ── Back to top button ─────────────────────────────────────────────── */ document.addEventListener('DOMContentLoaded', function () { var btn = document.getElementById('backToTop'); if (!btn) return; function toggleBtn() { if (window.scrollY > 400) btn.classList.add('visible'); else btn.classList.remove('visible'); } window.addEventListener('scroll', toggleBtn, { passive: true }); btn.addEventListener('click', function () { window.scrollTo({ top: 0, behavior: 'smooth' }); }); }); /* Scroll-reveal: elements fade + slide up as they enter viewport */ if ('IntersectionObserver' in window) { (function () { var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { entry.target.classList.add('revealed'); observer.unobserve(entry.target); } }); }, { rootMargin: '0px 0px -60px 0px', threshold: 0.05 }); // Target all cards, sections, and content blocks document.querySelectorAll( 'section[id], .icon-box-item, .trainer-item, .schedule_box, ' + '.classes-section .col-lg-6, .contact-info-warp, .rounded_frame_grey' ).forEach(function (el) { observer.observe(el); }); // Hero reveals immediately after preloader window.addEventListener('load', function () { var hero = document.querySelector('.hero-section'); if (hero) hero.classList.add('revealed'); }); })(); } /* Hide scroll hint after user scrolls the schedule */ document.addEventListener('DOMContentLoaded', function () { var panel = document.querySelector('.schedule_panel'); var hint = document.querySelector('.schedule-scroll-hint'); if (panel && hint) { panel.addEventListener('scroll', function () { hint.style.opacity = '0'; hint.style.transition = 'opacity .3s'; setTimeout(function () { hint.style.display = 'none'; }, 300); }, { once: true }); } }); /* ── 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. */ document.addEventListener('DOMContentLoaded', function () { var scroller = document.querySelector('.mod-scroll'); // 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'); }); } 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'); }); } cards.forEach(function (card) { card.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(); } }); }); document.addEventListener('click', function () { closeAll(null); syncPause(); }); }); /* ── Schedule day filter ───────────────────────────────────────────────────── The on-screen schedule shows the chosen day (phones) or that day centred between the day before and after (desktop ≥992px); a day-picker bar switches days (each header + class cell is tagged with data-day). Defaults to today when the gym is open that day, otherwise the first day. The JPG export is unaffected — its clone removes every .sched-hide and re-renders the full week. */ document.addEventListener('DOMContentLoaded', function () { // Works on the home page (#horario) and the CrossFit page (bare .schedule). var scope = document.querySelector('.schedule'); if (!scope) return; var filter = scope.querySelector('.schedule-day-filter'); var cells = scope.querySelectorAll('.schedule_cell, .schedule_dayhead'); if (!filter || !cells.length) return; var buttons = filter.querySelectorAll('.sdf-btn'); var labels = scope.querySelectorAll('.schedule_section_label'); // Desktop shows three days — the chosen one centred between the day before // and the day after (wrapping round the week); phones show just the one. var wide = window.matchMedia('(min-width: 992px)'); var week = Array.prototype.map.call(buttons, function (b) { return b.getAttribute('data-day'); }); var current = null; // Toggle classes (not inline display) — .sched-hide carries !important so it // reliably wins, and the export clone simply strips it (and ignores the // .sched-pos-* columns) to show every day. function showDay(day) { current = day; var i = week.indexOf(day), n = week.length; var three = wide.matches && n >= 3; var visible = three ? [week[(i - 1 + n) % n], day, week[(i + 1) % n]] : [day]; scope.classList.toggle('is-3day', three); cells.forEach(function (c) { var pos = visible.indexOf(c.getAttribute('data-day')); c.classList.toggle('sched-hide', pos === -1); c.classList.remove('sched-pos-1', 'sched-pos-2', 'sched-pos-3', 'sched-side'); if (pos !== -1) { var col = three ? pos + 1 : 1; c.classList.add('sched-pos-' + col); if (three && col !== 2) c.classList.add('sched-side'); } }); buttons.forEach(function (b) { b.classList.toggle('active', b.getAttribute('data-day') === day); }); // Show a Manhã / Tarde divider only when a visible day has a class in that block. labels.forEach(function (lbl) { var sec = lbl.getAttribute('data-section'); var has = visible.some(function (d) { return !!scope.querySelector('.schedule_cell[data-day="' + d + '"][data-section="' + sec + '"]'); }); lbl.classList.toggle('sched-hide', !has); }); } filter.addEventListener('click', function (e) { var btn = e.target.closest('.sdf-btn'); if (btn) showDay(btn.getAttribute('data-day')); }); // Clicking a side day's header centres it. scope.addEventListener('click', function (e) { var head = e.target.closest('.schedule_dayhead.sched-side'); if (head) showDay(head.getAttribute('data-day')); }); var reflow = function () { if (current) showDay(current); }; if (wide.addEventListener) wide.addEventListener('change', reflow); else if (wide.addListener) wide.addListener(reflow); // Pre-select today when the schedule has a button for it (PT day names), // otherwise the first day. var ptDays = { 1: 'segunda', 2: 'terça', 3: 'quarta', 4: 'quinta', 5: 'sexta', 6: 'sábado' }; var today = ptDays[new Date().getDay()]; var todayBtn = today ? filter.querySelector('.sdf-btn[data-day="' + today + '"]') : null; var start = todayBtn || buttons[0]; if (start) showDay(start.getAttribute('data-day')); }); /* ── Contact form: stretch the message box to meet the map ─────────────────── On desktop (≥992px, where the info+map column sits beside the form) the Mensagem textarea grows so its bottom lines up with the bottom of the compact map; the consent checkbox and submit button then follow just below it. Off on mobile, where everything is stacked. */ document.addEventListener('DOMContentLoaded', function () { var ta = document.querySelector('#contactos .contact-form textarea'); var map = document.querySelector('#contactos .map-side'); if (!ta || !map) return; var mq = window.matchMedia('(min-width: 992px)'); function sync() { if (!mq.matches) { ta.style.height = ''; return; } ta.style.height = 'auto'; // reset before measuring var taTop = ta.getBoundingClientRect().top; var mapBottom = map.getBoundingClientRect().bottom; ta.style.height = Math.max(150, Math.round(mapBottom - taTop)) + 'px'; } sync(); window.addEventListener('load', sync); // re-measure once fonts/layout settle if (mq.addEventListener) mq.addEventListener('change', sync); var t; window.addEventListener('resize', function () { clearTimeout(t); t = setTimeout(sync, 150); }, { passive: true }); }); /* ── 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'); 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'); } 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) { 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) { items.forEach(function (it) { if (it !== except) setOpen(it, false); }); } items.forEach(function (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; } 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); setOpen(item, true); }); item.addEventListener('keydown', function (e) { if (e.key === 'Escape') { setOpen(item, false); item.blur(); } else if ((e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') && e.target === item) { e.preventDefault(); setOpen(item, !item.classList.contains('is-open')); } }); }); 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 ───────── */ document.addEventListener('DOMContentLoaded', function () { var select = document.querySelector('#joinModal select[name="membership_id"]'); if (!select) return; document.querySelectorAll('[data-target="#joinModal"][data-membership]').forEach(function (btn) { btn.addEventListener('click', function () { select.value = btn.getAttribute('data-membership'); }); }); });