diff --git a/site/assets/css/style.css b/site/assets/css/style.css index 303c18d..831a32b 100644 --- a/site/assets/css/style.css +++ b/site/assets/css/style.css @@ -391,27 +391,30 @@ section[id] { /* ── CrossFit nav pill (follows the gym's brand colour) ─────────────────────────────────────────────────────── */ .crossfit-nav-link { - color: var(--fitme-accent-ink) !important; + /* the gym's own brand colour (not the darker "ink" shade), filled so the text + stays readable on the light navbar: dark/white text via --fitme-on-accent */ + background: var(--fitme-accent) !important; + color: var(--fitme-on-accent) !important; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; - border: 2px solid var(--fitme-accent-ink); + border: 2px solid var(--fitme-accent); border-radius: var(--fitme-radius); padding: 6px 14px !important; - transition: background-color var(--fitme-transition), color var(--fitme-transition); + transition: background-color .4s ease, color .4s ease, border-color .4s ease; } .crossfit-nav-link:after { display: none; } -/* Hover and current-page state: filled pill, white text (must outrank the +/* Hover and current-page state: near-black face with white text (outranks the generic .nav-item.active .nav-link transparent-background rule above) */ .crossfit-nav-link:hover, .nav-item.active .crossfit-nav-link { - background: var(--fitme-accent) !important; + background: color-mix(in srgb, var(--fitme-accent) 10%, #000) !important; border-color: var(--fitme-accent); - color: var(--fitme-on-accent) !important; + color: #fff !important; } /* ── Other-gym button (right end of the navbar) ────────────────────────────── */ @@ -683,7 +686,8 @@ section[id] { padding: 4px 8px; color: #fff; font: inherit; - font-weight: 600; + font-weight: 300; + letter-spacing: .02em; text-shadow: 0 1px 6px rgba(0, 0, 0, .6); cursor: pointer; transition: color .3s ease; @@ -691,8 +695,7 @@ section[id] { .hero-text-link:hover, .hero-text-link:focus-visible { color: var(--fitme-accent); - text-decoration: underline; - text-underline-offset: 4px; + text-decoration: none; } .hero-cta-row .site-btn { @@ -990,10 +993,7 @@ section[id] { position: absolute; inset: 0; border-radius: 0; - background: - radial-gradient(ellipse at 30% 20%, rgba(255, 255, 255, 0.25) 0%, transparent 50%), - radial-gradient(ellipse at 70% 80%, rgba(255, 255, 255, 0.08) 0%, transparent 45%), - linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, transparent 40%, rgba(0, 0, 0, 0.1) 100%); + background: none; /* no gloss gradient over the member photos */ pointer-events: none; z-index: 1; } @@ -1013,13 +1013,6 @@ section[id] { transform: rotate(0deg) translateY(-8px) scale(1.05); } -.trainer-item .trainer-pic:hover:after { - background: - radial-gradient(ellipse at 30% 20%, rgba(255, 255, 255, 0.32) 0%, transparent 50%), - radial-gradient(ellipse at 70% 80%, rgba(255, 255, 255, 0.10) 0%, transparent 45%), - linear-gradient(135deg, rgba(255, 255, 255, 0.18) 0%, transparent 40%, rgba(0, 0, 0, 0.08) 100%); -} - /* Same hover as the buttons / Mensalidades cards: the frame's 4px edge turns into a conic brand-colour sweep (--pc-a / pc-spin, shared) over a dimmed base of the same colour, with a soft glow round it. The photo covers the @@ -1996,7 +1989,7 @@ h3.contact-title { .consent-check a { color: #fff; - text-decoration: underline; + text-decoration: none; } .consent-check a:hover { @@ -2769,7 +2762,7 @@ h3.contact-title { padding: 12px 4px; margin: -12px -4px; min-height: 44px; - text-decoration: underline; + text-decoration: none; transition: color var(--fitme-transition); } @@ -3399,7 +3392,7 @@ h3.contact-title { cursor: pointer; } .event-consent input { margin-top: 3px; flex: 0 0 auto; } -.event-consent a { color: var(--fitme-accent-ink); text-decoration: underline; } +.event-consent a { color: var(--fitme-accent-ink); text-decoration: none; } @media (max-width: 575px) { .event-form-grid { grid-template-columns: 1fr; } } @@ -3563,29 +3556,756 @@ h3.contact-title { .trainer-item.is-open { z-index: 31; } +/* ── Open detail cards (team + Modalidades): the same spinning brand edge ────── + A 3px ring cut out of ::after with a mask, shown while the card is open, plus + a soft brand glow added to the existing lift shadow. ─────────────────────── */ +.mod-detail::after, +.trainer-detail::after { + content: ''; + position: absolute; + inset: 0; + z-index: 5; + padding: 3px; + border-radius: inherit; + background: conic-gradient(from var(--pc-a), rgba(var(--fitme-accent-rgb), .35) 0deg, rgba(var(--fitme-accent-rgb), .35) 150deg, + var(--fitme-accent) 250deg, #f4ff9c 300deg, var(--fitme-accent) 330deg, + rgba(var(--fitme-accent-rgb), .35) 360deg); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0); + opacity: 0; + transition: opacity .45s ease; + pointer-events: none; +} +.mod-detail.is-open::after, +.trainer-item.is-open .trainer-detail::after { opacity: 1; animation: pc-spin 3.2s linear infinite; } +.mod-detail.is-open { + box-shadow: 0 12px 30px rgba(var(--fitme-dark-rgb), .16), 0 2px 6px rgba(var(--fitme-dark-rgb), .10), + 0 0 22px 2px rgba(var(--fitme-accent-rgb), .35); +} +.trainer-item.is-open .trainer-detail { + box-shadow: 0 12px 30px rgba(0, 0, 0, .32), 0 2px 6px rgba(0, 0, 0, .18), + 0 0 22px 2px rgba(var(--fitme-accent-rgb), .35); +} +@media (prefers-reduced-motion: reduce) { + .mod-detail.is-open::after, + .trainer-item.is-open .trainer-detail::after { animation: none; } +} + + +.td-photo { + 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: calc(var(--td-ph, 200px) - 8px); + object-fit: cover; + object-position: top; + display: block; +} + +.td-body { + flex: 1 1 auto; + 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 { + display: block; + font-size: 1.15rem; + font-weight: 800; + color: var(--fitme-dark); + padding-right: 20px; +} + +.td-title { + display: block; + font-size: .82rem; + font-weight: 700; + color: var(--fitme-accent-ink); + margin-bottom: .6rem; +} + +.td-bio { + font-size: .88rem; + color: var(--fitme-text-soft); + margin-bottom: .6rem; +} + +.td-bio p:last-child { margin-bottom: 0; } + +.td-block + .td-block { margin-top: .55rem; } + +.td-label { + display: block; + font-size: .68rem; + font-weight: 800; + letter-spacing: .14em; + text-transform: uppercase; + color: var(--fitme-text-muted); + margin-bottom: .25rem; +} + +.td-block ul { + list-style: none; + padding: 0; + margin: 0; +} + +.td-block li { + position: relative; + padding-left: 14px; + font-size: .87rem; + line-height: 1.45; + color: var(--fitme-text); +} + +.td-block li:before { + content: ''; + position: absolute; + left: 0; + top: .55em; + width: 6px; + height: 6px; + border-radius: 1px; + background: var(--fitme-accent); +} + +.td-close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + width: 44px; + height: 44px; + align-items: center; + justify-content: center; + background: transparent; + border: 0; + font-size: 1.6rem; + line-height: 1; + color: var(--fitme-text-muted); + cursor: pointer; + display: none; +} + +/* 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.td-flip { + position: fixed; + top: var(--td-top, 20vh); + left: 16px; + right: 16px; + width: auto; + max-width: 560px; + min-height: 0; + margin: 0 auto; + max-height: calc(100dvh - 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:after { + content: ''; + position: fixed; + inset: 0; + z-index: 29; + background: rgba(0, 0, 0, .55); + } + .td-close { display: flex; } + .td-photo { flex-basis: 34%; border-width: 3px; } + .td-photo img { min-height: 180px; } + .td-body, + .td-flip .td-body { max-height: calc(100dvh - 48px); padding: 16px 16px 16px 14px; transform: none; } +} + +@media (max-width: 575px) { + .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; } +} + + +/* ══════════════════════════════════════════════════════════════════════════ + Mensalidades — cartões de preço + ══════════════════════════════════════════════════════════════════════════ */ +/* Same matte-black metal texture as the reviews section / footer, instead of + the plain light surface — the two dark bands now bookend the page. */ +.pricing-section { + padding: 90px 0 70px; + background-color: #141517; + background-image: var(--metal-layers); + background-size: var(--metal-sizes); + color: var(--fitme-ash); +} + +.pricing-section .section-title h2 { color: #fff; } +.pricing-section .section-subtitle { color: var(--fitme-ash); } + +.pricing-grid { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: stretch; + gap: 24px; +} + +/* Spinning green edge (hover). --pc-a is a registered , so the conic + gradient's start angle can be animated — an unregistered custom property + would just jump. The gradient is centred on the mouse (--pc-x/--pc-y from + main.js), so the sweep pivots around the pointer; the card also tilts and + drifts toward it (--pc-rx/ry/mx/my), like the team cards. + Layers behind the content: ::after = the blurred gradient (the glow), + ::before = the card face with a 2px border whose border-box shows the crisp + gradient (padding-box/border-box backgrounds keep the rounded corners). */ +@property --pc-a { + syntax: ''; + inherits: false; + initial-value: 0deg; +} + +@keyframes pc-spin { + to { --pc-a: 360deg; } +} + +.price-card { + --pc-face: #fff; + --pc-edge: var(--fitme-surface-2); + --pc-bw: 1px; + --pc-scale: 1; + position: relative; + z-index: 0; + flex: 0 1 270px; + display: flex; + flex-direction: column; + background: var(--pc-face); + border: 1px solid transparent; + border-radius: 6px; + padding: 30px 26px 26px; + box-shadow: var(--fitme-shadow); + transform: perspective(900px) rotateX(var(--pc-rx, 0deg)) rotateY(var(--pc-ry, 0deg)) + translate(var(--pc-mx, 0px), calc(var(--pc-my, 0px) + var(--pc-lift, 0px))) scale(var(--pc-scale)); + transition: transform .45s cubic-bezier(.34, 1.35, .64, 1), box-shadow var(--fitme-transition); +} + +/* While the pointer is on it, follow quickly instead of springing. */ +.price-card.pc-tracking { + transition: transform .15s ease-out, box-shadow var(--fitme-transition); +} + +.price-card::before, +.price-card::after { + content: ''; + position: absolute; + pointer-events: none; +} + +.price-card::before { + inset: calc(-1 * var(--pc-bw)); + z-index: -1; + border-radius: calc(6px + var(--pc-bw)); + border: var(--pc-bw) solid transparent; + background: + linear-gradient(var(--pc-face), var(--pc-face)) padding-box, + linear-gradient(var(--pc-edge), var(--pc-edge)) border-box; +} + +.price-card::after { + inset: -3px; + z-index: -2; + border-radius: 10px; + background: conic-gradient(from var(--pc-a) at var(--pc-x, 50%) var(--pc-y, 50%), + rgba(var(--fitme-accent-rgb), 0) 0deg, rgba(var(--fitme-accent-rgb), 0) 150deg, + var(--fitme-accent) 250deg, #f4ff9c 300deg, var(--fitme-accent) 330deg, + rgba(var(--fitme-accent-rgb), 0) 360deg); + filter: blur(12px); + opacity: 0; + transition: opacity .35s ease; +} + +.price-card:hover { + --pc-lift: -4px; + --pc-bw: 2px; + box-shadow: var(--fitme-shadow-lg); +} + +.price-card:hover::before { + background: + linear-gradient(var(--pc-face), var(--pc-face)) padding-box, + conic-gradient(from var(--pc-a) at var(--pc-x, 50%) var(--pc-y, 50%), + rgba(var(--fitme-accent-rgb), 0) 0deg, rgba(var(--fitme-accent-rgb), 0) 150deg, + var(--fitme-accent) 250deg, #f4ff9c 300deg, var(--fitme-accent) 330deg, + rgba(var(--fitme-accent-rgb), 0) 360deg) border-box; + animation: pc-spin 3.2s linear infinite; +} + +.price-card:hover::after { + opacity: .75; + animation: pc-spin 3.2s linear infinite; +} + +.price-card.is-featured { + --pc-face: var(--fitme-dark-2); + --pc-edge: var(--fitme-accent); + --pc-bw: 2px; + border-width: 2px; + color: var(--fitme-ash); + box-shadow: 0 18px 50px rgba(var(--fitme-dark-rgb), .28); +} + +@media (min-width: 992px) { + .price-card.is-featured { --pc-scale: 1.04; } +} + +@media (prefers-reduced-motion: reduce) { + .price-card:hover::before, + .price-card:hover::after { animation: none; } +} + +.pc-badge { + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, -50%); + white-space: nowrap; + background: var(--fitme-accent); + color: var(--fitme-dark); + font-size: .72rem; + font-weight: 800; + letter-spacing: .12em; + text-transform: uppercase; + padding: 6px 14px; + border-radius: 999px; +} + +.pc-badge i { font-size: .65rem; } + +.price-card .pc-name { + font-size: 1.35rem; + font-weight: 900; + margin: 0 0 .35rem; + color: var(--fitme-dark); +} + +.price-card.is-featured .pc-name { color: #fff; } + +.pc-sub { + font-size: .9rem; + line-height: 1.45; + color: var(--fitme-text-muted); + margin: 0 0 1rem; +} + +.price-card.is-featured .pc-sub { color: var(--fitme-ash); } + +.pc-price { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 4px 8px; + padding: .9rem 0 1rem; + border-top: 1px solid var(--fitme-surface-2); + border-bottom: 1px solid var(--fitme-surface-2); + margin-bottom: 1rem; +} + +.price-card.is-featured .pc-price { border-color: rgba(255, 255, 255, .12); } + +.pc-amount { + font-size: 2.3rem; + font-weight: 900; + line-height: 1; + color: var(--fitme-dark); +} + +.pc-amount.is-text { font-size: 1.35rem; } + +.price-card.is-featured .pc-amount { color: var(--fitme-accent); } + +.pc-suffix { + font-size: .88rem; + color: var(--fitme-text-muted); +} + +.price-card.is-featured .pc-suffix { color: var(--fitme-ash); } + +.pc-features { + list-style: none; + padding: 0; + margin: 0 0 1.4rem; + flex: 1 1 auto; +} + +.pc-features li { + display: flex; + gap: 10px; + font-size: .92rem; + line-height: 1.4; + padding: 4px 0; + color: var(--fitme-text-soft); +} + +.price-card.is-featured .pc-features li { color: #fff; } + +.pc-features i { + color: var(--fitme-accent-ink); + margin-top: 3px; + font-size: .8rem; +} + +.price-card.is-featured .pc-features i { color: var(--fitme-accent); } + +.price-card .pc-cta { + width: 100%; + border: none; + margin-top: auto; +} + +.price-card:not(.is-featured) .pc-cta { + background: var(--fitme-dark-2); + color: var(--fitme-accent); +} + +.price-card:not(.is-featured) .pc-cta:hover { + background: var(--fitme-dark-3); + box-shadow: none; +} + +@media (max-width: 575px) { + .pricing-section { padding: 60px 0 50px; } + .price-card { flex-basis: 100%; } +} + +/* ══════════════════════════════════════════════════════════════════════════ + Reviews Google + ══════════════════════════════════════════════════════════════════════════ */ +/* Matte black "gym machine" metal, shared by the reviews section and the + footer: fine powder-coat grain (light + dark speckle from SVG turbulence), + a soft diffuse sheen like light on a frame tube, and barely-there brushed + streaks. All inline, no image requests. */ +:root { + --metal-layers: + radial-gradient(120% 70% at 22% 0%, rgba(255, 255, 255, .055), transparent 60%), + linear-gradient(112deg, transparent 38%, rgba(255, 255, 255, .03) 50%, transparent 62%), + repeating-linear-gradient(0deg, rgba(255, 255, 255, .012) 0 1px, transparent 1px 3px), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .09 -.03'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='d'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='1' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .22 -.06'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23d)'/%3E%3C/svg%3E"); + --metal-sizes: 100% 100%, 100% 100%, 100% 3px, 180px 180px, 140px 140px; +} + +.reviews-section { + padding: 90px 0 60px; + background: var(--fitme-white); +} + +.reviews-summary { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 6px 12px; + margin: 1rem 0 0; + color: var(--fitme-text-muted); +} + +.reviews-summary strong { + font-size: 1.8rem; + font-weight: 900; + color: var(--fitme-text); + line-height: 1; +} + +.rv-stars { + color: #FBBC04; + letter-spacing: 2px; + font-size: .95rem; +} + +.reviews-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 22px; +} + +.review-card { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + margin: 0; + background: var(--fitme-dark-2); + border: 1px solid var(--fitme-dark-3); + border-radius: 6px; + padding: 28px 24px 24px; +} + +.rv-avatar { + width: 84px; + height: 84px; + border-radius: 50%; + overflow: hidden; + border: 3px solid var(--fitme-accent); + background: var(--fitme-dark-3); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: .8rem; + flex: 0 0 auto; +} + +.rv-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.rv-avatar span { + font-size: 2rem; + font-weight: 900; + color: var(--fitme-accent); +} + +.rv-author { + display: flex; + flex-direction: column; + font-size: 1.05rem; + font-weight: 800; + color: #fff; +} + +.rv-author a { color: #fff; } +.rv-author a:hover { color: var(--fitme-accent); } + +.rv-author small { + font-size: .78rem; + font-weight: 400; + color: var(--fitme-ash); + margin-top: 2px; +} + +.rv-stars-lg { + font-size: 1.3rem; + letter-spacing: 3px; + margin: .6rem 0 .2rem; +} + +.rv-text { + margin: .6rem 0 0; + font-size: .85rem; + line-height: 1.55; + color: var(--fitme-ash); + display: -webkit-box; + -webkit-line-clamp: 6; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.reviews-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 18px; + margin-top: 2rem; +} + +/* Plain text link — "Deixa a tua review" stays the one highlighted action + (.site-btn), this is secondary. */ +.reviews-link { + font-weight: 700; + color: var(--fitme-text-soft); + text-decoration: none; +} + +.reviews-link:hover { + color: var(--fitme-text); +} + +.reviews-attribution { + margin: 1.2rem 0 0; + text-align: center; + font-size: .75rem; + color: var(--fitme-text-muted); + opacity: .8; +} + +/* ══════════════════════════════════════════════════════════════════════════ + Modalidades — animated icons + The FitMe icon set is inlined as (see + modalidade_icon_html()). Every drawable element has pathLength="1", so a + stroke-dasharray of 1 lets the lines draw themselves: once when the card + scrolls into view, again on hover / focus, and in the detail panel as it + opens. On top of that each family has its own small movement: + lift — weights going up (Pump, CrossFit, FBB, Core, Total Workout) + punch — a jab (Fight, Bootcamp) + breathe — slow in / out (Mind, Yoga, Pilates) + bounce — hop with squash (Zumba, Jump, Pulse, Hybrid) + flicker — flame (FitBurn) + ride — pedalling rock (Cycling) + run — running bob (Run3070, On Ramp, Localizada) + ══════════════════════════════════════════════════════════════════════════ */ +.ib-icon svg.mi { + width: 44px; + height: 44px; + color: #141414; + overflow: visible; + transform-box: fill-box; + transform-origin: 50% 50%; +} + +.mi [pathLength] { + stroke-dasharray: 1; + stroke-dashoffset: 0; +} + +@keyframes mi-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } } +@keyframes mi-draw-h { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } } + +/* Draw-in once as the card appears (.revealed comes from the scroll observer). */ +.mod-card.revealed .mi [pathLength] { + animation: mi-draw .9s cubic-bezier(.5, 0, .2, 1) both; +} +.mod-card.revealed .mi [pathLength]:nth-child(2) { animation-delay: .12s; } +.mod-card.revealed .mi [pathLength]:nth-child(3) { animation-delay: .24s; } +.mod-card.revealed .mi [pathLength]:nth-child(n+4) { animation-delay: .34s; } + +/* Hover / focus / open panel: redraw (a second keyframe name restarts it). */ +.mod-card:hover .mi [pathLength], +.mod-card:focus-visible .mi [pathLength], +.mod-detail.is-open .mi [pathLength] { + animation: mi-draw-h .6s cubic-bezier(.5, 0, .2, 1) both; +} +.mod-card:hover .mi [pathLength]:nth-child(2), +.mod-detail.is-open .mi [pathLength]:nth-child(2) { animation-delay: .08s; } +.mod-card:hover .mi [pathLength]:nth-child(n+3), +.mod-detail.is-open .mi [pathLength]:nth-child(n+3) { animation-delay: .16s; } + +@keyframes mi-lift { + 0%, 100% { transform: translateY(0); } + 30% { transform: translateY(-6px); } + 55% { transform: translateY(1px); } + 75% { transform: translateY(-2px); } +} +@keyframes mi-punch { + 0%, 100% { transform: translateX(0) rotate(0); } + 25% { transform: translateX(-3px) rotate(-6deg); } + 45% { transform: translateX(7px) rotate(4deg); } + 65% { transform: translateX(-1px) rotate(0); } +} +@keyframes mi-breathe { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.13); } +} +@keyframes mi-bounce { + 0%, 100% { transform: translateY(0) scale(1, 1); } + 18% { transform: translateY(0) scale(1.12, .86); } + 50% { transform: translateY(-9px) scale(.94, 1.07); } + 80% { transform: translateY(0) scale(1.06, .94); } +} +@keyframes mi-flicker { + 0%, 100% { transform: scale(1) skewX(0); } + 20% { transform: scale(1.06, .93) skewX(-4deg); } + 40% { transform: scale(.95, 1.09) skewX(3deg); } + 60% { transform: scale(1.04, .97) skewX(-2deg); } + 80% { transform: scale(.98, 1.05) skewX(2deg); } +} +@keyframes mi-ride { + 0%, 100% { transform: translateX(0) rotate(0); } + 25% { transform: translateX(-2px) rotate(-5deg); } + 75% { transform: translateX(3px) rotate(3deg); } +} +@keyframes mi-run { + 0%, 100% { transform: translateY(0) rotate(0); } + 25% { transform: translateY(-4px) rotate(-6deg); } + 50% { transform: translateY(0) rotate(0); } + 75% { transform: translateY(-4px) rotate(6deg); } +} + +.mod-card:hover .mi, .mod-card:focus-visible .mi, .mod-detail.is-open .mi { + animation-duration: .9s; + animation-timing-function: cubic-bezier(.45, 0, .25, 1); + animation-fill-mode: both; + animation-delay: .1s; +} +.mod-card:hover .mi[data-anim="lift"], .mod-card:focus-visible .mi[data-anim="lift"], .mod-detail.is-open .mi[data-anim="lift"] { animation-name: mi-lift; } +.mod-card:hover .mi[data-anim="punch"], .mod-card:focus-visible .mi[data-anim="punch"], .mod-detail.is-open .mi[data-anim="punch"] { animation-name: mi-punch; animation-duration: .7s; animation-iteration-count: 2; } +.mod-card:hover .mi[data-anim="breathe"], .mod-card:focus-visible .mi[data-anim="breathe"], .mod-detail.is-open .mi[data-anim="breathe"] { animation-name: mi-breathe; animation-duration: 1.8s; animation-iteration-count: 2; } +.mod-card:hover .mi[data-anim="bounce"], .mod-card:focus-visible .mi[data-anim="bounce"], .mod-detail.is-open .mi[data-anim="bounce"] { animation-name: mi-bounce; animation-duration: .75s; animation-iteration-count: 2; } +.mod-card:hover .mi[data-anim="flicker"], .mod-card:focus-visible .mi[data-anim="flicker"], .mod-detail.is-open .mi[data-anim="flicker"] { animation-name: mi-flicker; animation-iteration-count: 3; transform-origin: 50% 100%; } +.mod-card:hover .mi[data-anim="ride"], .mod-card:focus-visible .mi[data-anim="ride"], .mod-detail.is-open .mi[data-anim="ride"] { animation-name: mi-ride; animation-duration: .6s; animation-iteration-count: 3; } +.mod-card:hover .mi[data-anim="run"], .mod-card:focus-visible .mi[data-anim="run"], .mod-detail.is-open .mi[data-anim="run"] { animation-name: mi-run; animation-duration: .6s; animation-iteration-count: 3; } + +/* Detail panel (desktop): icon + name start where they sit on the card, then + glide to the vertical centre of the taller panel column as it opens + (--md-pad-open from main.js). Phones centre it with their own layout. */ +@media (min-width: 768px) and (hover: hover) { + .md-card { + transition: padding-top .38s cubic-bezier(.4, 0, .2, 1); + } + .mod-detail.is-open .md-card { + padding-top: var(--md-pad-open, var(--md-pad, 0px)); + transition: padding-top .55s cubic-bezier(.2, .9, .25, 1) .05s; + } +} + +@media (max-width: 767px) { + .ib-icon svg.mi { width: 34px; height: 34px; } +} + +@media (prefers-reduced-motion: reduce) { + .mi, .mi [pathLength] { animation: none !important; } +} + /* ── Galeria — endless horizontal strip + lightbox (site/includes/gallery.php) ── - Full-height photos alternate with 2x2 grids of four. The strip is rendered - --gal-copies times and slides by exactly one copy, so the loop is seamless; - it pauses on hover / focus. With "reduce motion" it is a plain swipeable row. */ + Full-height photos alternate with 2x2 grids of four. The strip is four copies + long; main.js auto-scrolls it, supports click-and-drag / swipe, and jumps by + exactly one copy to loop. */ .gal-wrap { --gal-h: 420px; --gal-gap: 12px; - overflow: hidden; + overflow-x: auto; /* scrolled by main.js (auto-advance + drag) and by touch */ + overflow-y: hidden; + scrollbar-width: none; margin: 0 0 70px; + cursor: grab; + user-select: none; + -webkit-user-select: none; -webkit-mask-image: linear-gradient(to right, transparent 0, #000 5%, #000 95%, transparent 100%); mask-image: linear-gradient(to right, transparent 0, #000 5%, #000 95%, transparent 100%); } -.gal-track { - display: flex; - width: max-content; - animation: gal-scroll var(--gal-dur, 60s) linear infinite; - will-change: transform; -} -.gal-marquee:hover .gal-track, -.gal-marquee:focus-within .gal-track { animation-play-state: paused; } -@keyframes gal-scroll { - to { transform: translateX(calc(-100% / var(--gal-copies, 2))); } -} +.gal-wrap::-webkit-scrollbar { display: none; } +.gal-wrap.is-dragging { cursor: grabbing; } +.gal-wrap img { -webkit-user-drag: none; } +.gal-track { display: flex; width: max-content; } .gal-set { display: flex; flex: 0 0 auto; } .gal-unit { flex: 0 0 auto; @@ -3603,11 +4323,6 @@ h3.contact-title { @media (max-width: 991px) { .gal-wrap { --gal-h: 340px; } } @media (max-width: 575px) { .gal-wrap { --gal-h: 260px; --gal-gap: 8px; margin-bottom: 48px; } } -@media (prefers-reduced-motion: reduce) { - .gal-wrap { overflow-x: auto; -webkit-mask-image: none; mask-image: none; scrollbar-width: thin; } - .gal-track { animation: none; } - .gal-set[aria-hidden="true"] { display: none; } -} .gal-link { display: block; @@ -3680,26 +4395,16 @@ h3.contact-title { .gal-link:hover::after { animation: none; } } -/* ── Photos at rest: 10% desaturated (team photos also get a light brand-colour - tint), both lifted on hover. ──────────────────────────────────────────── */ +/* ── Photos at rest: 10% desaturated + a discreet brand-colour tint (gallery: a + gradient fading to transparent; team: a light multiply layer), both lifted on + hover. The tint follows each gym's brand colour. ───────────────────────── */ .gal-link img { filter: saturate(.9); } .gal-link:hover img, .gal-link:focus-visible img { filter: saturate(1) brightness(1.05); } .trainer-item .trainer-pic img { filter: saturate(.9); transition: filter .45s ease; } -.trainer-item .trainer-pic::before { - content: ''; - position: absolute; - inset: 0; - z-index: 1; - background: color-mix(in srgb, var(--fitme-accent) 14%, #fff); - mix-blend-mode: multiply; - transition: opacity .45s ease; - pointer-events: none; -} .trainer-item .trainer-pic:hover img { filter: saturate(1); } -.trainer-item .trainer-pic:hover::before { opacity: 0; } /* Official YOURFIT lock-ups are wide (≈4.5:1) PNGs. In the square icon box they were squeezed to 44px wide while keeping their height attribute, so they came @@ -3888,3 +4593,25 @@ h3.contact-title { .icon-box-item.is-centered .ib-icon::after, .price-card.is-centered::before, .price-card.is-centered::after { animation: none; } } + +/* ── Links: never underlined; hover = colour shift + glow ───────────────────────── + Bootstrap underlines every . Links with their own hover colour keep it (their + selectors are more specific than the :where() below) and still get the glow. + --link-hover is the readable brand shade for the background it sits on: the + dark olive on light sections, the bright brand colour on the dark ones. */ +:root { --link-hover: var(--fitme-accent-ink); } +.hero-section, .trainers-section, .pricing-section, .contact-section, .footer-section, +.map-section, .event-preview-bar { --link-hover: var(--fitme-accent); } + +a { text-decoration: none; } +a:where(:not(.site-btn, .gym-switch-btn, .gal-link, .skip-link, .splide__arrow, .nav-link, .navbar-brand, .td-close)) { + transition: color .3s ease, text-shadow .3s ease; +} +a:where(:not(.site-btn, .gym-switch-btn, .gal-link, .skip-link, .splide__arrow, .nav-link, .navbar-brand, .td-close)):is(:hover, :focus-visible) { + color: var(--link-hover); + text-decoration: none; + text-shadow: 0 0 10px rgba(var(--fitme-accent-rgb), .6), 0 0 22px rgba(var(--fitme-accent-rgb), .3); +} +@media (prefers-reduced-motion: reduce) { + a { transition: none !important; } +} diff --git a/site/assets/js/main.js b/site/assets/js/main.js index 060b724..2867125 100644 --- a/site/assets/js/main.js +++ b/site/assets/js/main.js @@ -787,3 +787,79 @@ document.addEventListener('DOMContentLoaded', function () { } requestAnimationFrame(tick); }); + +/* ── Galeria: auto-scroll + click-and-drag ─────────────────────────────────── + The strip (.gal-wrap) is four identical copies long. It advances by itself, + follows a mouse drag (touch uses the native swipe), pauses while you hover or + hold it and resumes a moment after, and whenever the position leaves the two + middle copies it jumps by exactly one copy — invisible, since every copy is the + same. A drag longer than a few px swallows the click, so it doesn't open the + lightbox. */ +document.addEventListener('DOMContentLoaded', function () { + var reduce = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches; + document.querySelectorAll('.gal-marquee').forEach(function (strip) { + var wrap = strip.parentElement; + var set = strip.querySelector('.gal-set'); + if (!wrap || !set) return; + var loop = 0, pos = null, hold = false, resumeAt = 0, dragging = false, moved = 0, startX = 0, startLeft = 0, last = 0; + var SPEED = 40; // px per second + + function measure() { loop = set.offsetWidth; } + function normalise() { + if (!loop) return; + if (wrap.scrollLeft >= loop * 2) wrap.scrollLeft -= loop; + else if (wrap.scrollLeft < loop) wrap.scrollLeft += loop; + } + function start() { measure(); wrap.scrollLeft = loop; } + start(); + window.addEventListener('load', start); + window.addEventListener('resize', measure, { passive: true }); + wrap.addEventListener('scroll', normalise, { passive: true }); + + wrap.addEventListener('mouseenter', function () { hold = true; }); + wrap.addEventListener('mouseleave', function () { if (!dragging) { hold = false; resumeAt = performance.now() + 600; } }); + wrap.addEventListener('focusin', function () { hold = true; }); + wrap.addEventListener('focusout', function () { hold = false; }); + wrap.addEventListener('touchstart', function () { hold = true; }, { passive: true }); + wrap.addEventListener('touchend', function () { hold = false; resumeAt = performance.now() + 1800; }, { passive: true }); + + wrap.addEventListener('pointerdown', function (e) { + if (e.pointerType === 'touch' || e.button !== 0) return; + dragging = true; moved = 0; + startX = e.clientX; startLeft = wrap.scrollLeft; + wrap.classList.add('is-dragging'); + wrap.setPointerCapture(e.pointerId); + }); + wrap.addEventListener('pointermove', function (e) { + if (!dragging) return; + var dx = e.clientX - startX; + moved = Math.max(moved, Math.abs(dx)); + wrap.scrollLeft = startLeft - dx; + }); + function endDrag(e) { + if (!dragging) return; + dragging = false; + wrap.classList.remove('is-dragging'); + try { wrap.releasePointerCapture(e.pointerId); } catch (err) {} + resumeAt = performance.now() + 1200; + } + wrap.addEventListener('pointerup', endDrag); + wrap.addEventListener('pointercancel', endDrag); + // After a real drag, the click that follows must not open the lightbox. + wrap.addEventListener('click', function (e) { + if (moved > 5) { e.preventDefault(); e.stopPropagation(); moved = 0; } + }, true); + + function tick(now) { + requestAnimationFrame(tick); + var dt = last ? Math.min(now - last, 100) : 16; last = now; + if (reduce || document.hidden || dragging || hold || now < resumeAt) { pos = null; return; } + // scrollLeft only keeps whole pixels, so a ~0.6px/frame step would round + // away: carry the fraction in our own float and write it back. + if (pos === null || Math.abs(wrap.scrollLeft - pos) > 2) pos = wrap.scrollLeft; + pos += SPEED * dt / 1000; + wrap.scrollLeft = pos; + } + requestAnimationFrame(tick); + }); +}); diff --git a/site/includes/gallery.php b/site/includes/gallery.php index bfbcae0..54f2c53 100644 --- a/site/includes/gallery.php +++ b/site/includes/gallery.php @@ -1,11 +1,11 @@ lightbox with prev / -// next, arrow keys, Esc and swipe (main.js); the repeated copies are hidden -// from assistive tech and open the same photo. +// Gallery partial — an endless horizontal strip (home "instalações" and the +// CrossFit page): full-height photos alternate with 2x2 grids of four. main.js +// scrolls it by itself, lets you click-and-drag (mouse) or swipe (touch), pauses +// while you interact and loops seamlessly over the repeated copies. Clicking a +// tile (without dragging) opens it full size in a lightbox with prev / +// next, arrow keys, Esc and swipe; the repeated copies are hidden from assistive +// tech and open the same photo. $files = glob(rtrim($gallery_path, '/') . '/*.jpg'); if (!$files || !is_array($files)) $files = []; // Column span per tile, cycled: w = two columns, s = one. @@ -31,13 +31,12 @@ $gal_label = $gallery_h2 ?? 'Instalações'; $units[] = ['grid' => $grid, 'idx' => range($i, $i + $take - 1)]; $i += $take; $k++; } - // The strip is rendered several times so the endless scroll has no gap: two - // copies is enough for a normal gallery, a short one needs four. - $copies = $n >= 8 ? 2 : 4; - $dur = max(30, $n * 5); // seconds for one pass over the originals + // The strip is rendered four times: main.js keeps the scroll position inside + // the middle copies and jumps by exactly one copy, so it loops without a seam. + $copies = 4; ?>