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

+32 -90
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;
});
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 (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); }
});
}
requestAnimationFrame(tick);
});