release: promote beta to main (gyms, brand colours, JPEG XL, SEO per gym) #3

Merged
andre merged 74 commits from beta into main 2026-10-26 19:47:48 +00:00
4 changed files with 191 additions and 907 deletions
Showing only changes of commit 6c14e644b1 - Show all commits

No files matched your search

+117 -800
View File
@@ -670,12 +670,31 @@ section[id] {
.hero-cta-row {
display: flex;
flex-direction: column; /* the button, then the trial link as plain text under it */
align-items: center;
gap: 14px;
justify-content: center;
flex-wrap: wrap;
margin-top: 2.5rem;
}
/* "Marca uma aula experimental": text only, under the main button. */
.hero-text-link {
background: none;
border: 0;
padding: 4px 8px;
color: #fff;
font: inherit;
font-weight: 600;
text-shadow: 0 1px 6px rgba(0, 0, 0, .6);
cursor: pointer;
transition: color .3s ease;
}
.hero-text-link:hover,
.hero-text-link:focus-visible {
color: var(--fitme-accent);
text-decoration: underline;
text-underline-offset: 4px;
}
.hero-cta-row .site-btn {
margin-top: 0;
}
@@ -3544,782 +3563,50 @@ 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 <angle>, 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: '<angle>';
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%;
/* ── 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. */
.gal-wrap {
--gal-h: 420px;
--gal-gap: 12px;
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: underline;
text-underline-offset: 3px;
}
.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 <svg class="mi" data-anim="…"> (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 — masonry + lightbox (site/includes/gallery.php)
Without JS: a plain CSS grid (photos cropped to equal tiles). With JS
(.is-masonry): main.js positions every tile absolutely — shortest column
first, true aspect ratio, some tiles two columns wide — and sets the list's
height. 4 columns on desktop, 3 on tablets, 2 on phones.
══════════════════════════════════════════════════════════════════════════ */
.gal-masonry {
list-style: none;
margin: 0 0 70px;
padding: 0;
-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-set { display: flex; flex: 0 0 auto; }
.gal-unit {
flex: 0 0 auto;
height: var(--gal-h);
margin-right: var(--gal-gap);
}
.gal-unit--full { width: calc(var(--gal-h) * .82); }
.gal-unit--grid {
width: calc(var(--gal-h) * 1.05);
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-auto-rows: 170px;
grid-auto-flow: dense;
gap: 12px;
grid-template: 1fr 1fr / 1fr 1fr;
gap: var(--gal-gap);
}
.gal-tile { position: relative; margin: 0; min-width: 0; min-height: 0; height: 100%; }
.gal-tile { position: relative; margin: 0; }
.gal-w { grid-column: span 2; }
/* JS masonry: absolute tiles, sized and placed by main.js. */
.gal-masonry.is-masonry {
display: block;
position: relative;
}
.gal-masonry.is-masonry .gal-tile {
position: absolute;
top: 0;
left: 0;
transition: transform .5s cubic-bezier(.2, .8, .2, 1), width .5s cubic-bezier(.2, .8, .2, 1), height .5s cubic-bezier(.2, .8, .2, 1), opacity .6s ease;
}
/* Entrance: tiles rise in one after another when the gallery comes into view
(main.js adds .is-in with a per-tile delay). */
.gal-masonry.is-masonry .gal-tile:not(.is-in) {
opacity: 0;
}
.gal-masonry.is-masonry .gal-tile .gal-link {
transition: transform .6s cubic-bezier(.2, .8, .2, 1);
}
.gal-masonry.is-masonry .gal-tile:not(.is-in) .gal-link {
transform: translateY(24px) scale(.97);
@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 {
@@ -4393,27 +3680,12 @@ h3.contact-title {
.gal-link:hover::after { animation: none; }
}
/* ── 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. ───────────────────────── */
/* ── Photos at rest: 10% desaturated (team photos also get a light brand-colour
tint), both lifted on hover. ──────────────────────────────────────────── */
.gal-link img { filter: saturate(.9); }
.gal-link:hover img,
.gal-link:focus-visible img { filter: saturate(1) brightness(1.05); }
/* Gallery tint: a brand-colour gradient that fades from the bottom edge to fully
transparent, laid over the tile (the link's own pseudo-elements are taken by
the hover edge); it dissolves on hover. */
.gal-tile::after {
content: '';
position: absolute;
inset: 0;
border-radius: var(--fitme-radius);
background: linear-gradient(to top, rgba(var(--fitme-accent-rgb), .28) 0%, rgba(var(--fitme-accent-rgb), 0) 80%);
pointer-events: none;
transition: opacity .45s ease;
}
.gal-tile:hover::after,
.gal-tile:focus-within::after { opacity: 0; }
.trainer-item .trainer-pic img { filter: saturate(.9); transition: filter .45s ease; }
.trainer-item .trainer-pic::before {
@@ -4460,14 +3732,6 @@ h3.contact-title {
.md-card .ib-icon img[src*="yourfit"] { width: 106px; }
}
@media (max-width: 991px) {
.gal-masonry { grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-rows: 150px; }
}
@media (max-width: 575px) {
.gal-masonry { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-auto-rows: 120px; gap: 8px; margin-bottom: 48px; }
}
/* Lightbox */
.gal-lightbox {
width: 100vw;
@@ -4568,6 +3832,59 @@ h3.contact-title {
@media (prefers-reduced-motion: reduce) {
.gal-link img, .gal-lb-figure img { transition: none; animation: none; }
.gal-masonry.is-masonry .gal-tile,
.gal-masonry.is-masonry .gal-tile .gal-link { transition: none; }
}
/* ── "In the middle of the screen" = hovered ─────────────────────────────────────
main.js adds .is-centered to the photo / card / icon that sits on the vertical
middle of the viewport (and, inside the sliding strips, on the horizontal
middle too), so scrolling past shows the same state as a hover. These mirror
the :hover rules above. ─────────────────────────────────────────────────── */
.gal-tile.is-centered .gal-link img { transform: scale(1.06); filter: saturate(1) brightness(1.05); }
.gal-tile.is-centered .gal-link::before { opacity: 1; }
.gal-tile.is-centered .gal-link::after { opacity: 1; animation: pc-spin 3.2s linear infinite; }
.trainer-item .trainer-pic.is-centered,
#powerRotation>div:nth-child(even) .trainer-pic.is-centered {
transform: rotate(0deg) translateY(-8px) scale(1.05);
border-color: transparent;
background:
linear-gradient(var(--fitme-dark), var(--fitme-dark)) padding-box,
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) border-box;
box-shadow: 0 0 18px 2px rgba(var(--fitme-accent-rgb), .4), var(--fitme-shadow-lg);
animation: pc-spin 3.2s linear infinite;
}
.trainer-item .trainer-pic.is-centered img { filter: saturate(1); }
.trainer-item .trainer-pic.is-centered::before { opacity: 0; }
.icon-box-item.is-centered { transform: translateY(-3px); }
.icon-box-item.is-centered .ib-icon {
background: color-mix(in srgb, var(--fitme-accent) 10%, #000);
color: #fff;
transform: scale(1.06);
box-shadow: 0 0 16px 1px rgba(var(--fitme-accent-rgb), .4);
}
.icon-box-item.is-centered .ib-icon::after { opacity: 1; animation: pc-spin 3.2s linear infinite; }
.icon-box-item.is-centered .ib-icon svg.mi { color: #fff; }
.icon-box-item.is-centered .ib-icon img { filter: brightness(0) invert(1); mix-blend-mode: normal; }
.icon-box-item.is-centered .ib-icon:has(img[src*="yourfit"]) { background: #fff; }
.icon-box-item.is-centered .ib-icon img[src*="yourfit"] { filter: none; }
.price-card.is-centered { --pc-lift: -4px; --pc-bw: 2px; box-shadow: var(--fitme-shadow-lg); }
.price-card.is-centered::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.is-centered::after { opacity: .75; animation: pc-spin 3.2s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.gal-tile.is-centered .gal-link::after,
.trainer-item .trainer-pic.is-centered,
.icon-box-item.is-centered .ib-icon::after,
.price-card.is-centered::before, .price-card.is-centered::after { animation: none; }
}
+31 -89
View File
@@ -710,10 +710,11 @@ document.addEventListener('DOMContentLoaded', function () {
opens the <dialog> instead: prev / next buttons, ← → keys, Esc, swipe on
touch, and a click on the dark backdrop closes it. */
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.gal-masonry').forEach(function (grid) {
document.querySelectorAll('.gal-marquee').forEach(function (grid) {
var box = grid.parentElement.nextElementSibling;
if (!box || !box.classList.contains('gal-lightbox') || typeof box.showModal !== 'function') return;
var links = Array.prototype.slice.call(grid.querySelectorAll('.gal-link'));
var all = Array.prototype.slice.call(grid.querySelectorAll('.gal-link'));
var links = all.filter(function (a) { return !a.hasAttribute('data-dup'); }); // the repeated copies open the same photo
var img = box.querySelector('.gal-lb-figure img');
var count = box.querySelector('.gal-lb-count');
var current = 0;
@@ -728,10 +729,10 @@ document.addEventListener('DOMContentLoaded', function () {
count.textContent = (current + 1) + ' / ' + links.length;
}
links.forEach(function (a, i) {
all.forEach(function (a) {
a.addEventListener('click', function (e) {
e.preventDefault();
show(i);
show(+a.getAttribute('data-index') || 0);
box.showModal();
});
});
@@ -744,7 +745,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (e.key === 'ArrowLeft') show(current - 1);
else if (e.key === 'ArrowRight') show(current + 1);
});
box.addEventListener('close', function () { links[current].focus(); });
box.addEventListener('close', function () { if (links[current]) links[current].focus(); });
var x0 = null;
box.addEventListener('touchstart', function (e) { x0 = e.touches[0].clientX; }, { passive: true });
@@ -757,91 +758,32 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
/* ── Galeria: masonry layout ─────────────────────────────────────────────────
A small hand-rolled masonry (no library): the grid is split into columns
(4 / 3 / 2 by width), and each tile — at its photo's true aspect ratio, from
the width/height attributes the PHP writes — is dropped into the shortest
column; tiles marked .gal-w take two adjacent columns (the pair whose taller
one is lowest), which is what staggers the rows. Tiles are absolutely
positioned with transforms, so a relayout on resize glides into place.
The first time the gallery scrolls into view the tiles rise in one by one. */
/* ── Scroll "hover": what sits in the middle of the screen looks hovered ───────
Photos, cards and icons get .is-centered while their box crosses the middle
of the viewport (±12% of its height); inside a sliding strip (gallery,
Modalidades) they must also cross the horizontal middle. Throttled, paused
while the tab is hidden, skipped with "reduce motion". The CSS mirrors the
:hover rules (style.css, "In the middle of the screen"). */
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.gal-masonry').forEach(function (grid) {
var tiles = Array.prototype.slice.call(grid.querySelectorAll('.gal-tile'));
if (!tiles.length) return;
grid.classList.add('is-masonry');
function ratio(tile) {
var img = tile.querySelector('img');
var w = +img.getAttribute('width') || img.naturalWidth || 4;
var h = +img.getAttribute('height') || img.naturalHeight || 3;
return h / w;
}
function layout() {
var width = grid.clientWidth;
var cols = width >= 900 ? 4 : width >= 520 ? 3 : 2;
var gap = width >= 520 ? 12 : 8;
var colW = (width - gap * (cols - 1)) / cols;
var heights = [];
for (var c = 0; c < cols; c++) heights.push(0);
tiles.forEach(function (tile) {
var span = (tile.classList.contains('gal-w') && cols > 1) ? 2 : 1;
// Best column (or pair of columns) = lowest top edge.
var pick = function (span) {
var best = 0, bestTop = Infinity, waste = 0;
for (var i = 0; i <= cols - span; i++) {
var cells = heights.slice(i, i + span);
var top = Math.max.apply(null, cells);
if (top < bestTop - 0.5) { bestTop = top; best = i; waste = top - Math.min.apply(null, cells); }
}
return [best, bestTop, waste];
};
var p = pick(span);
// A wide tile whose two columns end at different heights would
// leave a hole under the shorter one — use a single column then.
if (span === 2 && p[2] > 60) { span = 1; p = pick(1); }
var best = p[0], bestTop = p[1];
var w = colW * span + gap * (span - 1);
var h = Math.round(w * ratio(tile));
tile.style.width = w + 'px';
tile.style.height = h + 'px';
tile.style.transform = 'translate(' + (best * (colW + gap)) + 'px,' + bestTop + 'px)';
for (var j = best; j < best + span; j++) heights[j] = bestTop + h + gap;
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var items = Array.prototype.slice.call(document.querySelectorAll(
'.gal-tile, .trainer-item .trainer-pic, .icon-box-item, .price-card')).map(function (el) {
return { el: el, strip: !!el.closest('.gal-marquee, .mod-scroll'), on: false };
});
if (!items.length) return;
var last = 0;
function tick(now) {
requestAnimationFrame(tick);
if (document.hidden || now - last < 120) return;
last = now;
var vw = window.innerWidth, vh = window.innerHeight, cy = vh / 2, band = vh * 0.12;
items.forEach(function (it) {
var r = it.el.getBoundingClientRect();
var on = r.bottom > 0 && r.top < vh &&
r.top <= cy + band && r.bottom >= cy - band &&
(!it.strip || (r.left <= vw / 2 && r.right >= vw / 2));
if (on !== it.on) { it.on = on; it.el.classList.toggle('is-centered', on); }
});
grid.style.height = Math.max(0, Math.max.apply(null, heights) - gap) + 'px';
}
layout();
// Re-lay out whenever the grid's own width changes (window resize, but
// also a gallery that was laid out while hidden / zero-width).
var t, lastW = grid.clientWidth;
var relayout = function () {
if (grid.clientWidth === lastW) return;
lastW = grid.clientWidth;
clearTimeout(t); t = setTimeout(layout, 80);
};
if ('ResizeObserver' in window) new ResizeObserver(relayout).observe(grid);
window.addEventListener('resize', relayout, { passive: true }); // belt and braces; relayout() is idempotent
// Re-run once images report their size, in case an attribute was missing.
tiles.forEach(function (tile) {
var img = tile.querySelector('img');
if (!img.getAttribute('width')) img.addEventListener('load', layout, { once: true });
});
// Staggered entrance.
var enter = function () {
tiles.forEach(function (tile, i) {
setTimeout(function () { tile.classList.add('is-in'); }, Math.min(i, 12) * 70);
});
};
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
if (entries.some(function (e) { return e.isIntersecting; })) { enter(); io.disconnect(); }
}, { rootMargin: '0px 0px -80px 0px' });
io.observe(grid);
} else {
enter();
}
});
requestAnimationFrame(tick);
});
+42 -17
View File
@@ -1,11 +1,11 @@
<?php
// Gallery partial — masonry (home "instalações" and the CrossFit page).
// main.js lays the tiles out itself: each photo keeps its own aspect ratio (no
// cropping) and drops into the shortest column; every few tiles one spans two
// columns, which is what staggers the heights — the photos are all landscape,
// so equal-width tiles alone would just form a grid. Without JS the list falls
// back to a plain CSS grid. Clicking a tile opens it full size in a <dialog>
// lightbox with prev / next, arrow keys, Esc and swipe (main.js).
// Gallery partial — an endless horizontal strip that scrolls by itself (home
// "instalações" and the CrossFit page). Pure CSS: full-height photos alternate
// with 2x2 grids of four; the strip is repeated so the loop is seamless and it
// pauses on hover / focus. With "reduce motion" it becomes a plain swipeable
// row. Clicking a tile opens it full size in a <dialog> lightbox with prev /
// next, arrow keys, Esc and swipe (main.js); 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.
@@ -21,23 +21,48 @@ $gal_label = $gallery_h2 ?? 'Instalações';
</div>
</div>
</div>
<?php if ($files): ?>
<div class="container">
<ul class="gal-masonry">
<?php foreach ($files as $gi => $f):
<?php if ($files):
// Units of the strip: a full-height photo, then a 2x2 grid of four, and so on.
// (Fewer than four left over → a full-height photo.)
$units = []; $i = 0; $n = count($files); $k = 0;
while ($i < $n) {
$grid = ($k % 2 === 1) && ($n - $i >= 4);
$take = $grid ? 4 : 1;
$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
?>
<div class="gal-wrap">
<div class="gal-marquee" style="--gal-copies: <?= $copies ?>; --gal-dur: <?= $dur * $copies ?>s">
<div class="gal-track">
<?php for ($c = 0; $c < $copies; $c++): $dup = $c > 0; ?>
<div class="gal-set"<?= $dup ? ' aria-hidden="true"' : '' ?>>
<?php foreach ($units as $u): ?>
<div class="gal-unit <?= $u['grid'] ? 'gal-unit--grid' : 'gal-unit--full' ?>">
<?php foreach ($u['idx'] as $gi):
$f = $files[$gi];
$src = base_url($f);
$alt = $gal_label . ' — FitMe 3070, foto ' . ($gi + 1); // distinct alt per photo
$dim = @getimagesize($f) ?: [0, 0];
?>
<li class="gal-tile gal-<?= $gal_shapes[$gi % count($gal_shapes)] ?>">
<a href="<?= htmlspecialchars($src) ?>" class="gal-link" data-index="<?= $gi ?>" aria-label="Ampliar: <?= htmlspecialchars($alt) ?>">
<img src="<?= htmlspecialchars($src) ?>" alt="<?= htmlspecialchars($alt) ?>"
<div class="gal-tile">
<a href="<?= htmlspecialchars($src) ?>" class="gal-link" data-index="<?= $gi ?>"<?= $dup ? ' data-dup="1" tabindex="-1"' : '' ?> aria-label="Ampliar: <?= htmlspecialchars($alt) ?>">
<img src="<?= htmlspecialchars($src) ?>" alt="<?= $dup ? '' : htmlspecialchars($alt) ?>"
<?= $dim[0] ? 'width="' . (int) $dim[0] . '" height="' . (int) $dim[1] . '"' : '' ?>
<?= $gi > 1 ? 'loading="lazy"' : '' ?> decoding="async">
<?= $gi > 2 || $dup ? 'loading="lazy"' : '' ?> decoding="async">
</a>
</li>
</div>
<?php endforeach; ?>
</ul>
</div>
<?php endforeach; ?>
</div>
<?php endfor; ?>
</div>
</div>
</div>
<dialog class="gal-lightbox" aria-label="<?= htmlspecialchars($gal_label) ?>">
<button type="button" class="gal-lb-close" aria-label="Fechar">&times;</button>
+1 -1
View File
@@ -77,7 +77,7 @@ $hero_img = $gym['hero_image'] !== '' ? $gym['hero_image'] : 'site/assets/img
<h1><?= htmlspecialchars($gym['hero_title'] !== '' ? $gym['hero_title'] : $gym['name']) ?><?php if ($gym['hero_subtitle'] !== ''): ?><br><span><?= htmlspecialchars($gym['hero_subtitle']) ?></span><?php endif; ?></h1>
<div class="hero-cta-row">
<a href="?assunto=insc#contactos" class="site-btn">Inscreve-te Já!</a>
<button type="button" class="site-btn sb-ghost" data-bs-toggle="modal" data-bs-target="#trialModal">Marca uma Aula Experimental</button>
<button type="button" class="hero-text-link" data-bs-toggle="modal" data-bs-target="#trialModal">Marca uma aula experimental</button>
</div>
</div>
</div>