Home Page 2027

<!– WORDPRESS CUSTOM HTML PROTOTYPE
Paste this entire file into one Custom HTML block.
1. Replace YOUR_VIDEO_URL.mp4 with the File URL of an MP4 in your Media Library.
2. Edit the messages in the

    below; each

  1. becomes one bubble.
    3. Timing is editable in the script: cycleMs and lifetimeMs.
    View the saved page on the front end to test JavaScript.
    –>

    .ob-scene, .ob-scene * { box-sizing: border-box; }
    .ob-scene {
    position: relative; isolation: isolate; overflow: hidden;
    width: 100%; min-height: 480px; height: min(75vh, 760px);
    border-radius: 18px; background: radial-gradient(ellipse at 60% 35%, #245866, #061e2c 75%);
    color: #fff; font-family: inherit;
    }
    .ob-scene .ob-video, .ob-scene .ob-shade, .ob-scene .ob-stage {
    position: absolute; inset: 0; width: 100%; height: 100%;
    }
    .ob-scene .ob-video { object-fit: cover; z-index: 0; }
    .ob-scene .ob-shade {
    z-index: 1; pointer-events: none;
    background: linear-gradient(180deg, rgba(0,15,25,.12), rgba(0,15,25,.2) 60%, rgba(0,15,25,.7));
    }
    .ob-scene .ob-stage { z-index: 2; pointer-events: none; }
    .ob-scene .ob-bubble, .ob-scene .ob-fallback {
    position: absolute; left: 50%; top: 43%; margin: 0;
    width: min(70%, 520px); padding: 25px 32px;
    border: 1px solid rgba(255,255,255,.3); border-radius: 30px;
    background: rgba(5,27,39,.72); color: #fff;
    font-size: clamp(19px, 2.6vw, 29px); line-height: 1.45;
    font-weight: 400; text-align: center; text-shadow: 0 1px 3px rgba(0,0,0,.3);
    box-shadow: 0 16px 55px rgba(0,0,0,.15);
    transform: translate(-50%, -50%);
    }
    .ob-scene .ob-bubble { will-change: transform, opacity; }
    .ob-scene .ob-controls {
    position: absolute; z-index: 3; left: 20px; right: 20px; bottom: 20px;
    display: flex; align-items: flex-end; gap: 12px;
    }
    .ob-scene .ob-pause, .ob-scene .ob-transcript {
    color: #fff; background: rgba(4,23,33,.92);
    border: 1px solid rgba(255,255,255,.35); border-radius: 12px;
    font: inherit; font-size: 14px; line-height: 1.5;
    }
    .ob-scene .ob-pause { padding: 10px 15px; cursor: pointer; flex-shrink: 0; }
    .ob-scene .ob-pause[hidden] { display: none; }
    .ob-scene .ob-transcript { max-width: 560px; }
    .ob-scene .ob-transcript summary { padding: 10px 15px; cursor: pointer; }
    .ob-scene .ob-transcript ol { margin: 0; padding: 0 24px 16px 38px; max-height: 230px; overflow: auto; }
    .ob-scene .ob-transcript li { margin-bottom: 10px; }
    .ob-scene button:focus-visible, .ob-scene summary:focus-visible { outline: 3px solid #a8e6ff; outline-offset: 3px; }
    @media (max-width: 600px) {
    .ob-scene { min-height: 500px; border-radius: 12px; }
    .ob-scene .ob-bubble, .ob-scene .ob-fallback { width: 86%; padding: 22px; }
    .ob-scene .ob-controls { left: 12px; right: 12px; bottom: 12px; flex-wrap: wrap; }
    }
    @media (prefers-reduced-motion: reduce) {
    .ob-scene .ob-video { visibility: hidden; }
    }

    (function () {
    document.querySelectorAll(‘.ob-scene’).forEach(function (scene) {
    if (scene.dataset.obReady) return;
    scene.dataset.obReady = ‘true’;
    const stage = scene.querySelector(‘.ob-stage’);
    const video = scene.querySelector(‘.ob-video’);
    const button = scene.querySelector(‘.ob-pause’);
    const transcript = scene.querySelector(‘.ob-transcript’);
    const messages = Array.from(transcript.querySelectorAll(‘li’), li => li.textContent.trim()).filter(Boolean);
    const reduced = window.matchMedia(‘(prefers-reduced-motion: reduce)’);
    if (!messages.length || !stage.animate) return;

    // EDIT TIMING: a new message every 9 seconds; each lasts 11 seconds.
    // The last 3 seconds are the retreat, giving 2 seconds of overlap.
    const cycleMs = 9000;
    const lifetimeMs = 11000;
    let userPaused = false, elapsed = 0, nextAt = 0, index = 0, previous = null, frame;
    const animations = new Set();
    const positions = [ [46,42], [58,47], [43,49], [54,39] ];
    stage.replaceChildren();
    button.hidden = false;

    function stopped() {
    return userPaused || reduced.matches || document.hidden || transcript.open;
    }

    function sync() {
    const pause = stopped();
    animations.forEach(a => pause ? a.pause() : a.play());
    if (pause) video.pause();
    else {
    const attempt = video.play();
    if (attempt) attempt.catch(function () {});
    }

    button.textContent = userPaused ? ‘Resume motion’ : ‘Pause motion’;
    button.hidden = reduced.matches;

    if (reduced.matches) {
    animations.forEach(a => a.cancel());
    animations.clear();
    stage.replaceChildren();
    const text = document.createElement(‘p’);
    text.className = ‘ob-fallback’;
    text.textContent = messages[0];
    stage.appendChild(text);
    } else {
    const fallback = stage.querySelector(‘.ob-fallback’);
    if (fallback) {
    fallback.remove();
    nextAt = elapsed;
    }
    }
    }

    function spawn() {
    const bubble = document.createElement(‘p’);
    bubble.className = ‘ob-bubble’;
    bubble.textContent = messages[index % messages.length];

    const position = window.matchMedia(‘(max-width: 600px)’).matches
    ? [50,43]
    : positions[index % positions.length];

    const dx = (50 – position[0]) * stage.clientWidth / 100;
    const dy = (32 – position[1]) * stage.clientHeight / 100;

    bubble.style.left = position[0] + ‘%’;
    bubble.style.top = position[1] + ‘%’;
    stage.appendChild(bubble);
    index++;

    const base = ‘translate(-50%, -50%) perspective(900px)’;
    const animation = bubble.animate([
    {
    opacity: 0,
    transform: base + ‘ translateZ(30px)’,
    offset: 0
    },
    {
    opacity: 1,
    transform: base + ‘ translateZ(0)’,
    offset: 1000 / lifetimeMs
    },
    {
    opacity: 1,
    transform: base + ‘ translateZ(0)’,
    offset: (lifetimeMs – 3000) / lifetimeMs
    },
    {
    opacity: 0,
    transform: ‘translate(calc(-50% + ‘ + dx + ‘px), calc(-50% + ‘ + dy + ‘px)) perspective(900px) translateZ(-650px)’,
    offset: 1
    }
    ], {
    duration: lifetimeMs,
    easing: ‘ease-in-out’,
    fill: ‘forwards’
    });

    animations.add(animation);
    animation.finished.then(
    function () {
    animations.delete(animation);
    bubble.remove();
    },
    function () {
    bubble.remove();
    }
    );
    }

    function tick(now) {
    if (!scene.isConnected) {
    animations.forEach(a => a.cancel());
    video.pause();
    return;
    }

    if (previous !== null && !stopped()) {
    elapsed += Math.min(now – previous, 100);
    }

    previous = now;

    if (!stopped() && elapsed >= nextAt) {
    spawn();
    nextAt = elapsed + cycleMs;
    }

    frame = requestAnimationFrame(tick);
    }

    button.addEventListener(‘click’, function () {
    userPaused = !userPaused;
    sync();
    });

    transcript.addEventListener(‘toggle’, sync);
    document.addEventListener(‘visibilitychange’, sync);
    reduced.addEventListener(‘change’, sync);

    sync();
    frame = requestAnimationFrame(tick);
    });
    })();