go("/")}>
{d.festival.logoImage
?

:
{d.festival.logoText}
}
{d.festival.logoSubtitle && (
{d.festival.logoSubtitle}
)}
setOpen(false)} />
);
}
// ── Footer ──────────────────────────────────────────────────────────────────
// ── Accent colour injector ──────────────────────────────────────────────────
function ColorInjector() {
const d = useSite();
React.useEffect(() => {
const c1 = d.festival.accentColor || "#ff5c70";
const c2 = d.festival.accentColor2 || "#784c95";
let el = document.getElementById("__site-accent");
if (!el) { el = document.createElement("style"); el.id = "__site-accent"; document.head.appendChild(el); }
el.textContent = `:root { --accent: ${c1}; --accent-bright: ${c1}; --coral: ${c1}; --coral-bright: ${c1}; --accent2: ${c2}; }`;
}, [d.festival.accentColor, d.festival.accentColor2]);
return null;
}
function Footer({ navigate }) {
const d = useSite();
const columns = d.festival.footer?.columns || [];
const handleLink = (link) => {
if (link.type === "url") {
if (link.url) window.open(link.url, "_blank", "noopener");
} else {
if (link.page) navigate(link.page);
}
};
return (
);
}
// ── Kicker row (section label with rule) ─────────────────────────────────────
function Kicker({ children, n, color }) {
// Replace § with ▸ (right-facing triangle) globally
const label = n ? String(n).replace(/§/g, "▸") : n;
return (
{label && {label}}
{children}
);
}
// ── Page transition wrapper ───────────────────────────────────────────────
function Page({ children, k }) {
return
{children};
}
// ── YouTubeThumb ─────────────────────────────────────────────────────────────
// Click-to-load YouTube embed. Shows thumbnail + play button initially; on
// click swaps to a plain youtube.com/embed iframe with autoplay. Origin is
// only passed when the page has a real http(s) origin (never "null"), since
// passing origin=null is what triggers YouTube's 153 embed error.
function YouTubeThumb({ id, title, style }) {
const [playing, setPlaying] = React.useState(false);
if (!id) return
;
if (playing) {
const o = typeof window !== "undefined" && window.location && /^https?:/.test(window.location.origin) ? window.location.origin : "";
const embedSrc = "https://www.youtube.com/embed/" + id +
"?autoplay=1&rel=0&modestbranding=1&playsinline=1" + (
o ? "&origin=" + encodeURIComponent(o) : "");
return (
);
}
return (
);
}
// ── Lightbox (global image viewer) ────────────────────────────────────────
// Call openLightbox(images, startIndex). images = array of { url, caption } or
// plain url strings. With >1 image you get next/prev arrows + keyboard nav.
function openLightbox(images, startIndex = 0) {
const norm = (images || [])
.map(im => (typeof im === "string" ? { url: im } : im))
.filter(im => im && im.url);
if (!norm.length) return;
window.dispatchEvent(new CustomEvent("lightbox:open", { detail: { images: norm, index: startIndex } }));
}
function Lightbox() {
const [state, setState] = React.useState(null); // { images, index }
React.useEffect(() => {
const onOpen = (e) => setState({ images: e.detail.images, index: e.detail.index || 0 });
window.addEventListener("lightbox:open", onOpen);
return () => window.removeEventListener("lightbox:open", onOpen);
}, []);
React.useEffect(() => {
if (!state) return;
const onKey = (e) => {
if (e.key === "Escape") setState(null);
else if (e.key === "ArrowRight") setState(s => s && { ...s, index: (s.index + 1) % s.images.length });
else if (e.key === "ArrowLeft") setState(s => s && { ...s, index: (s.index - 1 + s.images.length) % s.images.length });
};
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = prevOverflow; };
}, [state]);
if (!state) return null;
const { images, index } = state;
const cur = images[index];
const multi = images.length > 1;
const go = (dir) => setState(s => s && { ...s, index: (s.index + dir + s.images.length) % s.images.length });
return (
setState(null)}>
{multi &&
}
e.stopPropagation()}>
{(cur.caption || multi) &&
{cur.caption && {cur.caption}}
{multi && {index + 1} / {images.length}}
}
{multi &&
}
);
}
Object.assign(window, {
SiteDataProvider, useSite, usePublishedFilms, usePublishedNews,
SIcon, Frame, Nav, Footer, ColorInjector, Kicker, Page, hashStr, YouTubeThumb,
Lightbox, openLightbox
});