The iframe is created on first hover and destroyed on leave, so
the page never pays the YouTube embed cost unless someone engages.
============================================================ */
(function () {
const box = document.getElementById('featureVideoBox');
if (!box) return;
const videoId = box.getAttribute('data-video-id');
const frameWrap = box.querySelector('.rb-feature-video-frame');
if (!videoId || !frameWrap) return;
let iframeEl = null;
let leaveTimer = null;
function play() {
clearTimeout(leaveTimer);
if (!iframeEl) {
iframeEl = document.createElement('iframe');
iframeEl.src = `https://www.youtube.com/embed/${videoId}?autoplay=1&mute=1&loop=1&playlist=${videoId}&controls=0&modestbranding=1&rel=0`;
iframeEl.allow = 'autoplay; encrypted-media';
iframeEl.title = 'Featured video preview';
frameWrap.appendChild(iframeEl);
}
box.classList.add('is-playing');
}
function stop() {
box.classList.remove('is-playing');
// Small delay so a quick mouse-out/in doesn't rebuild the iframe.
leaveTimer = setTimeout(() => {
if (iframeEl) {
iframeEl.remove();
iframeEl = null;
}
}, 200);
}
box.addEventListener('mouseenter', play);
box.addEventListener('mouseleave', stop);
// Touch devices have no hover — first tap previews, the link works after.
box.addEventListener('touchstart', function (e) {
if (!iframeEl) {
e.preventDefault();
play();
}
}, { passive: false });
})();
/* ============================================================
REELS / SHORTS CAROUSEL
------------------------------------------------------------
Expects markup:
...
Card widths come from CSS (4 / 3 / 1 per breakpoint); this only
measures and translates. Steps are one card at a time.
============================================================ */
(function () {
const section = document.querySelector('.rb-reels');
if (!section) return;
const outer = section.querySelector('.rb-carousel-outer');
const track = document.getElementById('reelsTrack');
const dotWrap = document.getElementById('reelsDotContainer');
const prevBtn = document.getElementById('reelsPrevBtn');
const nextBtn = document.getElementById('reelsNextBtn');
if (!outer || !track || !dotWrap || !prevBtn || !nextBtn) return;
const cards = Array.from(track.querySelectorAll('.rb-reel-card'));
const TOTAL = cards.length;
if (!TOTAL) return;
let currentIndex = 0;
function outerWidth() {
// Fractional width — offsetWidth rounds, and the lost sub-pixels
// are what clip the last card.
return outer.getBoundingClientRect().width;
}
function getVisible() {
const w = outerWidth();
if (w <= 560) return 1;
if (w <= 1024) return 3;
return 4;
}
function getGap() {
// Read the real gap from CSS so the JS can't drift out of sync
// with the media queries.
return parseFloat(getComputedStyle(track).gap) || 22;
}
function cardWidth() {
return cards[0] ? cards[0].getBoundingClientRect().width : 0;
}
function totalSteps() {
return Math.max(0, TOTAL - getVisible());
}
function buildDots() {
dotWrap.innerHTML = '';
// Nothing to page through — hide the controls entirely rather than
// showing one dead dot and two greyed arrows.
if (totalSteps() === 0) {
dotWrap.style.display = 'none';
prevBtn.parentElement.style.visibility = 'hidden';
return;
}
dotWrap.style.display = '';
prevBtn.parentElement.style.visibility = '';
for (let i = 0; i <= totalSteps(); i++) {
const d = document.createElement('div');
d.className = 'rb-dot' + (i === currentIndex ? ' active' : '');
d.addEventListener('click', () => goTo(i));
dotWrap.appendChild(d);
}
}
function updateDots() {
dotWrap.querySelectorAll('.rb-dot').forEach((d, i) => {
d.classList.toggle('active', i === currentIndex);
});
}
function applyTransform() {
const offset = currentIndex * (cardWidth() + getGap());
track.style.transform = `translate3d(-${offset}px, 0, 0)`;
}
function goTo(idx) {
currentIndex = Math.max(0, Math.min(idx, totalSteps()));
applyTransform();
updateDots();
prevBtn.classList.toggle('disabled', currentIndex === 0);
nextBtn.classList.toggle('disabled', currentIndex >= totalSteps());
}
function init() {
currentIndex = Math.min(currentIndex, totalSteps());
buildDots();
applyTransform();
prevBtn.classList.toggle('disabled', currentIndex === 0);
nextBtn.classList.toggle('disabled', currentIndex >= totalSteps());
}
prevBtn.addEventListener('click', () => goTo(currentIndex - 1));
nextBtn.addEventListener('click', () => goTo(currentIndex + 1));
// Touch / swipe support
let touchStartX = 0;
outer.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; }, { passive: true });
outer.addEventListener('touchend', e => {
const dx = touchStartX - e.changedTouches[0].clientX;
if (Math.abs(dx) > 40) dx > 0 ? goTo(currentIndex + 1) : goTo(currentIndex - 1);
}, { passive: true });
// Debounced resize
let resizeTimer;
const reflow = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(init, 80);
};
window.addEventListener('resize', reflow);
// The container can change width without the window doing so
// (scrollbar appearing, embedded in a themed page). Track it directly.
if (window.ResizeObserver) new ResizeObserver(reflow).observe(outer);
init();
// Re-measure once images/fonts have settled so widths are correct.
window.addEventListener('load', init);
})();