feat(site): auto-scrolling gallery strip (full-height + 2x2), centre-of-screen hover state, trial link as text under the hero button

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Sonnet 5.5 committed 2026-10-13 18:44:31 +01:00
1 parent 99c28f04b4
commit 6c14e644b1
4 files changed
+199 -915

No files matched your search

+117 -800
View File
@@ -670,12 +670,31 @@ section[id] {
.hero-cta-row { .hero-cta-row {
display: flex; display: flex;
flex-direction: column; /* the button, then the trial link as plain text under it */
align-items: center;
gap: 14px; gap: 14px;
justify-content: center;
flex-wrap: wrap;
margin-top: 2.5rem; 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 { .hero-cta-row .site-btn {
margin-top: 0; margin-top: 0;
} }
@@ -3544,782 +3563,50 @@ h3.contact-title {
.trainer-item.is-open { z-index: 31; } .trainer-item.is-open { z-index: 31; }
/* ── Open detail cards (team + Modalidades): the same spinning brand edge ────── /* ── Galeria — endless horizontal strip + lightbox (site/includes/gallery.php) ──
A 3px ring cut out of ::after with a mask, shown while the card is open, plus Full-height photos alternate with 2x2 grids of four. The strip is rendered
a soft brand glow added to the existing lift shadow. ─────────────────────── */ --gal-copies times and slides by exactly one copy, so the loop is seamless;
.mod-detail::after, it pauses on hover / focus. With "reduce motion" it is a plain swipeable row. */
.trainer-detail::after { .gal-wrap {
content: ''; --gal-h: 420px;
position: absolute; --gal-gap: 12px;
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%;
overflow: hidden; 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; 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; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template: 1fr 1fr / 1fr 1fr;
grid-auto-rows: 170px; gap: var(--gal-gap);
grid-auto-flow: dense;
gap: 12px;
} }
.gal-tile { position: relative; margin: 0; min-width: 0; min-height: 0; height: 100%; }
.gal-tile { position: relative; margin: 0; } @media (max-width: 991px) { .gal-wrap { --gal-h: 340px; } }
.gal-w { grid-column: span 2; } @media (max-width: 575px) { .gal-wrap { --gal-h: 260px; --gal-gap: 8px; margin-bottom: 48px; } }
@media (prefers-reduced-motion: reduce) {
/* JS masonry: absolute tiles, sized and placed by main.js. */ .gal-wrap { overflow-x: auto; -webkit-mask-image: none; mask-image: none; scrollbar-width: thin; }
.gal-masonry.is-masonry { .gal-track { animation: none; }
display: block; .gal-set[aria-hidden="true"] { display: none; }
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);
} }
.gal-link { .gal-link {
@@ -4393,27 +3680,12 @@ h3.contact-title {
.gal-link:hover::after { animation: none; } .gal-link:hover::after { animation: none; }
} }
/* ── Photos at rest: 10% desaturated + a discreet brand-colour tint (gallery: a /* ── Photos at rest: 10% desaturated (team photos also get a light brand-colour
gradient fading to transparent; team: a light multiply layer), both lifted on tint), both lifted on hover. ──────────────────────────────────────────── */
hover. The tint follows each gym's brand colour. ───────────────────────── */
.gal-link img { filter: saturate(.9); } .gal-link img { filter: saturate(.9); }
.gal-link:hover img, .gal-link:hover img,
.gal-link:focus-visible img { filter: saturate(1) brightness(1.05); } .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 img { filter: saturate(.9); transition: filter .45s ease; }
.trainer-item .trainer-pic::before { .trainer-item .trainer-pic::before {
@@ -4460,14 +3732,6 @@ h3.contact-title {
.md-card .ib-icon img[src*="yourfit"] { width: 106px; } .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 */ /* Lightbox */
.gal-lightbox { .gal-lightbox {
width: 100vw; width: 100vw;
@@ -4568,6 +3832,59 @@ h3.contact-title {
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.gal-link img, .gal-lb-figure img { transition: none; animation: none; } .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; }
} }
+32 -90
View File
@@ -710,10 +710,11 @@ document.addEventListener('DOMContentLoaded', function () {
opens the <dialog> instead: prev / next buttons, ← → keys, Esc, swipe on opens the <dialog> instead: prev / next buttons, ← → keys, Esc, swipe on
touch, and a click on the dark backdrop closes it. */ touch, and a click on the dark backdrop closes it. */
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.gal-masonry').forEach(function (grid) { document.querySelectorAll('.gal-marquee').forEach(function (grid) {
var box = grid.parentElement.nextElementSibling; var box = grid.parentElement.nextElementSibling;
if (!box || !box.classList.contains('gal-lightbox') || typeof box.showModal !== 'function') return; 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 img = box.querySelector('.gal-lb-figure img');
var count = box.querySelector('.gal-lb-count'); var count = box.querySelector('.gal-lb-count');
var current = 0; var current = 0;
@@ -728,10 +729,10 @@ document.addEventListener('DOMContentLoaded', function () {
count.textContent = (current + 1) + ' / ' + links.length; count.textContent = (current + 1) + ' / ' + links.length;
} }
links.forEach(function (a, i) { all.forEach(function (a) {
a.addEventListener('click', function (e) { a.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
show(i); show(+a.getAttribute('data-index') || 0);
box.showModal(); box.showModal();
}); });
}); });
@@ -744,7 +745,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (e.key === 'ArrowLeft') show(current - 1); if (e.key === 'ArrowLeft') show(current - 1);
else if (e.key === 'ArrowRight') 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; var x0 = null;
box.addEventListener('touchstart', function (e) { x0 = e.touches[0].clientX; }, { passive: true }); box.addEventListener('touchstart', function (e) { x0 = e.touches[0].clientX; }, { passive: true });
@@ -757,91 +758,32 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
}); });
/* ── Galeria: masonry layout ───────────────────────────────────────────────── /* ── Scroll "hover": what sits in the middle of the screen looks hovered ───────
A small hand-rolled masonry (no library): the grid is split into columns Photos, cards and icons get .is-centered while their box crosses the middle
(4 / 3 / 2 by width), and each tile — at its photo's true aspect ratio, from of the viewport (±12% of its height); inside a sliding strip (gallery,
the width/height attributes the PHP writes — is dropped into the shortest Modalidades) they must also cross the horizontal middle. Throttled, paused
column; tiles marked .gal-w take two adjacent columns (the pair whose taller while the tab is hidden, skipped with "reduce motion". The CSS mirrors the
one is lowest), which is what staggers the rows. Tiles are absolutely :hover rules (style.css, "In the middle of the screen"). */
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. */
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.gal-masonry').forEach(function (grid) { if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var tiles = Array.prototype.slice.call(grid.querySelectorAll('.gal-tile')); var items = Array.prototype.slice.call(document.querySelectorAll(
if (!tiles.length) return; '.gal-tile, .trainer-item .trainer-pic, .icon-box-item, .price-card')).map(function (el) {
grid.classList.add('is-masonry'); return { el: el, strip: !!el.closest('.gal-marquee, .mod-scroll'), on: false };
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;
});
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();
}
}); });
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); }
});
}
requestAnimationFrame(tick);
}); });
+49 -24
View File
@@ -1,11 +1,11 @@
<?php <?php
// Gallery partial — masonry (home "instalações" and the CrossFit page). // Gallery partial — an endless horizontal strip that scrolls by itself (home
// main.js lays the tiles out itself: each photo keeps its own aspect ratio (no // "instalações" and the CrossFit page). Pure CSS: full-height photos alternate
// cropping) and drops into the shortest column; every few tiles one spans two // with 2x2 grids of four; the strip is repeated so the loop is seamless and it
// columns, which is what staggers the heights — the photos are all landscape, // pauses on hover / focus. With "reduce motion" it becomes a plain swipeable
// so equal-width tiles alone would just form a grid. Without JS the list falls // row. Clicking a tile opens it full size in a <dialog> lightbox with prev /
// back to a plain CSS grid. Clicking a tile opens it full size in a <dialog> // next, arrow keys, Esc and swipe (main.js); the repeated copies are hidden
// lightbox with prev / next, arrow keys, Esc and swipe (main.js). // from assistive tech and open the same photo.
$files = glob(rtrim($gallery_path, '/') . '/*.jpg'); $files = glob(rtrim($gallery_path, '/') . '/*.jpg');
if (!$files || !is_array($files)) $files = []; if (!$files || !is_array($files)) $files = [];
// Column span per tile, cycled: w = two columns, s = one. // Column span per tile, cycled: w = two columns, s = one.
@@ -21,23 +21,48 @@ $gal_label = $gallery_h2 ?? 'Instalações';
</div> </div>
</div> </div>
</div> </div>
<?php if ($files): ?> <?php if ($files):
<div class="container"> // Units of the strip: a full-height photo, then a 2x2 grid of four, and so on.
<ul class="gal-masonry"> // (Fewer than four left over → a full-height photo.)
<?php foreach ($files as $gi => $f): $units = []; $i = 0; $n = count($files); $k = 0;
$src = base_url($f); while ($i < $n) {
$alt = $gal_label . ' — FitMe 3070, foto ' . ($gi + 1); // distinct alt per photo $grid = ($k % 2 === 1) && ($n - $i >= 4);
$dim = @getimagesize($f) ?: [0, 0]; $take = $grid ? 4 : 1;
?> $units[] = ['grid' => $grid, 'idx' => range($i, $i + $take - 1)];
<li class="gal-tile gal-<?= $gal_shapes[$gi % count($gal_shapes)] ?>"> $i += $take; $k++;
<a href="<?= htmlspecialchars($src) ?>" class="gal-link" data-index="<?= $gi ?>" aria-label="Ampliar: <?= htmlspecialchars($alt) ?>"> }
<img src="<?= htmlspecialchars($src) ?>" alt="<?= htmlspecialchars($alt) ?>" // The strip is rendered several times so the endless scroll has no gap: two
<?= $dim[0] ? 'width="' . (int) $dim[0] . '" height="' . (int) $dim[1] . '"' : '' ?> // copies is enough for a normal gallery, a short one needs four.
<?= $gi > 1 ? 'loading="lazy"' : '' ?> decoding="async"> $copies = $n >= 8 ? 2 : 4;
</a> $dur = max(30, $n * 5); // seconds for one pass over the originals
</li> ?>
<?php endforeach; ?> <div class="gal-wrap">
</ul> <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];
?>
<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 > 2 || $dup ? 'loading="lazy"' : '' ?> decoding="async">
</a>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php endfor; ?>
</div>
</div>
</div> </div>
<dialog class="gal-lightbox" aria-label="<?= htmlspecialchars($gal_label) ?>"> <dialog class="gal-lightbox" aria-label="<?= htmlspecialchars($gal_label) ?>">
<button type="button" class="gal-lb-close" aria-label="Fechar">&times;</button> <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> <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"> <div class="hero-cta-row">
<a href="?assunto=insc#contactos" class="site-btn">Inscreve-te Já!</a> <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> </div>
</div> </div>