// site-awards.jsx — Awards Night page. Visible in nav only when awardsPage.live === true. const AW_COLORS = { Fiction: "var(--pink)", Documentary: "var(--purple)", Animation: "var(--yellow)", Experimental: "var(--cyan)", Audience: "#F0EEE9" }; const AW_TEXT = { Fiction: "#fff", Documentary: "#fff", Animation: "#16151a", Experimental: "#16151a", Audience: "#16151a" }; const AW_TROPHY = { Purple: "#784c95", Yellow: "#ffc823", Cyan: "#65c8d0", Pink: "#ff5c70", White: "#f0eee9", Red: "#d6394c", Green: "#3f7d52" }; const AW_LIGHT = new Set(["Yellow", "Cyan", "White"]); function AwardsPage({ navigate }) { const d = useSite(); const f = d.festival; const ap = f.awardsPage || {}; const awards = d.awards || []; const films = usePublishedFilms(); const people = d.people || []; const audienceWinner = films.find(fi => fi.prize === "Audience"); // Order jury winners by strand order from the schedule const _schedStrands = []; const _seen = new Set(); for (const day of (d.schedule || [])) { if (day.strand && day.strand !== "Off-night" && day.strand !== "Awards" && !_seen.has(day.strand)) { _schedStrands.push(day.strand); _seen.add(day.strand); } } // Fall back to default order if schedule is empty const _strandOrder = _schedStrands.length ? _schedStrands : ["Documentary","Animation","Experimental","Fiction"]; const juryWinners = _strandOrder .map(strand => films.find(fi => fi.prize === "Jury" && fi.strand === strand)) .filter(Boolean); const allWinners = audienceWinner ? [...juryWinners, audienceWinner] : juryWinners; const gallery = ap.gallery || []; const winnerDescs = ap.winnerDescs || {}; const juryPeople = people.filter(p => p.group === "jury"); return ( {/* ── HERO — full-bleed image ── */}
{/* String lights image */}
{/* Gradient: image visible at top → fades to ink at bottom */}
{/* Pixel texture overlay */}
{/* Content — anchored to the bottom */}
{ap.eveningLabel || ("Paper Bird Awards \u00b7 " + f.edition + " Edition")}

{ap.heading || "Awards Night."}

{(ap.pageDesc || f.awardsDesc) && (

{ap.pageDesc || f.awardsDesc}

)} {(ap.eveningDate || ap.eveningTime || ap.venueName) && (
{ap.eveningDate && (
Date
{ap.eveningDate}
)} {ap.eveningTime && (
Time
{ap.eveningTime}
)} {ap.venueName && (
Venue
{ap.venueName}
)}
)}
{/* ── EVENING DESC — yellow splash ── */} {ap.eveningDesc && (

{ap.eveningDesc}

)} {/* ── WINNERS CAROUSEL ── */} {allWinners.length > 0 && ( )} {/* ── GALLERY ── */} {gallery.filter(im => im.url).length > 0 && ( )} {/* ── VIDEO ── */} {(() => { const vids = (ap.videos || []).filter(v => v.youTubeId); const legacy = ap.videoYouTube ? [{ id: "legacy", youTubeId: ap.videoYouTube, label: ap.videoLabel || "Awards Evening · Highlights", desc: "" }] : []; const allVids = vids.length ? vids : legacy; return allVids.length > 0 ? : null; })()} {/* ── VENUE ── */} {(ap.venueName || ap.venueDesc) && (
The Venue

{ap.venueName || "The venue."}

{ap.venueAddress && (
{ap.venueAddress}
)} {ap.venueDesc && (

{ap.venueDesc}

)}
{ap.venuePhoto && (
openLightbox([{ url: ap.venuePhoto, caption: ap.venueName }])}> {ap.venueName
)}
)}
); } function PhotoCarousel({ images }) { const items = (images || []).filter(im => im.url); const total = items.length; const [cur, setCur] = React.useState(0); const [deltaX, setDeltaX] = React.useState(0); const [sliding, setSliding] = React.useState(false); const [slideDir, setSlideDir] = React.useState(null); const [incomingCenter, setIncomingCenter] = React.useState(null); const [exitingCenter, setExitingCenter] = React.useState(null); const [mobileMode, setMobileMode] = React.useState(() => window.innerWidth < 640); const trackRef = React.useRef(null); const pendingRef = React.useRef(null); const touchStartX = React.useRef(null); const onTouchStart = (e) => { touchStartX.current = e.touches[0].clientX; }; const onTouchEnd = (e) => { if (touchStartX.current === null) return; const delta = e.changedTouches[0].clientX - touchStartX.current; touchStartX.current = null; if (Math.abs(delta) < 40) return; navigate(delta < 0 ? 'next' : 'prev'); }; React.useEffect(() => { const check = () => setMobileMode(window.innerWidth < 640); window.addEventListener('resize', check, { passive: true }); return () => window.removeEventListener('resize', check); }, []); if (!total) return null; const lbItems = items.map(im => ({ url: im.url, caption: im.caption })); const idxAt = n => ((n % total) + total) % total; // Desktop: 3 cards (33.33% each). Mobile: 80% center with 10% peeks. const CARD_W = mobileMode ? 80 : 100 / 3; // BASE_X centers the middle slot (index 2 of 5) in the viewport const BASE_X = 50 - 2.5 * CARD_W; const navigate = (dir) => { if (sliding || total < 2) return; setSliding(true); setSlideDir(dir); const nextIdx = idxAt(cur + (dir === 'next' ? 1 : -1)); setIncomingCenter(nextIdx); setExitingCenter(cur); pendingRef.current = nextIdx; setDeltaX(dir === 'next' ? -CARD_W : CARD_W); }; const onTransitionEnd = () => { const track = trackRef.current; if (!track || pendingRef.current === null) return; track.style.transition = 'none'; setCur(pendingRef.current); setDeltaX(0); setSliding(false); setSlideDir(null); setIncomingCenter(null); setExitingCenter(null); pendingRef.current = null; requestAnimationFrame(() => requestAnimationFrame(() => { if (trackRef.current) trackRef.current.style.transition = ''; })); }; const slots = [-2, -1, 0, 1, 2].map(offset => ({ idx: idxAt(cur + offset), offset, isCenter: offset === 0, isSide: Math.abs(offset) === 1, })); const cardStyle = (slot) => { let op; if (slot.idx === exitingCenter && sliding) { op = 0.5; } else if (slot.idx === incomingCenter || slot.isCenter) { op = 1; } else if (Math.abs(slot.offset) === 2) { const entering = sliding && ( (slideDir === 'next' && slot.offset === 2) || (slideDir === 'prev' && slot.offset === -2) ); op = entering ? 0.5 : 0; } else { op = 0.5; } return { flexShrink: 0, width: CARD_W + '%', position: 'relative', padding: '0 clamp(4px,0.6vw,8px)', opacity: op, transition: 'opacity 0.55s cubic-bezier(0.16,1,0.3,1)', }; }; const arrowBase = { position: 'absolute', top: '50%', transform: 'translateY(-50%)', zIndex: 10, border: 'none', outline: 'none', background: 'var(--accent)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'background 0.15s', boxShadow: '0 2px 12px rgba(0,0,0,0.3)', }; // Arrow positions relative to outer wrapper (outside overflow:hidden) const prevX = mobileMode ? -12 : (CARD_W + '%'); const nextX = mobileMode ? -12 : ((CARD_W * 2) + '%'); const arwSz = mobileMode ? 36 : 48; const icnSz = mobileMode ? 14 : 18; return (
The Evening

In pictures.

{total > 1 && ( {String(cur + 1).padStart(2,'0')} / {String(total).padStart(2,'0')} )}
{/* Outer wrapper: position:relative for arrows, NO overflow:hidden */}
{/* Track clip: overflow:hidden only on this inner div */}
{slots.map((slot) => (
slot.isCenter ? openLightbox(lbItems, slot.idx) : navigate(slot.offset > 0 ? 'next' : 'prev')} style={{ aspectRatio: '3/4', overflow: 'hidden', cursor: slot.isCenter ? 'zoom-in' : 'pointer', transform: (slot.idx === exitingCenter && sliding) ? 'scale(0.92)' : (slot.idx === incomingCenter || slot.isCenter) ? 'scale(1)' : 'scale(0.92)', transition: 'transform 0.55s cubic-bezier(0.16,1,0.3,1)', }}>
{slot.isCenter && items[slot.idx].caption && (
{items[slot.idx].caption}
)}
))}
{/* Arrows — outside overflow:hidden, never clipped */} {total > 1 && ( <> )}
{total > 1 && (
{items.map((_, i) => (
)}
); } function DirectorCarousel({ bios }) { const [idx2, setIdx2] = React.useState(0); if (!bios || !bios.length) return null; const dir = bios[idx2]; const prev = () => setIdx2(i => (i - 1 + bios.length) % bios.length); const next = () => setIdx2(i => (i + 1) % bios.length); return (
{bios.length > 1 ? "Directors" : "Director"}
{bios.length > 1 && (
{String(idx2 + 1).padStart(2,"0")} / {String(bios.length).padStart(2,"0")}
)}
{dir.name}
{dir.name}
{dir.bio && (

{dir.bio}

)}
); } function WinnersCarousel({ winners, winnerDescs, navigate }) { const [active, setActive] = React.useState(0); if (!winners || !winners.length) return null; const film = winners[Math.min(active, winners.length - 1)]; const strandKey = film.prize === "Audience" ? "Audience" : film.strand; const accentHex = { Fiction: "var(--pink)", Documentary: "var(--purple)", Animation: "var(--yellow)", Experimental: "var(--cyan)", Audience: "var(--paper)" }[strandKey] || "var(--yellow)"; const juryDesc = winnerDescs[strandKey] || ""; const dirBios = film.directorBios || []; // Build carousel entries from the directors string, merging bio data where available const _dirNames = (film.directors || "").split(",").map(n => n.trim()).filter(Boolean); const _bioMap = {}; dirBios.forEach(b => { _bioMap[b.name] = b; }); const dirEntries = _dirNames.length > 1 ? _dirNames.map(name => _bioMap[name] || { name, bio: "", photo: "" }) : dirBios; const credits = film.credits || {}; const creditRows = Object.entries(credits).filter(([, v]) => v); return (
{/* heading */}
This Year’s Winners

The Paper Bird goes to.

{/* tab bar */}
{winners.map((w, i) => { const sk = w.prize === "Audience" ? "Audience" : w.strand; const on = i === active; const tbg = on ? ({ Fiction: "var(--pink)", Documentary: "var(--purple)", Animation: "var(--yellow)", Experimental: "var(--cyan)", Audience: "var(--paper)" }[sk] || "var(--yellow)") : "transparent"; const tfg = on ? ({ Fiction: "#fff", Documentary: "#fff", Animation: "#16151a", Experimental: "#16151a", Audience: "#16151a" }[sk] || "#fff") : "rgba(240,238,233,0.42)"; const tsub = on ? (tfg === "#16151a" ? "rgba(22,21,26,0.55)" : "rgba(240,238,233,0.65)") : "rgba(240,238,233,0.25)"; return ( ); })}
{/* film panel */}
{/* poster 2:3 */}
{film.title}
{film.prize}
{/* credits panel */}
{/* award kicker */}
Paper Bird · {strandKey === "Audience" ? "Audience Award" : strandKey + " · Jury Award"}
{/* film title */}

{film.title}

{/* meta strip */}
{film.country && {film.country}} {film.runtime && {film.runtime}'{String(film.runtimeSecs || "0").padStart(2,"0")}"} {film.school && {film.school}}
{/* jury statement OR synopsis */} {(juryDesc || film.synopsis) && (
Jury Statement

{juryDesc ? ("“" + juryDesc + "”") : film.synopsis}

)} {/* credits grid — film-credits style */} {creditRows.length > 0 && (
{creditRows.map(([role, name], i) => (
{role}
{name}
))}
)} {/* director carousel */} {dirEntries.length > 0 && (
)} {/* view film */}
); } function AwardsVideoCarousel({ videos, ap }) { const [active, setActive] = React.useState(0); const [shown, setShown] = React.useState(0); const [videoKey, setVideoKey] = React.useState(0); const [exitStyle, setExitStyle] = React.useState(null); const [enterAnim, setEnterAnim] = React.useState(null); const [textVisible, setTextVisible] = React.useState(true); const inFlight = React.useRef(false); const navigate = (dir) => { if (inFlight.current) return; inFlight.current = true; const next = (active + (dir === 'next' ? 1 : -1) + videos.length) % videos.length; const exitTo = dir === 'next' ? 'left' : 'right'; const enterFrom = dir === 'next' ? 'from-right' : 'from-left'; setTextVisible(false); setExitStyle(exitTo); setTimeout(() => { setActive(next); setShown(next); setVideoKey(k => k + 1); setExitStyle(null); setEnterAnim(enterFrom); setTimeout(() => setTextVisible(true), 80); setTimeout(() => { setEnterAnim(null); inFlight.current = false; }, 550); }, 390); }; const vid = videos[shown]; const hasMultiple = videos.length > 1; const vidTouchX = React.useRef(null); const onVidTouchStart = (e) => { vidTouchX.current = e.touches[0].clientX; }; const onVidTouchEnd = (e) => { if (vidTouchX.current === null) return; const delta = e.changedTouches[0].clientX - vidTouchX.current; vidTouchX.current = null; if (Math.abs(delta) < 40 || !hasMultiple) return; navigate(delta < 0 ? 'next' : 'prev'); }; const videoWrapStyle = exitStyle ? { transform: exitStyle === 'right' ? "translateX(55%)" : "translateX(-55%)", opacity: 0, transition: "transform 0.39s ease, opacity 0.33s ease" } : { transform: "translateX(0)", opacity: 1 }; const enterAnimStyle = enterAnim ? { animation: enterAnim === 'from-left' ? "awards-from-left 0.47s ease forwards" : "awards-from-right 0.47s ease forwards" } : {}; const arrowBtn = { width: 48, height: 48, borderRadius: 0, background: "var(--accent)", border: "none", outline: "none", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", overflow: "hidden", position: "absolute", top: "50%", transform: "translateY(-50%)", zIndex: 4, transition: "background 0.15s", }; return (
{/* Video + arrows */}
{hasMultiple && ( )} {hasMultiple && ( )}
{/* Text */}
{(vid.eyebrow || ap.eveningLabel) && (
{vid.eyebrow || ap.eveningLabel}
)}

{vid.title || vid.label || "Awards Evening Highlights."}

{vid.desc && (

{vid.desc}

)} {vid.buttonLabel && ( { e.currentTarget.style.background = "#fff"; e.currentTarget.style.color = "var(--ink)"; }} onMouseLeave={e => { e.currentTarget.style.background = "var(--accent)"; e.currentTarget.style.color = "#fff"; }}> {vid.buttonLabel} )} {hasMultiple && (
{String(active + 1).padStart(2,"0")} / {String(videos.length).padStart(2,"0")}
)}
); } Object.assign(window, { AwardsPage });