feat(modalidades): 6 ícones em falta, ícones animados, ícone centrado no cartão aberto

- Ícones novos (Bootcamp, YourFit Core, Run3070, Total Workout, On Ramp, Jump)
  do mesmo conjunto; só o LPO continua sem ícone no ficheiro de design.
- Os ícones fitme-*.svg passam a ir embutidos (modalidade_icon_html): cada
  traço tem pathLength=1 e desenha-se ao aparecer, no hover e ao abrir o
  cartão; cada família tem um movimento (levantar, soco, respirar, saltar,
  chama, pedalar, correr). Outros ícones continuam <img>.
- No cartão de detalhe (desktop) o ícone e o nome deslizam até ao centro
  vertical da coluna.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andreandClaude Opus 5.5 committed 2026-09-24 22:02:58 +01:00
1 parent 42f5919915
commit cb0e4540f2
10 files changed
+186 -8

No files matched your search

+127
View File
@@ -3968,3 +3968,130 @@ h3.contact-title {
color: var(--fitme-ash);
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; }
}
+12 -6
View File
@@ -220,12 +220,12 @@ document.addEventListener('DOMContentLoaded', function () {
function panelOpen() { return panel.classList.contains('is-open'); }
function fill(card) {
var img = card.querySelector('.ib-icon img');
var name = card.querySelector('h3').textContent;
var tpl = card.querySelector('template.mod-desc');
var pImg = panel.querySelector('.md-card img');
pImg.hidden = !img;
pImg.src = img ? img.src : '';
// Same icon markup as the card (inline animated SVG or <img>), so the
// panel's copy replays its animation as it opens.
var icon = card.querySelector('.ib-icon');
panel.querySelector('.md-card .ib-icon').innerHTML = icon ? icon.innerHTML : '';
panel.querySelector('.md-card h3').textContent = name;
panel.querySelector('.md-name').textContent = name;
panel.querySelector('.md-text').innerHTML = tpl ? tpl.innerHTML : '';
@@ -250,6 +250,11 @@ document.addEventListener('DOMContentLoaded', function () {
panel.style.setProperty('--md-y', (cr.top - sr.top) + 'px');
panel.style.setProperty('--md-cx', Math.max(0, w - cr.width) + 'px');
panel.style.setProperty('--md-cy', Math.max(0, panel.offsetHeight - cr.height) + 'px');
// Open, the icon + name slide from where they sit on the card to the
// vertical centre of the (taller) panel column.
var title = card.querySelector('h3');
var contentH = icon && title ? (title.offsetTop + title.offsetHeight) - icon.offsetTop : 0;
panel.style.setProperty('--md-pad-open', Math.max(0, (panel.offsetHeight - contentH) / 2) + 'px');
}
function flyFrom(card, opening) {
@@ -280,10 +285,11 @@ document.addEventListener('DOMContentLoaded', function () {
panel.style.setProperty('--md-top', Math.max(24, (window.innerHeight - panel.offsetHeight) / 2) + 'px');
flyFrom(card, true);
} else {
panel.style.transition = 'none';
var mdCard = panel.querySelector('.md-card');
panel.style.transition = mdCard.style.transition = 'none';
place(card);
void panel.offsetWidth;
panel.style.transition = '';
panel.style.transition = mdCard.style.transition = '';
void panel.offsetWidth;
panel.classList.add('is-open');
card.classList.add('is-source');