Skip to main content

Holiday Script for SKYNET2.net

I previously had some simple snowfall and Christmas lights on there, but decided to ramp it up a bit because Im that extra type of nerd. So I ended up moving the holiday stuff out of the site's core script and made a new one. I also added some extra CSS to my custom CSS file that is loaded so I don't have to touch the template one. 

Here is what I have, enjoy.

holiday.js

/* =========================================================
   SKYNET2 Holiday Controller + Unified Falling Stuff Engine
   - single canvas / single animation loop
   - low-end friendly (caps, pauses when hidden)
   - respects prefers-reduced-motion
   ========================================================= */

(() => {
    "use strict";

    // =========================================================
    // DEBUG OVERRIDE (URL-driven, no file edits needed)
    // Usage examples:
    //   ?debug=christmas-15
    //   ?debug=christmas-day
    //   ?debug=newyear-burst
    //   ?debug=2026-12-25
    //   ?debug=2026-12-25T00:05
    // =========================================================

    // Optional fallback preset if you want a default test when no URL is provided.
    // Leave null for normal operation.
    const DEBUG_PRESET = null; // e.g. "christmas-day"

    const DEBUG_DATE = debugDateFromUrl() || debugDateFromPreset(DEBUG_PRESET) || null;

    function debugDateFromUrl() {
    const params = new URLSearchParams(window.location.search);
    const v = params.get("debug");
    if (!v) return null;

    // 1) named presets
    const preset = debugDateFromPreset(v);
    if (preset) return preset;

    // 2) ISO-ish support:
    //   - 2026-12-25
    //   - 2026-12-25T00:05
    const iso = v.includes("T") ? v : `${v}T12:00`;
    const d = new Date(iso);
    return isNaN(d.getTime()) ? null : d;
    }

    function debugDateFromPreset(key) {
    if (!key) return null;

    // helpers
    const atNoon = (y, m, d) => new Date(y, m, d, 12, 0);
    const daysBefore = (y, m, d, n) => {
        const t = atNoon(y, m, d);
        t.setDate(t.getDate() - n);
        return t;
    };

    // Pick a “test year” that keeps stuff sensible.
    // (Any year works; it’s just used to compute dates.)
    const Y = 2026;

    // Map of ALL supported presets
    const map = {
        // --- New Year ---
        "newyear-5":  daysBefore(Y + 1, 0, 1, 5),         // Dec 27
        "newyear-day":   atNoon(Y + 1, 0, 1),             // Jan 1
        "newyear-burst": new Date(Y + 1, 0, 1, 0, 5),     // Jan 1 00:05

        // --- Independence Day ---
        "independence-15": daysBefore(Y, 6, 4, 15),
        "independence-day": atNoon(Y, 6, 4),

        // --- Birthdays (day only) ---
        "allie-bday": atNoon(Y, 6, 10),  // Jul 10
        "jenny-bday": atNoon(Y, 7, 5),   // Aug 5

        // --- Halloween ---
        "halloween-15": daysBefore(Y, 9, 31, 15),
        "halloween-day": atNoon(Y, 9, 31),

        // --- USMC Birthday ---
        "usmc-9":  daysBefore(Y, 10, 10, 9),               // Nov 1 (USMC countdown works)
        "usmc-day": atNoon(Y, 10, 10),

        // --- Veterans Day (day only; your code has windowStartDays: 0) ---
        "veterans-day": atNoon(Y, 10, 11),

        // --- Thanksgiving (dynamic) ---
        "thanksgiving-14": new Date(2026, 10, 12, 12, 0), // Nov 12
        "thanksgiving-day": new Date(2026, 10, 26, 12, 0), // Nov 26, 2026

        // --- Christmas ---
        "christmas-15": daysBefore(Y, 11, 25, 15),
        "christmas-day": atNoon(Y, 11, 25),

        // --- Star Trek Day ---
        "startrek-day": atNoon(Y, 8, 8),

        // --- Star Wars Day ---
        "starwars-day":     atNoon(Y, 4, 4),
    };

    return map[key] || null;
    }

    function nowDate() {
    return DEBUG_DATE ? new Date(DEBUG_DATE) : new Date();
    }

    // ---------- DOM targets ----------
    const countdownWrap = document.getElementById("holiday-countdown");
    const titleEl = document.getElementById("countdown-title");
    const daysEl = document.getElementById("countdown-days");

    // If the countdown isn't present, just bail quietly.
    if (!countdownWrap || !titleEl || !daysEl) return;

    // ---------- Motion preference ----------
    const prefersReducedMotion = () =>
        window.matchMedia &&
        window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    // ---------- Date helpers ----------
    const pad2 = (n) => String(n).padStart(2, "0");

    // Midnight local time (browser local)
    function atMidnight(d) {
        return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
    }

    // Days between "now" and targetDate (midnight-based)
    function daysUntil(targetDate, baseNow = nowDate()) {
        const now = atMidnight(baseNow);
        const target = atMidnight(targetDate);
        const diffMs = target - now;
        return Math.floor(diffMs / 86400000);
    }

    // nth weekday of a month (0=Sun..6=Sat), month is 0-11
    function nthWeekdayOfMonth(year, month, weekday, nth) {
        const first = new Date(year, month, 1);
        const firstWeekday = first.getDay();
        const offset = (weekday - firstWeekday + 7) % 7;
        const day = 1 + offset + (nth - 1) * 7;
        return new Date(year, month, day);
    }

    function isSameYMD(a, b) {
        return (
            a.getFullYear() === b.getFullYear() &&
            a.getMonth() === b.getMonth() &&
            a.getDate() === b.getDate()
        );
    }

    // Ordinal suffix: 1ST, 2ND, 3RD, 4TH...
    function ordinal(n) {
        const mod100 = n % 100;
        if (mod100 >= 11 && mod100 <= 13) return `${n}TH`;
        const mod10 = n % 10;
        if (mod10 === 1) return `${n}ST`;
        if (mod10 === 2) return `${n}ND`;
        if (mod10 === 3) return `${n}RD`;
        return `${n}TH`;
    }

    // ---------- Unified Falling Stuff Engine ----------
    const FallingStuffEngine = (() => {
        let canvas = null;
        let ctx = null;

        let rafId = null;
        let running = false;
        let paused = false;

        let particles = [];
        let config = null;
        let lastFrame = 0;

        const BASE_FONT =
        '"Antonio", system-ui, -apple-system, "Segoe UI", Roboto, Arial, ' +
        '"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif';

        function ensureCanvas() {
            if (canvas) return;

            canvas = document.createElement("canvas");
            canvas.id = "skynet2-holiday-canvas";
            canvas.style.position = "fixed";
            canvas.style.inset = "0";
            canvas.style.pointerEvents = "none";
            canvas.style.zIndex = "9999";
            document.body.appendChild(canvas);

            ctx = canvas.getContext("2d", {
                alpha: true
            });

            resize(); // init

            window.addEventListener("resize", resize, {
                passive: true
            });
            window.addEventListener("pageshow", resize, {
                passive: true
            });
            window.addEventListener("focus", resize, {
                passive: true
            });

            document.addEventListener("visibilitychange", () => {
                paused = document.hidden;
                if (!document.hidden) {
                    // force a clean timing restart
                    lastFrame = 0;
                    resize();
                }
            });
        }

        function destroyCanvas() {
            if (canvas) canvas.remove();
            canvas = null;
            ctx = null;
        }

        function resize() {
            if (!canvas || !ctx) return;

            const w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
            const h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
            if (w < 200 || h < 200) return;

            const dpr = window.devicePixelRatio || 1;

            canvas.style.width = w + "px";
            canvas.style.height = h + "px";
            canvas.width = Math.round(w * dpr);
            canvas.height = Math.round(h * dpr);

            ctx.setTransform(1, 0, 0, 1, 0, 0);
            ctx.scale(dpr, dpr);
        }

        function randomFrom(arr) {
            return arr[Math.floor(Math.random() * arr.length)];
        }

        function spawnParticle() {
            const rect = canvas.getBoundingClientRect();
            const w = rect.width;
            const h = rect.height;

            const glyphArr = Array.isArray(config.glyphs) ? config.glyphs : [config.glyphs];

            const size = config.sizeMin + Math.random() * (config.sizeMax - config.sizeMin);
            const speed = config.speedMin + Math.random() * (config.speedMax - config.speedMin);

            const swayAmp = config.sway ? (config.swayAmpMin + Math.random() * (config.swayAmpMax - config.swayAmpMin)) : 0;
            const swayFreq = config.sway ? (config.swayFreqMin + Math.random() * (config.swayFreqMax - config.swayFreqMin)) : 0;

            const color = Array.isArray(config.colors) ? randomFrom(config.colors) : (config.colors || "rgba(255,255,255,0.85)");

            const x = Math.random() * w;
            const y = Math.random() * h - h; // start above screen like your old code

            return {
                x,
                y,
                baseX: x,
                size,
                speed,
                swayAmp,
                swayFreq,
                swayPhase: Math.random() * Math.PI * 2,
                glyph: randomFrom(glyphArr),
                color
            };
        }

        function clear(rectW, rectH) {
            ctx.clearRect(0, 0, rectW, rectH);
        }

        function drawParticle(p) {
            ctx.font = `${p.size}px ${BASE_FONT}`;
            ctx.fillStyle = p.color;
            ctx.fillText(p.glyph, p.x, p.y);
        }

        function tick(ts) {
            if (!running || !ctx || !config || !canvas) return;

            if (paused) {
                rafId = requestAnimationFrame(tick);
                return;
            }

            const rect = canvas.getBoundingClientRect();
            const w = rect.width;
            const h = rect.height;

            if (!lastFrame) lastFrame = ts;
            const dt = Math.min(0.05, (ts - lastFrame) / 1000);
            lastFrame = ts;

            clear(w, h);

            const killY = h + 20;

            for (let i = 0; i < particles.length; i++) {
                const p = particles[i];

                p.y += p.speed * dt;

                if (config.sway) {
                    const t = (ts / 1000) * p.swayFreq + p.swayPhase;
                    p.x = p.baseX + Math.sin(t) * p.swayAmp;
                }

                drawParticle(p);

                // recycle like the old working code
                if (p.y > killY) {
                    p.y = -20;
                    p.x = Math.random() * w;
                    p.baseX = p.x;

                    if (Array.isArray(config.glyphs)) p.glyph = randomFrom(config.glyphs);
                    if (Array.isArray(config.colors)) p.color = randomFrom(config.colors);
                }
            }

            rafId = requestAnimationFrame(tick);
        }

        function start(newConfig) {
            if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

            ensureCanvas();
            resize();

            config = {
                glyphs: "❄",
                colors: "rgba(255,255,255,0.85)",
                maxParticles: 120,
                sizeMin: 16,
                sizeMax: 34,
                speedMin: 30,
                speedMax: 80,
                sway: true,
                swayAmpMin: 2,
                swayAmpMax: 12,
                swayFreqMin: 0.6,
                swayFreqMax: 1.6,
                ...newConfig
            };

            // fixed pool — no “spawn timing” at all
            particles = [];
            for (let i = 0; i < config.maxParticles; i++) {
                particles.push(spawnParticle());
            }

            paused = document.hidden;
            lastFrame = 0;

            if (!running) {
                running = true;
                rafId = requestAnimationFrame(tick);
            }
        }

        function stop() {
            running = false;
            config = null;
            particles = [];
            if (rafId) cancelAnimationFrame(rafId);
            rafId = null;
            lastFrame = 0;
            destroyCanvas();
        }

        function isRunning() {
            return running;
        }

        return {
            start,
            stop,
            isRunning
        };
    })();

    // ---------- Holiday definitions ----------
    // windowStartDays: when countdown begins (days before holiday)
    // if windowStartDays is 0, countdown never shows (day-of only)
    // JS dates (specifically months) are 0-11 so the months are one off! - you arent crazy
    const HOLIDAYS = [{
            key: "newyear",
            labelUntil: "DAYS UNTIL<br>&#x1F389;&nbsp;NEW YEARS&nbsp;&#x1F389;",
            labelToday: "HAPPY NEW YEAR!",
            dateForYear: (y) => new Date(y, 0, 1), // January 1st
            windowStartDays: 30
        },
        {
            key: "may4",
            labelUntil: "DAYS UNTIL<br>&#x2B50;&nbsp;MAY THE FOURTH&nbsp;&#x2B50;", // ⭐
            labelToday: "MAY THE FOURTH<br>BE WITH YOU!",
            dateForYear: (y) => new Date(y, 4, 4), // May 4 (month=4)
            windowStartDays: 0 // surprise mode
        },
        {
            key: "independence",
            labelUntil: "DAYS UNTIL<br>&#x1F1FA;&#x1F1F8;&nbsp;JUL 4th&nbsp;&#x1F1FA;&#x1F1F8;",
            labelToday: "REBEL SCUM!",
            dateForYear: (y) => new Date(y, 6, 4), // July 4th
            windowStartDays: 30
        },
        {
            key: "bdayallie",
            labelUntil: "UNTIL ALLIE'S BIRTHDAY",
            labelToday: "HAPPY BIRTHDAY!",
            dateForYear: (y) => new Date(y, 6, 10), // July 10th
            windowStartDays: 0
        },
        {
            key: "bdayjenny",
            labelUntil: "UNTIL JENNY'S BIRTHDAY",
            labelToday: "HAPPY BIRTHDAY!",
            dateForYear: (y) => new Date(y, 7, 5), // August 5th
            windowStartDays: 0
        },
        {
            key: "startrek",
            labelUntil: "DAYS UNTIL<br>&#x1F596;&nbsp;STAR TREK DAY&nbsp;&#x1F596;", // 🖖
            labelToday: "STAR TREK DAY!",
            dateForYear: (y) => new Date(y, 8, 8), // Sep 8 (month=8)
            windowStartDays: 0 // surprise mode
        },
        {
            key: "halloween",
            labelUntil: "DAYS UNTIL<br>&#x1F383;&nbsp;HALLOWEEN&nbsp;&#x1F383;",
            labelToday: "HAPPY HALLOWEEN!",
            dateForYear: (y) => new Date(y, 9, 31), // October 31st
            // start Oct 1 (month theme), not 30 days
            windowStartDays: 31 // effectively "all October" since Oct has 31 days
        },
        {
            key: "usmc",
            labelUntil: "DAYS UNTIL<br>&#x1F1FA;&#x1F1F8;&nbsp;USMC BIRTHDAY&nbsp;&#x1F1FA;&#x1F1F8;",
            labelToday: "SEMPER FI!",
            dateForYear: (y) => new Date(y, 10, 10), // November 10th
            windowStartDays: 30,
            specialToday: (now) => {
                const n = now.getFullYear() - 1775;
                return ordinal(n); // e.g. 250TH
            }
        },
        {
            key: "veterans",
            labelUntil: "DAYS UNTIL<br>&#x1F1FA;&#x1F1F8;&nbsp;VETERANS DAY&nbsp;&#x1F1FA;&#x1F1F8;",
            labelToday: "HAPPY VETERANS DAY!",
            dateForYear: (y) => new Date(y, 10, 11), // November 11th
            windowStartDays: 0 // day-of only to avoid fighting USMC
        },
        {
            key: "thanksgiving",
            labelUntil: "DAYS UNTIL<br>&#x1F983;&nbsp;THANKSGIVING&nbsp;&#x1F983;",
            labelToday: "HAPPY<br>THANKSGIVING!",
            dateForYear: (y) => nthWeekdayOfMonth(y, 10, 4, 4), // November, Thursday=4, 4th Thrusday of the month
            windowStartDays: 14
        },
        {
            key: "christmas",
            labelUntil: "DAYS UNTIL<br>&#x1F384;&nbsp;CHRISTMAS&nbsp;&#x1F384;",
            labelToday: "MERRY CHRISTMAS!",
            dateForYear: (y) => new Date(y, 11, 25), // December 25th
            windowStartDays: 30
        }
    ];

    // Seasonal window: Dec 1 -> Feb 2
    function inWinterWindow(now) {
        const y = now.getFullYear();
        const dec1 = new Date(y, 11, 1);
        const feb2 = new Date(y + 1, 1, 2); // Feb 2 next year
        // If we're in Jan/Feb, the window started last year
        const dec1Prev = new Date(y - 1, 11, 1);
        const feb2This = new Date(y, 1, 2);
        const t = now.getTime();
        return (t >= dec1.getTime() && t < feb2.getTime()) ||
            (t >= dec1Prev.getTime() && t < feb2This.getTime());
    }

    // Countdown day color per holiday (numbers only)
    const HOLIDAY_DAY_COLORS = {
        newyear: "var(--gold)",
        independence: "var(--blue)",
        halloween: "var(--orange)",
        usmc: "var(--almond)",
        veterans: "var(--bluey)",
        thanksgiving: "var(--gold)",
        christmas: "var(--christmas-green)",
    };

    // Holidays where you do NOT want blink behavior
    const NO_BLINK_KEYS = new Set(["usmc", "veterans"]);

    // helper: remove/add blink classes cleanly
    function setBlinkClass(el, clsOrNull) {
        el.classList.remove("blink-slow", "blink", "blink-fast");
        if (clsOrNull) el.classList.add(clsOrNull);
    }

    function applyCountdownStyling(state) {
        // Always start clean
        daysEl.style.color = "";
        setBlinkClass(daysEl, null);

        // Only countdown gets holiday color + blink ladder
        if (state.mode !== "countdown") return;

        const key = state.holiday?.key;

        // Color by holiday (numbers only)
        const color = HOLIDAY_DAY_COLORS[key];
        if (color) daysEl.style.color = color;

        // Blink ladder (unless excluded)
        if (NO_BLINK_KEYS.has(key)) return;

        const d = state.days;
        if (d <= 5) setBlinkClass(daysEl, "blink-fast");
        else if (d <= 7) setBlinkClass(daysEl, "blink");
        else if (d <= 10) setBlinkClass(daysEl, "blink-slow");
    }


    // ---------- Effect profiles (single engine) ----------
    const HOLIDAY_EMOJI = {
        newyear: "\u{1F942}", // 🥂
        may4: "\u2B50", // ⭐
        independence: "\u{1F1FA}\u{1F1F8}", // 🇺🇸 (flag = TWO codepoints)
        bdayjenny: "\u{1F973}", // 🥳 party!
        startrek: "\u{1F596}", // 🖖
        bdayallie: "\u{1F973}", // 🥳 party!
        halloween: "\u{1F47B}", // 👻
        usmc: "\u2694\uFE0F", // ⚔️ (variation selector matters)
        veterans: "\u{1F1FA}\u{1F1F8}", // 🇺🇸 flag
        thanksgiving: "\u{1F983}", // 🦃
        christmas: "\u{1F384}", // 🎄
    };

    // NOTE: keep emoji-heavy profiles lower maxParticles for weaker hardware.
    const PROFILES = {
        winterSnow: {
            glyphs: ["\u2744", "\u2745", "\u2746", "\u2744"], // ❄ ❅ ❆ ❄️ (encoding-safe)
            colors: "rgba(255,255,255,0.80)",
            maxParticles: 120,
            spawnEveryMs: 60,
            sizeMin: 16,
            sizeMax: 34,
            speedMin: 30,
            speedMax: 80,
            sway: true
        },
        rwbConfetti: {
            glyphs: ["\u25A0", "\u25AA", "\u25C6", "\u25CF", "\u{1F1FA}\u{1F1F8}"], // ■ ▪ ◆ ●
            colors: ["rgba(255,60,60,0.85)", "rgba(255,255,255,0.85)", "rgba(80,140,255,0.85)"],
            maxParticles: 90,
            spawnEveryMs: 55,
            sizeMin: 12,
            sizeMax: 22,
            speedMin: 70,
            speedMax: 140,
            sway: true,
            swayAmpMin: 2,
            swayAmpMax: 12
        },
        halloween: {
            glyphs: ["\u{1F383}", "\u{1F47B}", "\u{1F36C}", "\u{1F987}", "\u{1F480}"], // 🎃👻🍬🦇💀
            colors: "rgba(255,255,255,0.90)",
            maxParticles: 35,
            spawnEveryMs: 140,
            sizeMin: 18,
            sizeMax: 34,
            speedMin: 55,
            speedMax: 110,
            sway: true
        },
        thanksgiving: {
            glyphs: ["\u{1F983}", "\u{1F342}", "\u{1F341}", "\u{1F967}", "\u{1F33D}"], // 🎃🦃🍂🍁🥧🌽🍗
            colors: "rgba(255,255,255,0.90)",
            maxParticles: 35,
            spawnEveryMs: 160,
            sizeMin: 18,
            sizeMax: 34,
            speedMin: 60,
            speedMax: 120,
            sway: true
        },
        christmasDay: {
            glyphs: ["\u{1F381}", "\u{1F385}", "\u{1F384}", "\u{26C4}", "\u{1F384}", "\u{1F9F8}", "\u{1F385}", "\u{26C4}", "\u{1F31F}"], // 🎁🎅🏻🎄⛄🎄🧸🎅🏻⛄⭐
            colors: "rgba(255,255,255,0.92)",
            maxParticles: 45,
            spawnEveryMs: 120,
            sizeMin: 18,
            sizeMax: 34,
            speedMin: 55,
            speedMax: 115,
            sway: true
        },
        newYearBurst: {
            glyphs: ["\u2726", "\u273A", "\u2739", "\u2738", "\u{1F38A}"], // ✦ ✺ ✹ ✸ 🎊
            colors: ["rgba(255,255,255,0.9)", "rgba(255,200,80,0.9)", "rgba(160,220,255,0.9)"],
            maxParticles: 80,
            spawnEveryMs: 45,
            sizeMin: 12,
            sizeMax: 22,
            speedMin: 90,
            speedMax: 180,
            sway: true,
            swayAmpMin: 0,
            swayAmpMax: 8
        },
        birthday: {
            glyphs: ["\u{1F382}", "\u{1F381}", "\u{1F389}", "\u2728", "\u{1F973}"], // 🎂 🎁 🎉 ✨ 🥳
            colors: [
            "rgba(255,180,220,0.92)",
            "rgba(200,255,200,0.92)",
            "rgba(180,210,255,0.92)",
            "rgba(255,230,150,0.92)"
            ],
            maxParticles: 45,
            spawnEveryMs: 120,
            sizeMin: 18,
            sizeMax: 34,
            speedMin: 55,
            speedMax: 115,
            sway: true
        },
        startrekStars: {
            glyphs: ["\u2728", "\u2B50", "\u{1F30C}", "\u{1F680}", "\u{1F596}", "\u{1F6F0}\u{FE0F}"], // ✨ ⭐ 🌌 🚀 🖖 🛰️
            colors: ["rgba(200,220,255,0.92)", "rgba(255,255,255,0.92)", "rgba(180,210,255,0.92)"],
            maxParticles: 45,
            sizeMin: 16,
            sizeMax: 32,
            speedMin: 55,
            speedMax: 115,
            sway: true
            },
            may4Stars: {
            glyphs: ["\u2B50", "\u2728", "\u{1F30C}", "\u{1F680}", "\u{1F916}"], // ⭐ ✨ 🌌 🚀 🤖
            colors: ["rgba(255,230,150,0.92)", "rgba(255,255,255,0.92)", "rgba(180,210,255,0.92)"],
            maxParticles: 45,
            sizeMin: 16,
            sizeMax: 32,
            speedMin: 55,
            speedMax: 115,
            sway: true
        }
    };

    // ---------- Controller: choose current holiday + what to show ----------
    function getNextHoliday(now) {
        const y = now.getFullYear();

        // Create a list of holiday occurrences for "this year" and "next year"
        const candidates = [];
        for (const h of HOLIDAYS) {
            const dThis = h.dateForYear(y);
            const dNext = h.dateForYear(y + 1);
            candidates.push({
                h,
                date: dThis
            });
            candidates.push({
                h,
                date: dNext
            });
        }

        // Filter to future or today, then pick soonest
        candidates.sort((a, b) => a.date - b.date);

        for (const c of candidates) {
            // Accept today or future
            const diff = daysUntil(c.date, now);
            if (diff >= 0) return c;
        }
        return null;
    }

    function computeState(now) {
        const today = atMidnight(now);

        // Day-of holiday takes priority
        for (const h of HOLIDAYS) {
            const d = h.dateForYear(now.getFullYear());
            if (isSameYMD(d, today)) {
                return {
                    mode: "today",
                    holiday: h,
                    date: d
                };
            }
        }

        // Otherwise pick next holiday and see if we're in its countdown window
        const next = getNextHoliday(now);
        if (!next) return {
            mode: "inactive"
        };

        const {
            h,
            date
        } = next;
        const du = daysUntil(date, now);

        if (h.windowStartDays > 0 && du <= h.windowStartDays) {
            return {
                mode: "countdown",
                holiday: h,
                date,
                days: du
            };
        }

        return {
            mode: "inactive"
        };
    }

    // ---------- Apply UI state ----------
    function hideCountdown() {
        countdownWrap.style.display = "none";
    }

    function showCountdown() {
        countdownWrap.style.display = "block";
    }

    function setBodyHolidayClass(key) {
        // Clear prior holiday classes
        document.body.classList.forEach((c) => {
            if (c.startsWith("holiday--")) document.body.classList.remove(c);
        });
        if (key) document.body.classList.add(`holiday--${key}`);
    }

    // Decide what engine profile should run given the current time/state
    function chooseProfile(now, state) {
        // Reduced motion = no engine at all
        if (prefersReducedMotion()) return null;

        // Winter snow season (Dec 1 -> Feb 2) is the baseline
        const winter = inWinterWindow(now);

        // Day-of overrides
        if (state.mode === "today") {
            switch (state.holiday.key) {
                case "christmas":
                    return PROFILES.christmasDay;
                case "bdayjenny":
                case "bdayallie":
                    return PROFILES.birthday;
                case "startrek":
                    return PROFILES.startrekStars;
                case "may4":
                    return PROFILES.may4Stars;
                case "usmc":
                case "veterans":
                    return PROFILES.rwbConfetti;
                case "halloween":
                    return PROFILES.halloween;
                case "thanksgiving":
                    return PROFILES.thanksgiving;
                case "newyear": {
                    // 00:00–00:10 burst. If you want confetti after, we can schedule it later.
                    const hr = now.getHours();
                    const min = now.getMinutes();
                    if (hr === 0 && min < 10) return PROFILES.newYearBurst;
                    // Otherwise: winter snow if in window, else no engine
                    return winter ? PROFILES.winterSnow : null;
                }
                case "independence":
                    // Placeholder: use burst profile for now, or swap later for true fireworks
                    return PROFILES.rwbConfetti;
            }
        }

        // Countdown (month vibes)
        if (state.mode === "countdown") {
            if (state.holiday.key === "halloween") {
                // October: keep it light (emoji set but capped)
                // If you’d rather do CSS-only webs for October, return null here.
                return PROFILES.halloween;
            }
            // Otherwise: default winter snow in winter window only
            return winter ? PROFILES.winterSnow : null;
        }

        // Inactive: still run winter snow season if applicable
        return winter ? PROFILES.winterSnow : null;
    }

    function render(now) {
        const state = computeState(now);

        if (state.mode === "inactive") {
            hideCountdown();
            setBodyHolidayClass(null);
        } else if (state.mode === "countdown") {
            showCountdown();
            setBodyHolidayClass(state.holiday.key);
            titleEl.innerHTML = state.holiday.labelUntil;
            daysEl.textContent = pad2(state.days);
        } else if (state.mode === "today") {
            showCountdown();
            setBodyHolidayClass(state.holiday.key);
            titleEl.innerHTML = state.holiday.labelToday;

            if (typeof state.holiday.specialToday === "function") {
                daysEl.textContent = state.holiday.specialToday(now);
            } else {
                daysEl.textContent = HOLIDAY_EMOJI[state.holiday.key] || "00";
            }
        }

        // one call, always
        applyCountdownStyling(state);

        const profile = chooseProfile(now, state);
        if (!profile) {
            if (FallingStuffEngine.isRunning()) FallingStuffEngine.stop();
        } else {
            FallingStuffEngine.start(profile);
        }
    }


    // ---------- Scheduling ----------
    // Low-impact updates: every 10 minutes normally.
    // If we're in the first 10 minutes of New Year, update every 10 seconds.
    let timer = null;

    function scheduleOnce() {
        const now = nowDate();
        const state = computeState(now);

        let intervalMs = 10 * 60 * 1000;
        if (state.mode === "today" && state.holiday.key === "newyear") {
            if (now.getHours() === 0 && now.getMinutes() < 10) intervalMs = 10 * 1000;
        }

        setTimeout(() => {
            render(nowDate());
            scheduleOnce();
        }, intervalMs);
    }

    // Pause engine hard when the tab is hidden (extra kiosk friendliness)
    document.addEventListener("visibilitychange", () => {
        if (!document.hidden) render(nowDate());
    });

    // Boot
    document.addEventListener("DOMContentLoaded", () => {
        render(nowDate());
        scheduleOnce();
    });
})();

The CSS

Just add this to your CSS file and call it good.

/* HOLIDAY CSS */
#holiday-countdown {
  display: none;            /* JS will flip it to block */
  margin: 12px 0;
  text-align: center;
}

#holiday-countdown .go-center {
  display: block;           /* make spans behave like full-width lines */
  width: 100%;
  white-space: nowrap;      /* prevent wrapping */
}

#countdown-title {
  font-size: clamp(1.4rem, 0.5vw + 1.2rem, 2.4rem);
  color: #fba;
  letter-spacing: 0.08em;
}

#countdown-days {
  font-size: clamp(2.5rem, 2.5vw + 2rem, 7.5rem);
  color: #89f;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  margin-bottom: 35px;
  padding-bottom: 15px;
}