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 + in-page player
------------------------------------------------------------
Card widths come from CSS (4 / 3 / 2 / 1 per breakpoint); the JS
only measures and translates, one card per step. Clicking a card
opens the overlay player instead of navigating to YouTube; the
href stays as a fallback for crawlers and no-JS.
============================================================ */
(function () {
function boot() {
var section = document.getElementById('rvlSection');
var player = document.getElementById('rvlPlayer');
if (!section || !player) return;
if (section.dataset.rvlReady === '1') return;
section.dataset.rvlReady = '1';
var viewport = document.getElementById('rvlViewport');
var track = document.getElementById('rvlTrack');
var dotWrap = document.getElementById('rvlDots');
var nav = document.getElementById('rvlNav');
var prevBtn = document.getElementById('rvlPrev');
var nextBtn = document.getElementById('rvlNext');
if (!viewport || !track || !dotWrap || !nav || !prevBtn || !nextBtn) return;
var cards = Array.prototype.slice.call(track.querySelectorAll('.rvl-card'));
var TOTAL = cards.length;
if (!TOTAL) return;
/* ---------- carousel ---------- */
var index = 0;
// Mirrors the CSS media queries. Reads the viewport, not the
// container, so section padding can't desync the two.
function visible() {
var w = window.innerWidth;
if (w <= 560) return 1;
if (w <= 700) return 2;
if (w <= 1024) return 3;
return 4;
}
function gap() {
return parseFloat(getComputedStyle(track).gap) || 22;
}
function cardWidth() {
return cards[0] ? cards[0].getBoundingClientRect().width : 0;
}
function maxIndex() {
return Math.max(0, TOTAL - visible());
}
function paint() {
var offset = index * (cardWidth() + gap());
track.style.transform = 'translate3d(-' + offset + 'px, 0, 0)';
prevBtn.disabled = index === 0;
nextBtn.disabled = index >= maxIndex();
var dots = dotWrap.querySelectorAll('.rvl-dot');
for (var i = 0; i < dots.length; i++) {
dots[i].classList.toggle('is-active', i === index);
dots[i].setAttribute('aria-current', i === index ? 'true' : 'false');
}
}
function goTo(i) {
index = Math.max(0, Math.min(i, maxIndex()));
paint();
}
function layout() {
var steps = maxIndex();
index = Math.min(index, steps);
// Nothing to page through at this width → hide arrows and dots
if (steps === 0) {
nav.hidden = true;
dotWrap.hidden = true;
dotWrap.innerHTML = '';
track.style.transform = 'translate3d(0, 0, 0)';
return;
}
nav.hidden = false;
dotWrap.hidden = false;
if (dotWrap.childElementCount !== steps + 1) {
dotWrap.innerHTML = '';
for (var i = 0; i <= steps; i++) {
var d = document.createElement('button');
d.type = 'button';
d.className = 'rvl-dot';
d.setAttribute('aria-label', 'Go to slide ' + (i + 1));
(function (n) {
d.addEventListener('click', function () { goTo(n); });
})(i);
dotWrap.appendChild(d);
}
}
paint();
}
prevBtn.addEventListener('click', function () { goTo(index - 1); });
nextBtn.addEventListener('click', function () { goTo(index + 1); });
// Axis-locked swipe — a vertical scroll that drifts sideways
// won't flip a slide
var sx = 0, sy = 0;
viewport.addEventListener('touchstart', function (e) {
sx = e.touches[0].clientX;
sy = e.touches[0].clientY;
}, { passive: true });
viewport.addEventListener('touchend', function (e) {
var dx = sx - e.changedTouches[0].clientX;
var dy = sy - e.changedTouches[0].clientY;
if (Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy) * 1.5) {
goTo(dx > 0 ? index + 1 : index - 1);
}
}, { passive: true });
var t;
window.addEventListener('resize', function () {
clearTimeout(t);
t = setTimeout(layout, 120);
});
layout();
/* ---------- player ---------- */
var stage = document.getElementById('rvlStage');
var scrim = document.getElementById('rvlScrim');
var closeBtn = document.getElementById('rvlClose');
var muteBtn = document.getElementById('rvlMute');
var pPrev = document.getElementById('rvlPlayerPrev');
var pNext = document.getElementById('rvlPlayerNext');
var caption = document.getElementById('rvlCaption');
var frame = null;
var muted = true;
var playing = -1;
var lastFocus = null;
function src(id) {
return 'https://www.youtube-nocookie.com/embed/' + id +
'?autoplay=1&mute=1&playsinline=1&rel=0&modestbranding=1' +
'&loop=1&playlist=' + id + '&enablejsapi=1';
}
function command(fn) {
if (!frame || !frame.contentWindow) return;
frame.contentWindow.postMessage(
JSON.stringify({ event: 'command', func: fn, args: [] }), '*'
);
}
function setMuted(on) {
muted = on;
command(on ? 'mute' : 'unMute');
muteBtn.setAttribute('aria-label', on ? 'Unmute' : 'Mute');
muteBtn.innerHTML = '';
}
function load(i) {
playing = (i + TOTAL) % TOTAL;
var card = cards[playing];
var id = card.getAttribute('data-rvl-id');
if (!id) return;
if (frame) frame.remove();
frame = document.createElement('iframe');
frame.className = 'rvl-frame';
frame.src = src(id);
var nameEl = card.querySelector('.rvl-name');
frame.title = nameEl ? nameEl.textContent : 'Reel';
frame.allow = 'autoplay; encrypted-media; picture-in-picture';
frame.setAttribute('allowfullscreen', '');
stage.appendChild(frame);
// Autoplay only works muted; sound is re-applied once the
// embed's JS API is listening.
if (!muted) setTimeout(function () { command('unMute'); }, 900);
caption.textContent = nameEl ? nameEl.textContent : '';
pPrev.hidden = pNext.hidden = TOTAL < 2;
}
function open(i, trigger) {
lastFocus = trigger || document.activeElement;
player.classList.add('is-open');
document.documentElement.classList.add('rvl-locked');
document.body.classList.add('rvl-locked');
setMuted(true);
load(i);
closeBtn.focus();
}
function close() {
player.classList.remove('is-open');
document.documentElement.classList.remove('rvl-locked');
document.body.classList.remove('rvl-locked');
if (frame) { frame.remove(); frame = null; }
playing = -1;
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
cards.forEach(function (card, i) {
card.addEventListener('click', function (e) {
if (!card.getAttribute('data-rvl-id')) return; // fall back to the href
e.preventDefault();
open(i, card);
});
});
closeBtn.addEventListener('click', close);
scrim.addEventListener('click', close);
pPrev.addEventListener('click', function () { load(playing - 1); });
pNext.addEventListener('click', function () { load(playing + 1); });
muteBtn.addEventListener('click', function () { setMuted(!muted); });
document.addEventListener('keydown', function (e) {
if (!player.classList.contains('is-open')) return;
if (e.key === 'Escape') close();
else if (e.key === 'ArrowLeft') load(playing - 1);
else if (e.key === 'ArrowRight') load(playing + 1);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
})();