diff --git a/css/pages/_hub.css b/css/pages/_hub.css
index ed63d1fa..04784437 100644
--- a/css/pages/_hub.css
+++ b/css/pages/_hub.css
@@ -76,21 +76,27 @@
.hub-story-text p { font-size: 17px; line-height: 28px; color: rgba(255, 255, 255, 0.9); margin: 0 0 16px; }
.hub-story-text p:last-child { margin-bottom: 0; }
-/* ---------- Use cases (image cards, continuous auto-scrolling marquee) ---------- */
+/* ---------- Use cases (image cards, auto-scrolling + drag + dots) ---------- */
.hub-carousel { position: relative; }
-/* masked viewport: cards fade out at both edges instead of being hard-cut */
+/* masked, scrollable viewport: cards fade out at both edges instead of being
+ hard-cut. Auto-scroll, click-and-drag and touch swiping are driven by JS
+ (setupHubCarousel); the native scrollbar is hidden. */
.hub-uses {
- overflow: hidden;
+ overflow-x: auto; overflow-y: hidden;
padding: 6px 0 10px;
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 2.5%, #000 97.5%, transparent 100%);
mask-image: linear-gradient(90deg, transparent 0, #000 2.5%, #000 97.5%, transparent 100%);
+ cursor: grab;
+ -webkit-user-select: none; user-select: none;
+ scrollbar-width: none; -ms-overflow-style: none;
+ scroll-behavior: auto;
}
-/* the moving track = two identical groups; translateX(-50%) loops seamlessly */
+.hub-uses::-webkit-scrollbar { display: none; }
+.hub-uses.is-dragging { cursor: grabbing; }
+/* the track = three identical groups so the JS loop is seamless in both directions */
.hub-track {
display: flex; width: max-content;
- animation: hub-marquee 55s linear infinite;
}
-.hub-carousel:hover .hub-track { animation-play-state: paused; }
.hub-group {
display: flex; gap: 18px; padding-right: 18px; flex: 0 0 auto;
}
@@ -106,17 +112,11 @@
.hub-use:nth-child(3n+2) { background: linear-gradient(135deg, #FA7850 0%, #E8613B 100%); }
.hub-use:nth-child(3n+3) { background: linear-gradient(135deg, #3B82C4 0%, #2C6BA6 100%); }
.hub-use:hover { transform: translateY(-4px); box-shadow: 0 18px 36px rgba(16, 24, 40, 0.18); }
-@keyframes hub-marquee {
- from { transform: translateX(0); }
- to { transform: translateX(-50%); }
-}
-@media (prefers-reduced-motion: reduce) {
- .hub-track { animation: none; }
-}
.hub-use-img {
position: absolute; inset: 0; width: 100%; height: 100%;
object-fit: cover; display: block;
transition: transform 0.4s ease;
+ -webkit-user-drag: none; user-select: none; pointer-events: none;
}
.hub-use:hover .hub-use-img { transform: scale(1.05); }
/* gradient rising from the bottom for text legibility */
@@ -138,6 +138,25 @@
.hub-use-body h3 { font-size: 18px; line-height: 1.25; color: #fff; margin: 0; font-weight: 700; }
.hub-use-body p { font-size: 13px; line-height: 19px; color: rgba(255, 255, 255, 0.9); margin: 0; }
+/* ---------- Scroll-position dots ---------- */
+.hub-dots {
+ display: flex; justify-content: center; align-items: center;
+ gap: 9px; margin-top: 18px;
+}
+.hub-dot {
+ width: 8px; height: 8px; padding: 0; border: 0; border-radius: 50%;
+ background: #CBD5DE; cursor: pointer; flex: 0 0 auto;
+ transition: width 0.25s ease, background-color 0.25s ease,
+ opacity 0.25s ease, transform 0.25s ease;
+}
+.hub-dot:hover { background: #9AA9B7; }
+.hub-dot.is-near { width: 7px; height: 7px; }
+.hub-dot.is-far { width: 6px; height: 6px; opacity: 0.6; }
+.hub-dot.is-active {
+ width: 24px; height: 8px; border-radius: 6px;
+ background: #2D738C; opacity: 1;
+}
+
/* ---------- Amenities ---------- */
.hub-amenities {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;
diff --git a/css/pages/_member.css b/css/pages/_member.css
index 824ce453..e15d284c 100644
--- a/css/pages/_member.css
+++ b/css/pages/_member.css
@@ -77,27 +77,22 @@
.mm-numbers-section .mm-note { color: rgba(255,255,255,0.6); margin-top: 24px; }
/* ---- ways (hub-style auto-scrolling slider) ---- */
-/* Mechanics (.hub-carousel/.hub-uses/.hub-track/.hub-group) and the marquee
- animation are reused from _hub.css; the JS clone step (setupHubCarousel) runs
- on every page and picks up the .hub-track here. These rules only restyle the
- card body for icon-based cards (no photo). */
+/* Mechanics (.hub-carousel/.hub-uses/.hub-track/.hub-group), the marquee
+ animation, the photo (.hub-use-img), the legibility gradient (::after) and
+ the bottom-anchored body are all reused from _hub.css; the JS clone step
+ (setupHubCarousel) runs on every page and picks up the .hub-track here.
+ These rules only tweak card size and add a small icon badge over the photo. */
.mm-ways-carousel { margin-top: 4px; }
-.mm-use {
- display: flex; flex-direction: column; justify-content: space-between;
- gap: 18px; padding: 24px 22px;
- width: clamp(240px, 24vw, 300px);
- aspect-ratio: 5 / 6;
-}
-/* No photo here, so drop the hub image-legibility overlay. */
-.mm-use::after { display: none; }
+.mm-use { width: clamp(240px, 24vw, 300px); aspect-ratio: 5 / 6; }
+/* Brand icon as a subtle glass badge floating over the photo. */
.mm-use-ico {
+ position: absolute; top: 14px; left: 14px; z-index: 3;
display: inline-flex; align-items: center; justify-content: center;
- width: 54px; height: 54px; border-radius: 14px;
- background: rgba(255, 255, 255, 0.18);
- color: #fff; font-size: 23px; flex: 0 0 auto;
+ width: 44px; height: 44px; border-radius: 12px;
+ background: rgba(10, 18, 26, 0.42);
+ -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
+ color: #fff; font-size: 19px;
}
-/* Body is absolute-positioned for photo cards in _hub.css; here it flows normally. */
-.mm-use .hub-use-body { position: static; padding: 0; gap: 6px; }
.mm-use .hub-use-body h3 { font-size: 19px; line-height: 1.25; }
.mm-use .hub-use-body p { font-size: 14px; line-height: 21px; }
diff --git a/en/become-a-member/index.html b/en/become-a-member/index.html
index e3c15eab..dd9a5285 100644
--- a/en/become-a-member/index.html
+++ b/en/become-a-member/index.html
@@ -76,8 +76,8 @@
@@ -135,8 +135,8 @@
@@ -209,7 +209,7 @@
Why become a member?
@@ -266,26 +266,32 @@
+
Stay connected
Receive updates and know what is happening.
+
Join the community
Come to events, meet people and take part when it suits you.
+
Volunteer sometimes
Help with selected activities without a long-term commitment.
+
Join projects
Contribute to specific initiatives, campaigns, guides or collaborations.
+
Organise events
Help plan, coordinate, promote and deliver activities.
+
Take responsibility
Explore leadership, governance, representation or local team roles.
diff --git a/images/mm-benefit-piece-of-home.webp b/images/mm-benefit-piece-of-home.webp
new file mode 100644
index 00000000..4aca1f2c
Binary files /dev/null and b/images/mm-benefit-piece-of-home.webp differ
diff --git a/images/mm-benefit-real-events.webp b/images/mm-benefit-real-events.webp
new file mode 100644
index 00000000..d8ef6d03
Binary files /dev/null and b/images/mm-benefit-real-events.webp differ
diff --git a/images/mm-way-community.webp b/images/mm-way-community.webp
new file mode 100644
index 00000000..f40c220d
Binary files /dev/null and b/images/mm-way-community.webp differ
diff --git a/images/mm-way-events.webp b/images/mm-way-events.webp
new file mode 100644
index 00000000..7c827025
Binary files /dev/null and b/images/mm-way-events.webp differ
diff --git a/images/mm-way-projects.webp b/images/mm-way-projects.webp
new file mode 100644
index 00000000..975e9eb0
Binary files /dev/null and b/images/mm-way-projects.webp differ
diff --git a/images/mm-way-responsibility.webp b/images/mm-way-responsibility.webp
new file mode 100644
index 00000000..65a565d6
Binary files /dev/null and b/images/mm-way-responsibility.webp differ
diff --git a/images/mm-way-stay-connected.webp b/images/mm-way-stay-connected.webp
new file mode 100644
index 00000000..dba2fe82
Binary files /dev/null and b/images/mm-way-stay-connected.webp differ
diff --git a/images/mm-way-volunteer.webp b/images/mm-way-volunteer.webp
new file mode 100644
index 00000000..41c34db4
Binary files /dev/null and b/images/mm-way-volunteer.webp differ
diff --git a/main.js b/main.js
index 2ecc76de..2cc3fc98 100644
--- a/main.js
+++ b/main.js
@@ -931,22 +931,174 @@ document.addEventListener('DOMContentLoaded', function() {
/**
* ===================================================================
- * HUB USE-CASE CAROUSEL (auto-scrolling)
+ * HUB USE-CASE CAROUSEL (auto-scrolling + drag + position dots)
* ===================================================================
+ * Drives every .hub-carousel on the page (Hub "What you can do here"
+ * and the "ways to be part of MSOS" slider). The track is built from
+ * three identical .hub-group copies so the scroll loops seamlessly in
+ * both directions; auto-scroll is JS-driven (not a CSS animation) so it
+ * can co-exist with click-and-drag and touch swiping.
*/
function setupHubCarousel() {
- const trackEl = document.querySelector('.hub-track');
- if (!trackEl) return;
- const group = trackEl.querySelector('.hub-group');
- if (!group) return;
+ const carousels = document.querySelectorAll('.hub-carousel');
+ if (!carousels.length) return;
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ const LOOP_MS = 55000; // time to travel one group width (matches old 55s marquee)
- // Duplicate the group so translateX(-50%) loops back seamlessly (waterslide flow).
- // Guard against double-cloning if the script somehow runs twice.
- if (trackEl.querySelectorAll('.hub-group').length < 2) {
- const clone = group.cloneNode(true);
- clone.setAttribute('aria-hidden', 'true');
- trackEl.appendChild(clone);
- }
+ carousels.forEach(function (carousel) {
+ if (carousel.dataset.carouselInit) return;
+ carousel.dataset.carouselInit = '1';
+
+ const scroller = carousel.querySelector('.hub-uses');
+ const track = carousel.querySelector('.hub-track');
+ if (!scroller || !track) return;
+ const firstGroup = track.querySelector('.hub-group');
+ if (!firstGroup) return;
+
+ // Need 3 identical groups so a full neighbour is always visible at each wrap point.
+ while (track.querySelectorAll('.hub-group').length < 3) {
+ const clone = firstGroup.cloneNode(true);
+ clone.setAttribute('aria-hidden', 'true');
+ track.appendChild(clone);
+ }
+
+ const cards = firstGroup.children;
+ const cardCount = cards.length;
+ if (!cardCount) return;
+
+ // --- Position dots ---
+ const dotsWrap = document.createElement('div');
+ dotsWrap.className = 'hub-dots';
+ dotsWrap.setAttribute('role', 'tablist');
+ dotsWrap.setAttribute('aria-label', 'Carousel position');
+ const dotEls = [];
+ for (let i = 0; i < cardCount; i++) {
+ const b = document.createElement('button');
+ b.type = 'button';
+ b.className = 'hub-dot';
+ b.setAttribute('aria-label', 'Go to item ' + (i + 1));
+ b.addEventListener('click', (function (idx) {
+ return function () { goTo(idx); };
+ })(i));
+ dotsWrap.appendChild(b);
+ dotEls.push(b);
+ }
+ carousel.appendChild(dotsWrap);
+
+ // --- Measurements (recomputed on resize / image load) ---
+ let groupWidth = 0, step = 0;
+ function measure() {
+ groupWidth = firstGroup.offsetWidth;
+ step = cardCount > 1 ? (cards[1].offsetLeft - cards[0].offsetLeft) : groupWidth;
+ if (!step) step = groupWidth / cardCount;
+ }
+
+ // Keep scroll within the middle copy [groupWidth, 2*groupWidth) so it loops both ways.
+ function wrap() {
+ if (!groupWidth) return;
+ if (scroller.scrollLeft >= 2 * groupWidth) scroller.scrollLeft -= groupWidth;
+ else if (scroller.scrollLeft < groupWidth) scroller.scrollLeft += groupWidth;
+ }
+
+ function updateDots() {
+ if (!step || !groupWidth) return;
+ const within = ((scroller.scrollLeft % groupWidth) + groupWidth) % groupWidth;
+ const active = Math.round(within / step) % cardCount;
+ for (let i = 0; i < cardCount; i++) {
+ const dist = Math.min(Math.abs(i - active), cardCount - Math.abs(i - active));
+ dotEls[i].classList.toggle('is-active', i === active);
+ dotEls[i].classList.toggle('is-near', dist === 1);
+ dotEls[i].classList.toggle('is-far', dist >= 2);
+ }
+ }
+
+ function reset() {
+ measure();
+ if (groupWidth) scroller.scrollLeft = groupWidth;
+ updateDots();
+ }
+ reset();
+ window.addEventListener('load', reset);
+ window.addEventListener('resize', function () { measure(); wrap(); updateDots(); });
+
+ // --- Jump to a specific card when a dot is clicked ---
+ // Custom eased scroll (not native smooth) so it never fights the
+ // rAF auto-scroll loop. Target always sits in the middle copy
+ // [groupWidth, 2*groupWidth), so wrap() stays a no-op during it.
+ let programmatic = 0; // ms remaining where auto-scroll yields
+ let animId = 0;
+ function goTo(idx) {
+ if (!step) measure();
+ const start = scroller.scrollLeft;
+ const target = groupWidth + idx * step;
+ const dur = 450, t0 = performance.now();
+ const ease = function (t) { return 1 - Math.pow(1 - t, 3); };
+ cancelAnimationFrame(animId);
+ programmatic = dur + 80;
+ (function anim(now) {
+ const t = Math.min(1, (now - t0) / dur);
+ scroller.scrollLeft = start + (target - start) * ease(t);
+ updateDots();
+ if (t < 1) animId = requestAnimationFrame(anim);
+ })(t0);
+ }
+
+ // --- Auto-scroll loop ---
+ let paused = false;
+ let dragging = false;
+ let last = null;
+ function tick(ts) {
+ if (last == null) last = ts;
+ const dt = ts - last;
+ last = ts;
+ if (!reduceMotion && !paused && programmatic <= 0 && groupWidth) {
+ scroller.scrollLeft += (groupWidth / LOOP_MS) * dt;
+ }
+ if (programmatic > 0) programmatic -= dt;
+ wrap();
+ updateDots();
+ requestAnimationFrame(tick);
+ }
+ requestAnimationFrame(tick);
+
+ // Pause on hover (desktop).
+ carousel.addEventListener('mouseenter', function () { paused = true; });
+ carousel.addEventListener('mouseleave', function () { if (!dragging) paused = false; });
+
+ // --- Click-and-drag (mouse) + pause during touch (native scroll handles the swipe) ---
+ let lastX = 0;
+ scroller.addEventListener('pointerdown', function (e) {
+ paused = true;
+ if (e.pointerType !== 'mouse') return; // touch/pen: let the browser scroll natively
+ dragging = true;
+ lastX = e.clientX;
+ scroller.classList.add('is-dragging');
+ try { scroller.setPointerCapture(e.pointerId); } catch (err) {}
+ });
+ scroller.addEventListener('pointermove', function (e) {
+ if (!dragging) return;
+ const dx = e.clientX - lastX;
+ lastX = e.clientX;
+ scroller.scrollLeft -= dx;
+ wrap();
+ updateDots();
+ e.preventDefault();
+ });
+ function endDrag(e) {
+ if (dragging) {
+ dragging = false;
+ scroller.classList.remove('is-dragging');
+ try { scroller.releasePointerCapture(e.pointerId); } catch (err) {}
+ }
+ // Resume auto-scroll unless the cursor is still hovering the carousel.
+ if (!carousel.matches(':hover')) paused = false;
+ }
+ scroller.addEventListener('pointerup', endDrag);
+ scroller.addEventListener('pointercancel', endDrag);
+
+ // Prevent the browser's native image drag-ghost while dragging.
+ track.addEventListener('dragstart', function (e) { e.preventDefault(); });
+ });
}
/**
diff --git a/mk/become-a-member/index.html b/mk/become-a-member/index.html
index 8a7d26fa..99f1ba45 100644
--- a/mk/become-a-member/index.html
+++ b/mk/become-a-member/index.html
@@ -76,8 +76,8 @@
@@ -135,8 +135,8 @@
@@ -209,7 +209,7 @@
Зошто да станеш член?
@@ -265,26 +265,32 @@
+
Остани поврзан
Примај новости и знај што се случува.
+
Приклучи се на заедницата
Доаѓај на настани, запознавај луѓе и учествувај кога ти одговара.
+
Волонтирај повремено
Помагај на избрани активности без долгорочна обврска.
+
Приклучи се на проекти
Придонеси на конкретни иницијативи, кампањи, водичи или соработки.
+
Организирај настани
Помагај да се планираат, координираат, промовираат и реализираат активности.
+
Преземи одговорност
Истражи лидерство, управување, застапување или улоги во локален тим.
diff --git a/si/become-a-member/index.html b/si/become-a-member/index.html
index 635cfba1..56aa868e 100644
--- a/si/become-a-member/index.html
+++ b/si/become-a-member/index.html
@@ -76,8 +76,8 @@
@@ -135,8 +135,8 @@
@@ -209,7 +209,7 @@
Zakaj postati član?
@@ -266,26 +266,32 @@
+
Ostani povezan
Prejemaj novice in vedi, kaj se dogaja.
+
Pridruži se skupnosti
Obišči dogodke, spoznaj ljudi in sodeluj, ko ti ustreza.
+
Občasno prostovoljči
Pomagaj pri izbranih dejavnostih brez dolgoročne zaveze.
+
Pridruži se projektom
Prispevaj k pobudam, kampanjam, vodnikom ali sodelovanjem.
+
Organiziraj dogodke
Pomagaj načrtovati, usklajevati, promovirati in izvesti dejavnosti.
+
Prevzemi odgovornost
Razišči vodenje, upravljanje, zastopanje ali vloge v lokalni ekipi.