New
SolsticeExperiences
A prom-night invitation on a scroll-scrubbed film: a single 30s take runs dusk to a confetti finale as full-screen beats fade through the frame, then the film releases and loops at the bottom while a silver bloom crests on the RSVP. Below the fold, a midnight-and-moonlight invitation — a gold-threaded timeline of the evening, tickets and details. Ships the hero film and five graded stills; reduced-motion gets a static fallback.
import { AfterglowView } from "./afterglow-view"
import "./afterglow.css"
/* -------------------------------------------------------------------------
AFTERGLOW — a prom-night invitation on a scroll-scrubbed film.
One continuous 30s take, dusk → confetti finale, and the page rides its
light arc: a midnight-and-moonlight night that builds to a single silver
bloom on the closing RSVP. When the scroll bottoms out the film releases
and plays on a loop — the night keeps living; scrolling back up hands
control to the scrub again.
A whole page, not a component: `.pnRoot` is the single wrapper every rule
in afterglow.css hangs off, so the sheet touches nothing else in the app.
The film and the six stills ship as real bytes under /afterglow/.
------------------------------------------------------------------------- */
export const metadata = {
title: "Afterglow — the prom",
description:
"A prom-night invitation on a scroll-scrubbed film: dusk to a confetti finale, one night the whole year has been leaning toward.",
}
export default function AfterglowPage() {
return <AfterglowView />
}"use client"
import { useEffect, useRef, useState } from "react"
/* -------------------------------------------------------------------------
AFTERGLOW — prom-night view.
MECHANIC (same family as ../sillage-lumen): dependency-free native
`video.currentTime` scrubbing eased by a requestAnimationFrame loop toward
a scroll-derived target, CSS sticky stage, duration captured with the
cached-load readyState guard, prefers-reduced-motion static fallback.
TWO THINGS THAT SET IT APART:
1. It keeps the warm film WARM — a champagne grade, not a cool one — and
the one luminous moment (a golden bloom) lands at the END, on the
confetti finale, not the centre.
2. When the scroll bottoms out, the film stops scrubbing and PLAYS ON A
LOOP — the night keeps living. Scrolling back up hands control to the
scrub again.
Panels FADE THROUGH (never crossfade): each dissolves fully to the film
before the next appears, so two centred lines never stack.
------------------------------------------------------------------------- */
type Beat = {
key: string
note: string
title: string
body: string
still: string
}
const HERO = {
eyebrow: "you're invited",
wordmark: "Afterglow",
tagline: "one night the whole year has been leaning toward",
meta: "Senior Prom, Saturday the sixteenth of May. Doors open at eight."
}
/* the arc of the evening, riding the film's real light beats:
arrival (blue hour) → the floor (warm peak) → the slow ones (dark warm) */
const BEATS: Beat[] = [
{
key: "arrival",
note: "half past eight",
title: "The arrival",
body: "Headlights on the drive, a held breath at the door. The room goes quiet for a second when you walk in — then it doesn't.",
still: "/afterglow/still-bluehour.jpg"
},
{
key: "floor",
note: "somewhere near eleven",
title: "The floor",
body: "The lights drop, the bass finds your chest, and the whole grade is one crowd. This is the part you'll be talking about for years.",
still: "/afterglow/still-dance.jpg"
},
{
key: "slow",
note: "the last few songs",
title: "The slow ones",
body: "When the tempo finally falls and the room softens to gold. Whoever you came with, this is the one you stay for.",
still: "/afterglow/still-slow.jpg"
}
]
const CLOSING = {
line: "Come be unforgettable.",
cta: "reserve your spot"
}
/* ---- content below the film: the complete invitation ------------------- */
const THEME = {
title: "A golden hour that doesn't end",
body:
"Afterglow is the light just after sunset — warm, gold, gone too soon, the one you always wish you'd stayed in longer. That's the night we're building: string lights and slow songs, the whole class in one room, an evening that keeps glowing long after you've gone home.",
figure: "/afterglow/still-confetti.jpg"
}
/* a real sequence, so the clock earns its place as the structure */
type Moment = { time: string; title: string; detail: string }
const EVENING: Moment[] = [
{ time: "8:00", title: "Doors & red carpet", detail: "Arrivals, the photo wall, and a welcome mocktail bar." },
{ time: "8:45", title: "Dinner is served", detail: "A plated dinner with vegetarian and halal options." },
{ time: "9:30", title: "The floor opens", detail: "Live DJ set. Requests taken all night at the booth." },
{ time: "11:00", title: "Court & crowning", detail: "This year's prom court announced under the lights." },
{ time: "11:30", title: "The slow set", detail: "The tempo falls. Grab someone. Stay for it." },
{ time: "Midnight", title: "Confetti finale", detail: "The whole room, one last song, and a ceiling full of gold." }
]
/* the immersive breath between the schedule and the practicalities */
const BAND = {
still: "/afterglow/still-slow.jpg",
line: "Long after the lights come up, this is the part that stays with you."
}
const DETAILS = [
{ label: "When", value: "Saturday, May 16", sub: "Doors at eight, dancing till midnight" },
{ label: "Where", value: "The Grand Marigold", sub: "1400 Ellery Ave, downtown" },
{ label: "Dress", value: "Black tie, your way", sub: "Gowns, suits, however you shine" },
{ label: "Tickets", value: "$65 single", sub: "$120 a pair, on sale till May 9" }
]
const TICKETS = {
title: "Get your tickets",
body: "Sold through the front office and here online until May 9, or while they last. Tables of ten can be reserved together — bring the whole group.",
tiers: [
{ name: "Single", price: "$65", detail: "one ticket, one unforgettable night" },
{ name: "Pair", price: "$120", detail: "two tickets, save fifteen" },
{ name: "Table of ten", price: "$560", detail: "reserve together, sit together" }
],
cta: "reserve your spot"
}
const FOOTER = {
statement: "Thrown by the Senior Class Committee, for the class that made it.",
columns: [
{ head: "The night", links: ["Theme", "The evening", "Details", "Tickets"] },
{ head: "Good to know", links: ["Dress code", "Getting there", "Safe rides", "Accessibility"] },
{ head: "Ask us", links: ["Committee", "Email", "Instagram", "Front office"] }
],
reminderNote: "We'll send one reminder before the night. Nothing else.",
fine: "© Afterglow. Organised by the Senior Class Committee."
}
/** smoothstep — soft ends so the beats trade cleanly */
function smooth(t: number) {
const x = Math.min(1, Math.max(0, t))
return x * x * (3 - 2 * x)
}
export function AfterglowView() {
const [reduced, setReduced] = useState(false)
const [decided, setDecided] = useState(false)
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)")
const apply = () => setReduced(mq.matches)
apply()
setDecided(true)
mq.addEventListener?.("change", apply)
return () => mq.removeEventListener?.("change", apply)
}, [])
if (decided && reduced) return <PromStatic />
return <PromMotion />
}
/* ---- motion mode -------------------------------------------------------- */
/** past this scroll progress the finale has arrived — let the film play/loop */
const END = 0.995
function PromMotion() {
const stageRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const panelsRef = useRef<HTMLDivElement>(null)
const bloomRef = useRef<HTMLDivElement>(null)
const cueRef = useRef<HTMLDivElement>(null)
const durationRef = useRef(0)
const targetTimeRef = useRef(0)
const rafRef = useRef(0)
const runningRef = useRef(false)
const idleFramesRef = useRef(0)
const playingRef = useRef(false) // true once the finale is playing on a loop
useEffect(() => {
const video = videoRef.current
const stage = stageRef.current
if (!video || !stage) return
const captureDuration = () => {
const d = video.duration
if (Number.isFinite(d) && d > 0) durationRef.current = d
}
if (video.readyState >= 1) captureDuration()
video.addEventListener("loadedmetadata", captureDuration)
const tick = () => {
const dur = durationRef.current
const vh = window.innerHeight
const rect = stage.getBoundingClientRect()
const travel = rect.height - vh
const progress = travel > 0 ? Math.min(1, Math.max(0, -rect.top / travel)) : 0
// ---- video: scrub to scroll, then PLAY + LOOP once the finale lands ----
// At the bottom of the scrub the film would otherwise freeze on its last
// frame. Instead we let it play on its own in a loop — the confetti keeps
// falling, the night keeps living. Scrolling back up drops progress below
// END, so we pause, snap the frame to the scroll position, and hand
// control back to the scrub.
if (dur > 0) {
if (progress >= END) {
if (!playingRef.current) {
playingRef.current = true
video.loop = true
const played = video.play()
if (played && typeof played.catch === "function") played.catch(() => {})
}
idleFramesRef.current += 1 // native playback advances the film; rAF may idle
} else {
if (playingRef.current) {
playingRef.current = false
video.loop = false
video.pause()
try {
video.currentTime = Math.min(dur - 0.05, Math.max(0, progress * dur))
} catch {
/* re-seek on the next tick */
}
}
const target = Math.min(dur - 0.05, progress * dur)
targetTimeRef.current = target
const cur = video.currentTime
const diff = target - cur
if (Math.abs(diff) > 0.012) {
const next = cur + diff * 0.18
if (video.readyState >= 1 && !video.seeking) {
try {
video.currentTime = Math.min(dur - 0.05, Math.max(0, next))
} catch {
/* seeking can throw mid-load; the next tick retries */
}
}
idleFramesRef.current = 0
} else {
idleFramesRef.current += 1
}
}
}
// ---- panels FADE THROUGH — one line on screen at a time ----
// Every panel is full-screen centred text, so two visible at once read as
// a double-exposure. The curve holds a panel full near its centre, then
// fades it to EXACTLY 0 by the midpoint to its neighbour (d = 0.5): at
// each hand-off only the graded film shows for a beat, then the next
// words emerge.
const panels = panelsRef.current?.children
if (panels) {
const n = panels.length
const seg = 1 / n
for (let i = 0; i < panels.length; i++) {
const el = panels[i] as HTMLElement
const center = (i + 0.5) * seg
const d = Math.abs(progress - center) / seg
let o = 1 - smooth((d - 0.34) / 0.16)
if (i === 0 && progress < center) o = 1
if (i === n - 1 && progress > center) o = 1
el.style.opacity = o.toFixed(3)
el.style.transform = `translate3d(0, ${((1 - o) * 16).toFixed(1)}px, 0)`
el.style.visibility = o < 0.01 ? "hidden" : "visible"
}
}
// ---- THE signature: one golden bloom, centred on the FINALE ----
// Panels are [hero, arrival, floor, slow, closing] → closing centre = 0.9.
// The confetti peaks at ~90% too, so the bloom, the film, and the RSVP
// all crest together. A resting ember keeps it alive before then.
const bloom = bloomRef.current
if (bloom) {
const dist = Math.abs(progress - 0.9) / 0.26
const hump = 1 - smooth(dist)
const intensity = 0.1 + hump * 0.9
bloom.style.opacity = intensity.toFixed(3)
bloom.style.transform = `translate(-50%, -50%) scale(${(0.8 + hump * 0.5).toFixed(3)})`
}
if (cueRef.current) {
cueRef.current.style.opacity = Math.max(0, 1 - progress * 9).toFixed(3)
}
if (idleFramesRef.current < 6) {
rafRef.current = requestAnimationFrame(tick)
} else {
runningRef.current = false
}
}
const kick = () => {
idleFramesRef.current = 0
if (!runningRef.current) {
runningRef.current = true
rafRef.current = requestAnimationFrame(tick)
}
}
kick()
video.addEventListener("loadeddata", kick)
window.addEventListener("scroll", kick, { passive: true })
window.addEventListener("resize", kick)
return () => {
cancelAnimationFrame(rafRef.current)
runningRef.current = false
window.removeEventListener("scroll", kick)
window.removeEventListener("resize", kick)
video.removeEventListener("loadedmetadata", captureDuration)
video.removeEventListener("loadeddata", kick)
}
}, [])
return (
<div className="pnRoot" id="top">
<div className="pnStage" ref={stageRef}>
<div className="pnSticky">
<video
className="pnVideo"
ref={videoRef}
src="/afterglow/journey.mp4"
poster="/afterglow/poster.jpg"
muted
playsInline
preload="auto"
aria-hidden="true"
/>
{/* keep the warm film warm: a champagne wash + a deepening vignette */}
<div className="pnWarm" aria-hidden="true" />
<div className="pnDeepen" aria-hidden="true" />
{/* the one luminous moment — a golden bloom behind the finale */}
<div className="pnBloom" ref={bloomRef} aria-hidden="true" />
<div className="pnPanels" ref={panelsRef}>
<section className="pnPanel pnPanel--hero" aria-label="Afterglow">
<div className="pnPanelInner">
<p className="pnEyebrow">{HERO.eyebrow}</p>
<h1 className="pnWordmark">{HERO.wordmark}</h1>
<p className="pnTagline">{HERO.tagline}</p>
<p className="pnMeta">{HERO.meta}</p>
</div>
</section>
{BEATS.map((b) => (
<section
className={`pnPanel pnPanel--${b.key}`}
key={b.key}
aria-label={b.title}
>
<div className="pnPanelInner">
<p className="pnNote">{b.note}</p>
<h2 className="pnTitle">{b.title}</h2>
<p className="pnBody">{b.body}</p>
</div>
</section>
))}
<section className="pnPanel pnPanel--closing" aria-label="Come be unforgettable">
<div className="pnPanelInner">
<p className="pnClosing">{CLOSING.line}</p>
<a className="pnCta" href="#tickets">
{CLOSING.cta}
</a>
</div>
</section>
</div>
<div className="pnCue" ref={cueRef} aria-hidden="true">
<span className="pnCueWord">scroll into the night</span>
<span className="pnCueLine" />
</div>
</div>
</div>
<PromSite />
</div>
)
}
/* ---- reduced-motion / static mode --------------------------------------- */
function PromStatic() {
return (
<div className="pnRoot pnRoot--static" id="top">
<header className="pnHeroStatic">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="pnHeroStaticImg" src="/afterglow/poster.jpg" alt="" />
<div className="pnWarm" aria-hidden="true" />
<div className="pnDeepen" aria-hidden="true" />
<div className="pnPanelInner pnHeroStaticInner">
<p className="pnEyebrow">{HERO.eyebrow}</p>
<h1 className="pnWordmark">{HERO.wordmark}</h1>
<p className="pnTagline">{HERO.tagline}</p>
<p className="pnMeta">{HERO.meta}</p>
</div>
</header>
<div className="pnSections">
{BEATS.map((b) => (
<section
className={`pnSection pnSection--${b.key}`}
key={b.key}
aria-label={b.title}
>
<figure className="pnSectionFig">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={b.still} alt={`${b.title}, ${b.note}`} loading="lazy" decoding="async" />
<span className="pnSectionGrade" aria-hidden="true" />
</figure>
<div className="pnSectionText">
<p className="pnNote">{b.note}</p>
<h2 className="pnTitle">{b.title}</h2>
<p className="pnBody">{b.body}</p>
</div>
</section>
))}
</div>
<section className="pnClosingStatic" aria-label="Come be unforgettable">
<span className="pnBloom pnBloom--static" aria-hidden="true" />
<p className="pnClosing">{CLOSING.line}</p>
<a className="pnCta" href="#tickets">
{CLOSING.cta}
</a>
</section>
<PromSite />
</div>
)
}
/* ---- the complete invitation, below the hero (shared by both modes) ----- *
An immersive warm DESCENT, not a drop to flat black. The page stays lit by
the same golden light as the film (soft ambient glows on the ground); one
full-bleed graded still carries the film's world down as a quiet breath; the
evening runs as a real gold-threaded timeline. No eyebrow labels, no dotted
meta strings, no numbered chrome except the evening's own clock. The hero
film is the one spectacle — everything here is quiet, but richly lit. */
function PromSite() {
return (
<div className="pnSite">
{/* warm ambient — the party's light spilling onto the page */}
<div className="pnAmbient" aria-hidden="true" />
{/* THE THEME — what the night is */}
<section className="pnTheme" id="theme" aria-label="This year's theme">
<figure className="pnThemeFig">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={THEME.figure} alt="The dance floor under string lights" loading="lazy" decoding="async" />
<span className="pnSectionGrade" aria-hidden="true" />
</figure>
<div className="pnThemeText">
<h2 className="pnThemeTitle">{THEME.title}</h2>
<p className="pnThemeBody">{THEME.body}</p>
</div>
</section>
{/* THE EVENING — a real timed sequence, on a gold thread */}
<section className="pnEvening" id="evening" aria-label="The evening">
<div className="pnEveningHead">
<h2 className="pnBigTitle">The evening</h2>
<p className="pnEveningLede">
From the first pair of headlights on the drive to a ceiling full of gold — how the night unfolds.
</p>
</div>
<ol className="pnTimeline">
{EVENING.map((m, i) => (
<li
className={`pnMoment${i === EVENING.length - 1 ? " pnMoment--finale" : ""}`}
key={m.title}
>
<span className="pnMomentTime">{m.time}</span>
<div className="pnMomentText">
<h3 className="pnMomentTitle">{m.title}</h3>
<p className="pnMomentDetail">{m.detail}</p>
</div>
</li>
))}
</ol>
</section>
{/* THE BREATH — one full-bleed graded still, a line of voice */}
<section className="pnBand" aria-label="The part that stays">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="pnBandImg" src={BAND.still} alt="" loading="lazy" decoding="async" />
<span className="pnBandScrim" aria-hidden="true" />
<p className="pnBandLine">{BAND.line}</p>
</section>
{/* THE DETAILS — when / where / dress / tickets, read plainly */}
<section className="pnDetails" id="details" aria-label="The details">
<dl className="pnDetailGrid">
{DETAILS.map((d) => (
<div className="pnDetail" key={d.label}>
<dt className="pnDetailLabel">{d.label}</dt>
<dd className="pnDetailValue">{d.value}</dd>
<dd className="pnDetailSub">{d.sub}</dd>
</div>
))}
</dl>
</section>
{/* TICKETS — the CTA, three tiers, one action, in a warm glow */}
<section className="pnTickets" id="tickets" aria-label="Tickets">
<span className="pnTicketsGlow" aria-hidden="true" />
<div className="pnTicketsHead">
<h2 className="pnBigTitle">{TICKETS.title}</h2>
<p className="pnTicketsBody">{TICKETS.body}</p>
</div>
<ul className="pnTiers">
{TICKETS.tiers.map((t) => (
<li className="pnTier" key={t.name}>
<p className="pnTierName">{t.name}</p>
<p className="pnTierPrice">{t.price}</p>
<p className="pnTierDetail">{t.detail}</p>
</li>
))}
</ul>
<button className="pnBuy" type="button">
{TICKETS.cta}
</button>
</section>
{/* FOOTER */}
<footer className="pnFooter" aria-label="Footer">
<div className="pnFootTop">
<div className="pnFootBrand">
<span className="pnFootMark">Afterglow</span>
<p className="pnFootStatement">{FOOTER.statement}</p>
</div>
<nav className="pnFootNav">
{FOOTER.columns.map((col) => (
<div className="pnFootCol" key={col.head}>
<p className="pnFootHead">{col.head}</p>
<ul>
{col.links.map((l) => (
<li key={l}>
<a href="#tickets">{l}</a>
</li>
))}
</ul>
</div>
))}
</nav>
</div>
<form
className="pnSignup"
aria-label="Prom reminder"
onSubmit={(e) => e.preventDefault()}
>
<label className="pnSignupLabel" htmlFor="pn-email">
{FOOTER.reminderNote}
</label>
<div className="pnSignupRow">
<input
id="pn-email"
className="pnSignupInput"
type="email"
inputMode="email"
autoComplete="email"
placeholder="your email"
/>
<button className="pnSignupBtn" type="submit">
remind me
</button>
</div>
</form>
<p className="pnFine">{FOOTER.fine}</p>
</footer>
</div>
)
}/* -------------------------------------------------------------------------
AFTERGLOW — prom night. MIDNIGHT & MOONLIGHT: a cool sapphire night lit by
one silver-blue moon-light.
Everything is prefixed `pn` and scoped under `.pnRoot`. No rule here touches
html, body, :root, or any bare element outside `.pnRoot`; the background is
set on `.pnRoot` only — so this shares the bare origin with its neighbours
without either leaking.
The source film is warm (amber dance-floor, string lights). We FIGHT IT
COOL — desaturate hard, then recolour with a sapphire `color`-blend layer,
the same recipe SILLAGE·LUMEN used — so the room reads as moonlit blue, not
gold. Boldness is spent in exactly ONE place: the icy silver-blue bloom that
swells behind the closing RSVP as the finale crests. Below the film the page
does NOT drop to flat black — it keeps a cool ambient moonlight.
(Class names are unchanged from the warm build — `.pnWarm` etc. are just
hooks; here `.pnWarm` carries the COOL grade. The view file is untouched.)
------------------------------------------------------------------------- */
.pnRoot {
/* --- palette: deep sapphire night + a single moonlit silver-blue --- */
--pn-night: #070d1f; /* deep sapphire near-black — the night */
--pn-night-2: #0d1730; /* a half-tone up, for seams + footer */
--pn-accent: #b9d0ff; /* the one accent — moonlit silver-blue */
--pn-accent-hi: #eaf2ff; /* icy white — the hot core of the light */
--pn-accent-soft: rgba(185, 208, 255, 0.5);
--pn-ivory: #eef3fb; /* cool white — the type */
--pn-ivory-dim: rgba(238, 243, 251, 0.74);
--pn-ivory-faint: rgba(238, 243, 251, 0.46);
/* cool near-black rgb, for scrims + vignettes */
--pn-sink: 4, 8, 18;
--pn-gut: clamp(1.35rem, 6vw, 4rem);
--pn-measure: 40ch;
box-sizing: border-box;
min-height: 100vh;
background: var(--pn-night);
color: var(--pn-ivory);
color-scheme: dark;
font-family: var(--font-geist), "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: clamp(0.95rem, 1.5vw, 1.0625rem);
font-weight: 400;
line-height: 1.6;
letter-spacing: -0.003em;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.pnRoot *,
.pnRoot *::before,
.pnRoot *::after {
box-sizing: inherit;
}
.pnRoot :where(h1, h2, h3, p, ol, ul, dl, figure, figcaption) {
margin: 0;
}
.pnRoot :where(a) {
color: inherit;
text-decoration: none;
}
.pnRoot :where(a):focus-visible,
.pnRoot :where(button):focus-visible,
.pnRoot :where(input):focus-visible,
.pnRoot :where(video):focus-visible {
outline: 2px solid var(--pn-accent);
outline-offset: 4px;
border-radius: 2px;
}
/* --------------------------------------------------------------------- *
MOTION MODE — sticky stage, scroll-scrubbed film
* --------------------------------------------------------------------- */
/* 5 panels across the scroll → ~520vh, matching the SILLAGE cadence */
.pnStage {
position: relative;
height: 520vh;
}
.pnSticky {
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
background: var(--pn-night);
}
/* the film, full-bleed. Its native grade is warm/amber; we darken AND
desaturate hard here, so the sapphire tint layer above can recolour it to
moonlight. */
.pnVideo {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
filter: brightness(0.6) saturate(0.48) contrast(1.08);
}
/* GRADE 1 — a sapphire wash. `color` blend takes this layer's blue hue over
the (now desaturated) film, pulling the whole frame to moonlight. */
.pnWarm {
position: absolute;
inset: 0;
z-index: 1;
background: linear-gradient(180deg, #1e3f7a 0%, #163063 46%, #0e2148 100%);
mix-blend-mode: color;
opacity: 0.66;
pointer-events: none;
}
/* GRADE 2 — sink the frame into the sapphire night: a soft central veil for
type legibility, deepening hard to cool near-black at the edges. */
.pnDeepen {
position: absolute;
inset: 0;
z-index: 2;
background:
radial-gradient(
130% 100% at 50% 44%,
rgba(var(--pn-sink), 0.24) 0%,
rgba(var(--pn-sink), 0.5) 52%,
rgba(var(--pn-sink), 0.86) 100%
),
linear-gradient(180deg, rgba(var(--pn-sink), 0.48) 0%, rgba(var(--pn-sink), 0) 26%);
pointer-events: none;
}
/* THE SIGNATURE — one moonlight bloom. Icy silver-blue radial light that
swells behind the type as the finale crests (~90% scroll). `screen` adds
light to the night; JS drives opacity + scale from the scrub position. */
.pnBloom {
position: absolute;
left: 50%;
top: 50%;
z-index: 3;
width: min(155vh, 152vw);
height: min(155vh, 152vw);
transform: translate(-50%, -50%) scale(0.8);
border-radius: 50%;
background: radial-gradient(
circle at center,
rgba(234, 242, 255, 0.92) 0%,
rgba(185, 208, 255, 0.46) 22%,
rgba(126, 160, 232, 0.18) 44%,
rgba(70, 104, 176, 0.06) 62%,
rgba(7, 13, 31, 0) 74%
);
mix-blend-mode: screen;
filter: blur(16px);
opacity: 0.1;
pointer-events: none;
will-change: opacity, transform;
}
/* panels, stacked and centred; JS FADES THEM THROUGH on their scroll centres */
.pnPanels {
position: absolute;
inset: 0;
z-index: 4;
}
.pnPanel {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 12vh var(--pn-gut) 14vh;
will-change: opacity, transform;
/* PRE-JS STATE — the scrub loop assigns each panel's opacity from scroll
position, but until it first runs (SSR'd markup, the paint before
hydration, a resting preview frame) every panel would otherwise sit at
opacity 1 and pile on top of the hero. Start hidden; the hero opts back
in below, so the resting state already matches scroll progress 0. */
opacity: 0;
visibility: hidden;
}
.pnPanel--hero {
opacity: 1;
visibility: visible;
}
.pnPanelInner {
width: 100%;
max-width: var(--pn-measure);
text-align: center;
}
/* --- hero --- */
.pnEyebrow {
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(1.05rem, 2.6vw, 1.5rem);
line-height: 1.3;
color: var(--pn-accent);
letter-spacing: 0.004em;
text-shadow: 0 1px 20px rgba(var(--pn-sink), 0.7);
}
.pnWordmark {
margin-top: clamp(0.6rem, 1.6vw, 1rem);
font-family: var(--font-fraunces), Georgia, "Times New Roman", serif;
font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 0;
font-weight: 330;
font-size: clamp(3.4rem, 16vw, 10.5rem);
line-height: 0.9;
letter-spacing: -0.02em;
color: var(--pn-ivory);
/* a faint moonlit halo over the film, not decoration */
text-shadow: 0 1px 64px rgba(185, 208, 255, 0.2), 0 2px 30px rgba(var(--pn-sink), 0.6);
}
.pnTagline {
margin-top: clamp(1.1rem, 2.4vw, 1.9rem);
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(1.15rem, 3vw, 1.7rem);
line-height: 1.4;
letter-spacing: 0.002em;
color: var(--pn-ivory);
text-wrap: balance;
text-shadow: 0 1px 24px rgba(var(--pn-sink), 0.7);
}
.pnMeta {
margin-top: clamp(1.6rem, 3.4vw, 2.6rem);
font-size: clamp(0.85rem, 1.6vw, 1rem);
line-height: 1.6;
color: var(--pn-ivory-dim);
text-shadow: 0 1px 18px rgba(var(--pn-sink), 0.7);
}
/* --- beats (arrival / floor / slow) --- */
.pnNote {
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(1rem, 2.3vw, 1.35rem);
line-height: 1.4;
color: var(--pn-accent);
letter-spacing: 0.004em;
}
.pnTitle {
margin-top: clamp(0.5rem, 1.4vw, 0.9rem);
font-family: var(--font-fraunces), Georgia, "Times New Roman", serif;
font-variation-settings: "opsz" 72, "SOFT" 0, "WONK" 0;
font-weight: 360;
font-size: clamp(2.3rem, 8.5vw, 5rem);
line-height: 0.98;
letter-spacing: -0.015em;
color: var(--pn-ivory);
text-wrap: balance;
text-shadow: 0 2px 40px rgba(var(--pn-sink), 0.6);
}
.pnBody {
margin: clamp(1.1rem, 2.6vw, 1.7rem) auto 0;
max-width: 34ch;
font-size: clamp(1rem, 1.9vw, 1.15rem);
line-height: 1.65;
color: var(--pn-ivory-dim);
text-wrap: pretty;
text-shadow: 0 1px 22px rgba(var(--pn-sink), 0.72);
}
/* --- closing (where the bloom crests) --- */
.pnClosing {
font-family: var(--font-fraunces), Georgia, "Times New Roman", serif;
font-variation-settings: "opsz" 96, "SOFT" 0, "WONK" 0;
font-weight: 340;
font-size: clamp(2rem, 7vw, 4rem);
line-height: 1.08;
letter-spacing: -0.014em;
color: var(--pn-ivory);
text-wrap: balance;
/* the bloom is behind it — give the type a cool halo to sit in the light */
text-shadow: 0 0 52px rgba(200, 222, 255, 0.36), 0 2px 40px rgba(var(--pn-sink), 0.5);
}
.pnCta {
display: inline-block;
margin-top: clamp(1.8rem, 4vw, 2.8rem);
padding: 0.85rem 2.2rem;
font-size: clamp(0.9rem, 1.7vw, 1rem);
letter-spacing: 0.04em;
color: var(--pn-night);
background: var(--pn-accent);
border: 1px solid var(--pn-accent);
border-radius: 2px;
transition: background-color 0.4s ease, color 0.4s ease, transform 0.4s ease;
}
.pnCta:hover {
background: var(--pn-accent-hi);
border-color: var(--pn-accent-hi);
}
/* --- scroll cue --- */
.pnCue {
position: absolute;
left: 50%;
bottom: clamp(1.6rem, 4vh, 3rem);
transform: translateX(-50%);
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.9rem;
color: var(--pn-ivory-faint);
pointer-events: none;
}
.pnCueWord {
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(0.85rem, 1.6vw, 0.98rem);
letter-spacing: 0.01em;
}
.pnCueLine {
width: 1px;
height: clamp(2.2rem, 6vh, 3.4rem);
background: linear-gradient(180deg, var(--pn-accent-soft), rgba(185, 208, 255, 0));
}
/* --------------------------------------------------------------------- *
REDUCED-MOTION / STATIC MODE — poster hero + stills, bloom preserved
* --------------------------------------------------------------------- */
.pnHeroStatic {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 12vh var(--pn-gut);
overflow: hidden;
}
.pnHeroStaticImg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
filter: brightness(0.6) saturate(0.48) contrast(1.08);
}
.pnHeroStaticInner {
position: relative;
z-index: 4;
}
.pnSections {
background: linear-gradient(180deg, var(--pn-night) 0%, var(--pn-night-2) 50%, var(--pn-night) 100%);
}
.pnSection {
max-width: 74rem;
margin: 0 auto;
padding: clamp(4rem, 12vh, 9rem) var(--pn-gut);
display: grid;
gap: clamp(1.8rem, 5vw, 4rem);
align-items: center;
}
@media (min-width: 820px) {
.pnSection {
grid-template-columns: 1fr 1fr;
}
}
.pnSectionFig {
position: relative;
aspect-ratio: 4 / 5;
overflow: hidden;
border-radius: 2px;
}
.pnSectionFig img {
width: 100%;
height: 100%;
object-fit: cover;
filter: brightness(0.68) saturate(0.5) contrast(1.06);
}
/* recolour the warm stills to moonlight with a sapphire `color` blend */
.pnSectionGrade {
position: absolute;
inset: 0;
background: linear-gradient(180deg, #1e3f7a 0%, #0e2148 100%);
mix-blend-mode: color;
opacity: 0.62;
pointer-events: none;
}
/* the signature survives without motion: a CSS-only moonlight bloom */
.pnBloom--static {
position: absolute;
left: 50%;
top: 50%;
width: 150%;
height: 150%;
transform: translate(-50%, -50%);
border-radius: 50%;
background: radial-gradient(
circle at center,
rgba(234, 242, 255, 0.82) 0%,
rgba(185, 208, 255, 0.4) 24%,
rgba(126, 160, 232, 0.14) 46%,
rgba(7, 13, 31, 0) 66%
);
mix-blend-mode: screen;
filter: blur(12px);
opacity: 1;
pointer-events: none;
}
.pnSectionText {
max-width: var(--pn-measure);
}
.pnSection .pnBody {
margin-left: 0;
margin-right: 0;
text-shadow: none;
}
.pnSection .pnNote,
.pnSection .pnTitle,
.pnClosingStatic .pnClosing {
text-shadow: none;
}
.pnClosingStatic {
position: relative;
min-height: 70vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: clamp(4rem, 14vh, 10rem) var(--pn-gut);
overflow: hidden;
}
.pnClosingStatic .pnClosing,
.pnClosingStatic .pnCta {
position: relative;
z-index: 1;
}
/* ===================================================================== *
THE COMPLETE INVITATION — a cool, MOONLIT descent below the film.
Not a drop to flat black: `.pnAmbient` lays soft silver-blue glows down the
whole page so the ground reads as moonlight in a dark room. The evening runs
as a real silver-threaded timeline (the one place numbers live — its own
clock). One full-bleed graded still carries the film's world down as a quiet
breath. No eyebrow labels, no dotted meta strings, no cards, no shadows.
Imagery, cool light, space, and type.
* ===================================================================== */
.pnSite {
position: relative;
z-index: 1;
background: var(--pn-night);
overflow: hidden; /* contain the ambient glows */
}
/* moonlight spilled onto the page — soft, low, never flat black */
.pnAmbient {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
background:
radial-gradient(60% 32% at 78% 6%, rgba(140, 168, 255, 0.15) 0%, rgba(7, 13, 31, 0) 60%),
radial-gradient(55% 28% at 12% 40%, rgba(120, 150, 240, 0.1) 0%, rgba(7, 13, 31, 0) 62%),
radial-gradient(70% 34% at 60% 82%, rgba(180, 205, 255, 0.13) 0%, rgba(7, 13, 31, 0) 60%);
}
/* everything sits above the glow */
.pnSite > section,
.pnSite > footer {
position: relative;
z-index: 1;
padding-inline: var(--pn-gut);
}
/* --- THE THEME — what the night is (image + statement) --- */
.pnTheme {
max-width: 80rem;
margin: 0 auto;
padding-block: clamp(5rem, 15vh, 10rem);
display: grid;
gap: clamp(2rem, 6vw, 5rem);
align-items: center;
}
@media (min-width: 860px) {
.pnTheme {
grid-template-columns: 1fr 1fr;
}
}
.pnThemeFig {
position: relative;
aspect-ratio: 4 / 5;
overflow: hidden;
border-radius: 3px;
/* the image sits in its own pool of cool light */
box-shadow: 0 0 120px rgba(140, 168, 255, 0.2);
}
.pnThemeFig img {
width: 100%;
height: 100%;
object-fit: cover;
filter: brightness(0.8) saturate(0.52) contrast(1.05);
}
.pnThemeText {
max-width: 44ch;
}
.pnThemeTitle {
font-family: var(--font-fraunces), Georgia, "Times New Roman", serif;
font-variation-settings: "opsz" 120, "SOFT" 0, "WONK" 0;
font-weight: 340;
font-size: clamp(2.4rem, 6.4vw, 4.2rem);
line-height: 1;
letter-spacing: -0.016em;
color: var(--pn-ivory);
text-wrap: balance;
}
.pnThemeBody {
margin-top: clamp(1.3rem, 3vh, 2rem);
font-size: clamp(1.02rem, 1.9vw, 1.18rem);
line-height: 1.72;
color: var(--pn-ivory-dim);
text-wrap: pretty;
}
/* --- THE EVENING — a real timed sequence, strung on a silver thread --- */
.pnEvening {
max-width: 60rem;
margin: 0 auto;
padding-block: clamp(5rem, 16vh, 11rem);
}
.pnEveningHead {
max-width: 40rem;
margin-bottom: clamp(2.8rem, 7vh, 5rem);
}
.pnBigTitle {
font-family: var(--font-fraunces), Georgia, "Times New Roman", serif;
font-variation-settings: "opsz" 120, "SOFT" 0, "WONK" 0;
font-weight: 340;
font-size: clamp(2.4rem, 7vw, 4.2rem);
line-height: 1;
letter-spacing: -0.016em;
color: var(--pn-ivory);
text-wrap: balance;
}
.pnEveningLede {
margin-top: clamp(1rem, 2.4vh, 1.5rem);
max-width: 48ch;
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(1.1rem, 2.2vw, 1.4rem);
line-height: 1.5;
color: var(--pn-ivory-dim);
text-wrap: pretty;
}
/* the silver thread runs down a fixed rail column; the times sit beside it */
.pnTimeline {
position: relative;
list-style: none;
padding: 0;
}
.pnTimeline::before {
content: "";
position: absolute;
left: 0.875rem; /* centre of the 1.75rem rail column */
top: clamp(1.6rem, 4vh, 2.6rem);
bottom: clamp(1.6rem, 4vh, 2.6rem);
width: 1.5px;
transform: translateX(-50%);
background: linear-gradient(
180deg,
rgba(185, 208, 255, 0.12) 0%,
rgba(185, 208, 255, 0.5) 12%,
rgba(185, 208, 255, 0.5) 82%,
var(--pn-accent-hi) 100%
);
}
.pnMoment {
position: relative;
display: grid;
grid-template-columns: 1.75rem 1fr;
column-gap: clamp(0.8rem, 2vw, 1.4rem);
padding-block: clamp(1.5rem, 4vh, 2.4rem);
}
/* the node dot, on the thread, aligned to the time */
.pnMoment::before {
content: "";
position: absolute;
left: 0.875rem;
top: calc(clamp(1.5rem, 4vh, 2.4rem) + 0.62em);
width: 9px;
height: 9px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: var(--pn-accent);
box-shadow: 0 0 0 4px var(--pn-night), 0 0 14px rgba(185, 208, 255, 0.55);
}
.pnMomentTime {
grid-column: 2;
font-family: var(--font-fraunces), Georgia, serif;
font-variation-settings: "opsz" 40;
font-weight: 380;
font-size: clamp(1.15rem, 2.4vw, 1.5rem);
line-height: 1.1;
color: var(--pn-accent);
letter-spacing: -0.01em;
font-variant-numeric: lining-nums tabular-nums;
}
.pnMomentText {
grid-column: 2;
margin-top: 0.4rem;
}
@media (min-width: 640px) {
.pnMoment {
grid-template-columns: 1.75rem 8rem 1fr;
align-items: baseline;
}
.pnMomentTime {
grid-column: 2;
}
.pnMomentText {
grid-column: 3;
margin-top: 0;
}
}
.pnMomentTitle {
font-family: var(--font-fraunces), Georgia, serif;
font-variation-settings: "opsz" 40;
font-weight: 400;
font-size: clamp(1.3rem, 2.8vw, 1.75rem);
line-height: 1.12;
color: var(--pn-ivory);
letter-spacing: -0.01em;
}
.pnMomentDetail {
margin-top: 0.5rem;
max-width: 46ch;
font-size: clamp(0.95rem, 1.7vw, 1.05rem);
line-height: 1.6;
color: var(--pn-ivory-dim);
text-wrap: pretty;
}
/* the finale is the payoff — a hotter, larger node and a lift in the type */
.pnMoment--finale::before {
width: 13px;
height: 13px;
background: var(--pn-accent-hi);
box-shadow: 0 0 0 4px var(--pn-night), 0 0 24px rgba(234, 242, 255, 0.9);
}
.pnMoment--finale .pnMomentTime {
font-size: clamp(1.35rem, 3vw, 1.9rem);
color: var(--pn-accent-hi);
}
.pnMoment--finale .pnMomentTitle {
font-size: clamp(1.5rem, 3.4vw, 2.1rem);
color: var(--pn-ivory);
}
/* --- THE BREATH — one full-bleed graded still, a single line of voice --- */
.pnBand {
position: relative;
min-height: clamp(24rem, 62vh, 40rem);
display: flex;
align-items: center;
justify-content: center;
padding-block: clamp(4rem, 12vh, 8rem);
overflow: hidden;
/* full-bleed: cancel the site's inline gutter */
margin-inline: calc(var(--pn-gut) * -1);
padding-inline: var(--pn-gut);
}
.pnBandImg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
/* graded hard, dark + desaturated so the warm source reads as cool depth */
filter: brightness(0.46) saturate(0.4) contrast(1.1);
}
/* recolour the band still to moonlight, then darken for legibility */
.pnBandScrim {
position: absolute;
inset: 0;
z-index: 1;
background:
linear-gradient(180deg, #1e3f7a 0%, #0e2148 100%);
mix-blend-mode: color;
opacity: 0.6;
pointer-events: none;
}
/* a second, darkening veil above the recolour (normal blend) */
.pnBand::after {
content: "";
position: absolute;
inset: 0;
z-index: 2;
background:
radial-gradient(90% 90% at 50% 50%, rgba(var(--pn-sink), 0.32) 0%, rgba(var(--pn-sink), 0.74) 100%),
linear-gradient(180deg, rgba(var(--pn-sink), 0.5) 0%, rgba(var(--pn-sink), 0.22) 40%, rgba(var(--pn-sink), 0.62) 100%);
pointer-events: none;
}
.pnBandLine {
position: relative;
z-index: 3;
max-width: 22ch;
text-align: center;
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(1.6rem, 4.6vw, 3.2rem);
line-height: 1.25;
letter-spacing: 0.002em;
color: var(--pn-ivory);
text-wrap: balance;
text-shadow: 0 2px 40px rgba(var(--pn-sink), 0.7), 0 0 60px rgba(185, 208, 255, 0.2);
}
/* --- THE DETAILS — when / where / dress / tickets, read plainly --- */
.pnDetails {
max-width: 80rem;
margin: 0 auto;
padding-block: clamp(5rem, 14vh, 9rem);
}
.pnDetailGrid {
display: grid;
grid-template-columns: 1fr;
gap: clamp(2rem, 5vw, 3rem);
}
@media (min-width: 560px) {
.pnDetailGrid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 940px) {
.pnDetailGrid {
grid-template-columns: repeat(4, 1fr);
}
}
.pnDetail {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding-top: 1.5rem;
border-top: 1px solid var(--pn-accent-soft);
}
.pnDetailLabel {
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(1rem, 2vw, 1.2rem);
color: var(--pn-accent);
}
.pnDetailValue {
font-family: var(--font-fraunces), Georgia, serif;
font-variation-settings: "opsz" 40;
font-weight: 380;
font-size: clamp(1.3rem, 2.6vw, 1.7rem);
line-height: 1.12;
color: var(--pn-ivory);
letter-spacing: -0.01em;
}
.pnDetailSub {
font-size: clamp(0.9rem, 1.6vw, 1rem);
line-height: 1.5;
color: var(--pn-ivory-faint);
}
/* --- TICKETS — the CTA, in a pool of cool light. Tiers as columns --- */
.pnTickets {
position: relative;
max-width: 80rem;
margin: 0 auto;
padding-block: clamp(5rem, 16vh, 11rem);
}
/* a soft moonlight glow behind the whole ask — the light gathering to the CTA */
.pnTicketsGlow {
position: absolute;
left: 50%;
bottom: clamp(4rem, 12vh, 8rem);
width: min(46rem, 90%);
height: min(46rem, 90%);
transform: translateX(-50%);
z-index: 0;
border-radius: 50%;
background: radial-gradient(circle at center, rgba(160, 190, 255, 0.16) 0%, rgba(7, 13, 31, 0) 66%);
pointer-events: none;
}
.pnTickets > * {
position: relative;
z-index: 1;
}
.pnTicketsHead {
max-width: 46rem;
margin-bottom: clamp(2.6rem, 6vh, 4rem);
}
.pnTicketsBody {
margin-top: clamp(1.2rem, 3vh, 1.8rem);
max-width: 52ch;
font-size: clamp(1.02rem, 1.9vw, 1.18rem);
line-height: 1.72;
color: var(--pn-ivory-dim);
text-wrap: pretty;
}
.pnTiers {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: 1fr;
gap: clamp(1.6rem, 4vw, 2.4rem);
}
@media (min-width: 720px) {
.pnTiers {
grid-template-columns: repeat(3, 1fr);
}
}
.pnTier {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--pn-accent-soft);
}
.pnTierName {
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(1.1rem, 2.2vw, 1.35rem);
color: var(--pn-accent);
}
.pnTierPrice {
font-family: var(--font-fraunces), Georgia, serif;
font-variation-settings: "opsz" 72, "SOFT" 0, "WONK" 0;
font-weight: 350;
font-size: clamp(2.8rem, 6vw, 3.8rem);
line-height: 1;
color: var(--pn-ivory);
letter-spacing: -0.016em;
font-variant-numeric: lining-nums;
}
.pnTierDetail {
max-width: 26ch;
font-size: clamp(0.92rem, 1.6vw, 1rem);
line-height: 1.55;
color: var(--pn-ivory-dim);
text-wrap: pretty;
}
/* the one buy action: filled silver-blue with a cool glow, warms to icy white */
.pnBuy {
margin-top: clamp(2.6rem, 6vh, 3.8rem);
padding: 1rem 2.8rem;
font: inherit;
font-size: clamp(0.95rem, 1.7vw, 1.05rem);
letter-spacing: 0.04em;
color: var(--pn-night);
background: var(--pn-accent);
border: 1px solid var(--pn-accent);
border-radius: 2px;
cursor: pointer;
box-shadow: 0 0 40px rgba(185, 208, 255, 0.3);
transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
}
.pnBuy:hover {
background: var(--pn-accent-hi);
border-color: var(--pn-accent-hi);
box-shadow: 0 0 60px rgba(234, 242, 255, 0.5);
}
/* --- FOOTER --- */
.pnFooter {
border-top: 1px solid rgba(185, 208, 255, 0.14);
padding-block: clamp(4rem, 12vh, 7rem);
background: var(--pn-night-2);
}
.pnFootTop {
max-width: 82rem;
margin: 0 auto;
display: grid;
gap: clamp(2.4rem, 6vw, 4rem);
}
@media (min-width: 780px) {
.pnFootTop {
grid-template-columns: 1.2fr 2fr;
}
}
.pnFootMark {
font-family: var(--font-fraunces), Georgia, "Times New Roman", serif;
font-variation-settings: "opsz" 96, "SOFT" 0, "WONK" 0;
font-weight: 330;
font-size: clamp(2rem, 5vw, 2.8rem);
letter-spacing: -0.02em;
color: var(--pn-ivory);
}
.pnFootStatement {
margin-top: 0.9rem;
max-width: 34ch;
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(1rem, 2vw, 1.2rem);
line-height: 1.5;
color: var(--pn-ivory-dim);
}
.pnFootNav {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: clamp(1.4rem, 4vw, 2.5rem);
}
@media (max-width: 520px) {
.pnFootNav {
grid-template-columns: repeat(2, 1fr);
}
}
.pnFootHead {
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: 1.05rem;
color: var(--pn-accent);
margin-bottom: 0.9rem;
}
.pnFootCol ul {
list-style: none;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.pnFootCol a {
font-size: 0.95rem;
color: var(--pn-ivory-dim);
transition: color 0.3s ease;
}
.pnFootCol a:hover {
color: var(--pn-ivory);
}
.pnSignup {
max-width: 82rem;
margin: clamp(3rem, 8vh, 5rem) auto 0;
padding-top: clamp(2.4rem, 6vh, 3.6rem);
border-top: 1px solid rgba(238, 243, 251, 0.09);
}
.pnSignupLabel {
display: block;
font-family: var(--font-instrument), Georgia, serif;
font-style: italic;
font-size: clamp(1rem, 2vw, 1.25rem);
color: var(--pn-ivory-dim);
margin-bottom: 1.1rem;
}
.pnSignupRow {
display: flex;
gap: 0.75rem;
max-width: 30rem;
align-items: stretch;
}
.pnSignupInput {
flex: 1;
min-width: 0;
padding: 0.8rem 0;
font: inherit;
color: var(--pn-ivory);
background: transparent;
border: none;
border-bottom: 1px solid rgba(238, 243, 251, 0.24);
border-radius: 0;
transition: border-color 0.3s ease;
}
.pnSignupInput::placeholder {
color: var(--pn-ivory-faint);
}
.pnSignupInput:focus {
outline: none;
border-bottom-color: var(--pn-accent);
}
.pnSignupBtn {
padding: 0 1.6rem;
font: inherit;
font-size: 0.92rem;
letter-spacing: 0.04em;
color: var(--pn-accent);
background: transparent;
border: 1px solid var(--pn-accent-soft);
border-radius: 2px;
cursor: pointer;
transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease;
}
.pnSignupBtn:hover {
background: var(--pn-accent);
border-color: var(--pn-accent);
color: var(--pn-night);
}
.pnFine {
max-width: 82rem;
margin: clamp(3rem, 8vh, 4.5rem) auto 0;
font-size: 0.82rem;
letter-spacing: 0.02em;
color: var(--pn-ivory-faint);
}
/* --------------------------------------------------------------------- *
reduced motion: neutralise any transitions if this class tree is shown
* --------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
.pnRoot * {
transition: none !important;
animation: none !important;
}
}