Unverified Commit 32a2a5bc by Paul-Louis NECH Committed by GitHub

feat(cosmicfest): rebuild /cosmicfest as v2.67Hz, entanglement-themed invite (#7)

Full redesign of the standalone invite page for the 2026 edition (20-23 August,
Labenne-Ocean, jour J samedi 22). Concept: intrication (entanglement) - two
states, one system, communicating across distance. Dark OKLCH design system with
a paired cyan/magenta accent language, an EntanglementField canvas (starfield +
mirrored particle pairs + 67Hz interference waveform), Archivo (wdth) + Martian
Mono, replacing the v0 light theme.

- new EntanglementField signature canvas (DPR-capped, mobile-scaled, full
  prefers-reduced-motion static path)
- sections: hero, concept, jour-J lineup, sejour, hebergement, transport,
  packing, v0 souvenirs (lifted audio embeds + accessible photo lightbox), RSVP
- countdown retargeted to 2026-08-22, hydration-safe placeholder
- lightbox renders full-size via plain img with managed focus
- add PRODUCT.md / DESIGN.md design context at repo root

Verified: clean SSG build (6.3 kB), WCAG AA contrast, zero console errors,
correct SSR + reduced-motion paths.
parent 5fd1374e
# Design
> Visual system for the **cosmicfest_v2.67Hz** micro-site. Standalone from the main site's
> Syne identity — cosmicfest has its own fonts and tokens (`next/styles/cosmicfest.module.css`).
## Theme
Dark, electric, cosmic-laboratory. Scene: a beachside night, an oscilloscope glowing in the
dark, two particles entangled across the sand. Not space-opera purple — a deep cosmic ink with
two living accents (cyan + magenta) that always appear as a pair, the way entangled states do,
resolving to white phosphor where they meet. One warm amber spark for the summer/sun nod.
Color strategy: **Full palette** (committed dark ground + a deliberate dual-accent system).
## Color (OKLCH)
| Role | Token | Value | Notes |
|---|---|---|---|
| Ground | `--cf-bg` | `oklch(0.15 0.03 275)` | cosmic ink, faint violet-blue |
| Surface | `--cf-surface` | `oklch(0.19 0.035 278)` | raised panels |
| Surface alt | `--cf-surface-2` | `oklch(0.23 0.04 280)` | hover / nested ground |
| Border | `--cf-border` | `oklch(1 0 0 / 0.10)` | hairlines |
| Ink | `--cf-ink` | `oklch(0.97 0.01 270)` | primary text, ~14:1 on bg |
| Muted | `--cf-muted` | `oklch(0.80 0.02 272)` | secondary, ≥4.5:1 on bg |
| **Signal (state A)** | `--cf-cyan` | `oklch(0.84 0.14 200)` | first entangled accent |
| **Entangled (state B)** | `--cf-magenta` | `oklch(0.72 0.21 350)` | second entangled accent |
| Phosphor | `--cf-white` | `oklch(0.99 0 0)` | stars, the "meeting point" |
| Spark (sun) | `--cf-amber` | `oklch(0.82 0.15 75)` | rare warm accent, the 67Hz/sun |
The two accents are a **system**: use them together (a cyan label paired with a magenta value,
a gradient *between* them reserved only for the literal entanglement motif — never gradient text).
## Typography
Voice words: electric, precise, oscillating. Reflex-rejects avoided (no Space Mono/Grotesk,
Montserrat, Orbitron, Inter).
- **Display**`Archivo Expanded` (700–900, wide). The wordmark + section heads shout like a
concert poster. `letter-spacing` ≥ -0.02em; `text-wrap: balance`.
- **Body**`Archivo` (400–600). Same superfamily as display = cohesion. Logistics prose,
≤70ch, line-height 1.6 (light-on-dark needs the air).
- **Mono**`Martian Mono` (400–600). The lab instrument: frequencies, dates, coordinates,
the "67Hz", entanglement labels, countdown digits. Earned, not costume.
Loaded via `next/font/google`, exposed as CSS vars on the page container.
## Components
- **Hero canvas** (`EntanglementField`): full-bleed `<canvas>` — drifting starfield, ~6–10
entangled particle *pairs* (two points joined by a faint line, mirroring each other's motion
across the viewport), and a 67Hz sine/interference waveform along the lower third. Decorative,
behind content, `prefers-reduced-motion` → single static frame.
- **Wordmark**: `cosmicfest` (display) + `v2.67Hz` (mono badge). The `.67Hz` is the hook.
- **Lineup rows**: not cards — a typographic list, each act = name (display) · role (mono) ·
the artist's real name in parens. Cyan/magenta alternating per row.
- **Logistics blocks**: scannable definition-style groups (séjour, hébergement, transport,
à prévoir) with mono labels + body values. Generous, not walls.
- **Souvenirs v0**: ParVagues Bandcamp/SoundCloud embeds + a small photo strip with an
accessible lightbox (lifted + restyled from v0).
- **Countdown**: mono digits counting to 2026-08-22T00:00 (jour J). Retargeted from v0's June auto-calc.
- **CTA**: mailto `cosmic@nech.pl`, "réponds à l'invitation" — verb + object.
## Motion
- Hero canvas: continuous, GPU-cheap (transform/opacity on canvas paint; rAF loop).
- Section reveals: staggered fade+rise, ease-out-expo, enhancing an already-visible default
(never gating visibility). Entangled-pair micro-motion on lineup hover.
- All motion has a `prefers-reduced-motion: reduce` path (crossfade/instant, static canvas).
## Layout
Single long scroll, deliberate pacing. Fluid `clamp()` spacing. Asymmetry allowed for the hero.
Mobile-first; logistics reflow to single column; canvas stays performant on phones (capped DPR,
fewer particles).
# Product
> Scope: this file currently centers on the **cosmicfest** micro-site (`next/pages/cosmicfest/`),
> the active brand surface. The wider repo is a personal portfolio (dunbar, parvagues, hydras,
> talks, poems); each section is its own micro-brand. See `CLAUDE.md` for the repo architecture.
## Register
brand
## Users
Friends and fellow travellers invited to cosmicfest, a private, invitation-only music +
creative-coding gathering at Labenne-Océan (French Atlantic coast). They land on the page
from a personal message, on phone or laptop, deciding whether to come and needing the
practical details (dates, lineup, lodging, transport, what to pack) to say yes. Mostly
French-speaking. The job to be done: feel the vibe, get the logistics, RSVP.
## Product Purpose
A single-page invitation for **cosmicfest_v2.67Hz** (20–23 August 2026, jour J Saturday 22).
It exists to make people *want* to come and to give them everything they need to commit.
Not a ticketed event page: no commerce, no signup funnel. Success = the right people reply
to `cosmic@nech.pl` before 1 August, and the page itself feels like the festival.
## Brand Personality
Quantum, wild, connected. The organizing metaphor is **intrication** (entanglement): two
states forming one system, communicating instantly across distance — the festival as people
entangled on a shared 67Hz frequency. Voice is playful-cosmic French, self-aware
("cosmicpipoo"), warm not corporate. Cerebral physics wink over a sun-and-surf summer
underneath. Emotional goal: anticipation, belonging, a little awe.
## Anti-references
- Generic "cosmic" = deep-space purple gradient + scattered stars + Orbitron. The first reflex; banned.
- SaaS event-landing template: hero-metric blocks, identical card grids, eyebrow kickers on every section.
- Eventbrite / Luma ticketing-page utilitarianism. This is an invitation, not a checkout.
- Cream/warm-neutral "editorial restraint." Wrong register entirely; this page is dark and electric.
- Mono-everything as costume. Mono is earned here (livecoding/frequency), but display carries the shout.
## Design Principles
1. **The medium is the message.** Entanglement isn't just copy — it's the palette (two accents
that always travel together), the motion (paired particles mirroring across distance), the arc
(v2 entangle → v3 decohere). The physics is felt, not explained.
2. **One signature, done extraordinarily.** A live starfield + entangled-pairs + 67Hz waveform
canvas is the centerpiece. Everything else stays disciplined so it can shine.
3. **Logistics are hospitality.** Dense practical info (lodging, transport, packing) is part of the
warmth, not an afterthought. Make it scannable and generous, never a wall.
4. **Invitation, not transaction.** No tickets, no forms-by-default. A mailto and a countdown.
"Sur invitation. Riders, come to the storm."
5. **Earn the strangeness.** Brand register rewards a POV. Commit to dark + electric + physics-weird.
## Accessibility & Inclusion
WCAG AA target. Body text ≥4.5:1 on the dark cosmic ground; large/display ≥3:1. The signature
canvas is decorative — it must never gate content, and it must have a full
`prefers-reduced-motion: reduce` fallback (static render, no animation loop). Keyboard-operable
lightbox and CTA. French primary; clear, plain phrasing for logistics.
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import styles from '../../styles/cosmicfest.module.css'; // Assuming you might want specific styles import styles from '../../styles/cosmicfest.module.css';
const Countdown = ({ targetDate }) => { const LABELS = { jours: 'jours', heures: 'heures', minutes: 'min', secondes: 'sec' };
const calculateTimeLeft = () => {
const difference = +new Date(targetDate) - +new Date(); function computeLeft(targetDate) {
let timeLeft = {}; const diff = +new Date(targetDate) - Date.now();
if (diff <= 0) return null;
if (difference > 0) { return {
timeLeft = { jours: Math.floor(diff / 86400000),
jours: Math.floor(difference / (1000 * 60 * 60 * 24)), heures: Math.floor((diff / 3600000) % 24),
heures: Math.floor((difference / (1000 * 60 * 60)) % 24), minutes: Math.floor((diff / 60000) % 60),
minutes: Math.floor((difference / 1000 / 60) % 60), secondes: Math.floor((diff / 1000) % 60),
secondes: Math.floor((difference / 1000) % 60),
};
} else {
timeLeft = { jours: 0, heures: 0, minutes: 0, secondes: 0 };
}
return timeLeft;
}; };
}
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft()); export default function Countdown({ targetDate }) {
// undefined = not yet mounted (SSR / first paint) → neutral "--" placeholder, so the
// server never wrongly claims the event is happening now.
// null = actually expired → the celebratory message.
// object = live remaining time.
const [left, setLeft] = useState(undefined);
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { setLeft(computeLeft(targetDate));
setTimeLeft(calculateTimeLeft()); const id = setInterval(() => setLeft(computeLeft(targetDate)), 1000);
}, 1000); return () => clearInterval(id);
}, [targetDate]);
return () => clearTimeout(timer);
});
const timerComponents = [];
Object.keys(timeLeft).forEach((interval) => {
if (!timeLeft[interval] && timeLeft[interval] !== 0) { // handles case where initial timeLeft is empty
return;
}
timerComponents.push( if (left === null) {
<span key={interval} style={{ margin: '0 0.5rem', textAlign: 'center' }}> return (
<div style={{ fontSize: '2.5rem', fontWeight: 'bold' }}>{timeLeft[interval]}</div> <div className={styles.countdown} aria-live="polite">
<div style={{ fontSize: '0.8rem', textTransform: 'uppercase' }}>{interval}</div> <span className={styles.countDone}>c&rsquo;est l&rsquo;heure. on s&rsquo;intrique. 🌀</span>
</span> </div>
); );
}); }
return ( return (
<div className={styles.countdown} style={{ display: 'flex', justifyContent: 'center', alignItems: 'baseline' }}> <div className={styles.countdown} aria-label="compte à rebours jusqu'au jour J">
{timerComponents.length ? timerComponents : <span>c'est l'heure!</span>} {Object.keys(LABELS).map((unit) => (
<span key={unit} className={styles.countUnit}>
<span className={styles.countNum}>
{left ? String(left[unit]).padStart(2, '0') : '--'}
</span>
<span className={styles.countLabel}>{LABELS[unit]}</span>
</span>
))}
</div> </div>
); );
}; }
export default Countdown;
import { useEffect, useRef } from 'react';
/**
* EntanglementField — the cosmicfest_v2.67Hz signature canvas.
*
* Three layers, one idea (intrication):
* 1. a drifting starfield (the cosmos)
* 2. entangled particle PAIRS — each pair is two points mirrored across the field's
* center, brightening in perfect sync no matter the distance between them (the link)
* 3. a 67Hz interference waveform along the lower third (cyan + magenta crossing into white)
*
* Decorative only: sits behind content, never gates it. Honours prefers-reduced-motion by
* painting a single static frame (no rAF loop). Capped DPR + area-scaled counts for phones.
*/
const CYAN = 'oklch(0.84 0.14 200)';
const MAGENTA = 'oklch(0.72 0.21 350)';
const WHITE = 'oklch(0.99 0 0)';
export default function EntanglementField({ className }) {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let W = 0;
let H = 0;
let dpr = 1;
let stars = [];
let pairs = [];
let raf = null;
let running = true;
const rand = (a, b) => a + Math.random() * (b - a);
function build() {
const rect = canvas.getBoundingClientRect();
W = Math.max(1, rect.width);
H = Math.max(1, rect.height);
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const area = W * H;
const starCount = Math.min(Math.round(area / 7000), 240);
const pairCount = W < 640 ? 5 : W < 1100 ? 8 : 11;
stars = Array.from({ length: starCount }, () => ({
x: Math.random() * W,
y: Math.random() * H,
r: rand(0.3, 1.4),
base: rand(0.15, 0.7),
tw: rand(0.6, 2.2),
ph: rand(0, Math.PI * 2),
}));
// Each pair lives in the top-left quadrant; its twin is the mirror through center.
pairs = Array.from({ length: pairCount }, () => ({
x: rand(0.08, 0.46) * W,
y: rand(0.06, 0.62) * H,
ox: rand(8, 26),
oy: rand(8, 26),
speed: rand(0.18, 0.5),
ph: rand(0, Math.PI * 2),
link: rand(0.5, 1.2), // sync frequency of the shared brightness pulse
}));
}
function drawWave(t) {
// 67Hz rendered as two interfering travelling waves; where they cross, white.
const yBase = H * 0.74;
const amp = Math.min(H * 0.06, 46);
const step = Math.max(4, Math.round(W / 220));
const waves = [
{ color: CYAN, k: 0.018, w: 1.0, off: 0, a: 0.5 },
{ color: MAGENTA, k: 0.013, w: -1.35, off: Math.PI / 3, a: 0.5 },
];
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.lineWidth = 1.6;
for (const wv of waves) {
ctx.beginPath();
for (let x = 0; x <= W; x += step) {
const y =
yBase +
Math.sin(x * wv.k + t * wv.w + wv.off) * amp +
Math.sin(x * wv.k * 2.7 + t * wv.w * 0.6) * amp * 0.28;
x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.globalAlpha = wv.a;
ctx.strokeStyle = wv.color;
ctx.stroke();
}
ctx.restore();
}
function frame(now) {
if (!running) return;
const t = now / 1000;
ctx.clearRect(0, 0, W, H);
// 1 — starfield
for (const s of stars) {
const a = reduce ? s.base : s.base * (0.55 + 0.45 * Math.sin(t * s.tw + s.ph));
ctx.globalAlpha = a;
ctx.fillStyle = WHITE;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// 2 — entangled pairs
const cx = W / 2;
const cy = H / 2;
for (const p of pairs) {
const wob = reduce ? 0 : t * p.speed;
const ax = p.x + Math.cos(wob + p.ph) * p.ox;
const ay = p.y + Math.sin(wob * 1.3 + p.ph) * p.oy;
// twin: mirror through the field's center (instant correlation across distance)
const bx = 2 * cx - ax;
const by = 2 * cy - ay;
// shared brightness pulse — both ends flare in the same instant
const pulse = reduce ? 0.6 : 0.45 + 0.55 * Math.pow((Math.sin(t * p.link + p.ph) + 1) / 2, 2);
// the link
ctx.save();
ctx.globalCompositeOperation = 'lighter';
const grad = ctx.createLinearGradient(ax, ay, bx, by);
grad.addColorStop(0, CYAN);
grad.addColorStop(0.5, WHITE);
grad.addColorStop(1, MAGENTA);
ctx.strokeStyle = grad;
ctx.globalAlpha = 0.06 + 0.1 * pulse;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(bx, by);
ctx.stroke();
// the two states
ctx.globalAlpha = 0.55 + 0.45 * pulse;
const r = 2 + 1.6 * pulse;
ctx.fillStyle = CYAN;
ctx.beginPath();
ctx.arc(ax, ay, r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = MAGENTA;
ctx.beginPath();
ctx.arc(bx, by, r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.globalAlpha = 1;
// 3 — waveform
drawWave(reduce ? 0 : t);
if (!reduce) raf = requestAnimationFrame(frame);
}
let resizeTimer = null;
function onResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
build();
if (reduce) frame(0);
}, 150);
}
function onVisibility() {
if (document.hidden) {
running = false;
if (raf) cancelAnimationFrame(raf);
} else if (!reduce) {
running = true;
raf = requestAnimationFrame(frame);
}
}
build();
if (reduce) {
frame(0); // single static frame, no loop
} else {
raf = requestAnimationFrame(frame);
}
window.addEventListener('resize', onResize);
document.addEventListener('visibilitychange', onVisibility);
return () => {
running = false;
if (raf) cancelAnimationFrame(raf);
clearTimeout(resizeTimer);
window.removeEventListener('resize', onResize);
document.removeEventListener('visibilitychange', onVisibility);
};
}, []);
return <canvas ref={canvasRef} className={className} aria-hidden="true" />;
}
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; // Using Next.js Image component for optimization import Image from 'next/image';
import { useState, useEffect } from 'react'; // Added useState and useEffect import { useState, useEffect, useRef, useCallback } from 'react';
import { Archivo, Martian_Mono } from 'next/font/google';
import styles from '../../styles/cosmicfest.module.css'; import styles from '../../styles/cosmicfest.module.css';
import Countdown from '../../components/cosmicfest/Countdown'; import Countdown from '../../components/cosmicfest/Countdown';
// import ParVaguesFooter from '../../components/ParVaguesFooter'; // Optional: if you want to reuse the main site footer import EntanglementField from '../../components/cosmicfest/EntanglementField';
// Helper function to calculate next June 21st const sans = Archivo({ subsets: ['latin'], axes: ['wdth'], display: 'swap', variable: '--font-cf-sans' });
const getNextJune21st = () => { const mono = Martian_Mono({ subsets: ['latin'], display: 'swap', variable: '--font-cf-mono' });
const today = new Date();
const currentYear = today.getFullYear();
let nextJune21 = new Date(currentYear, 5, 21); // Month is 0-indexed (5 is June)
if (today > nextJune21) { const JOUR_J = '2026-08-22T18:00:00+02:00';
nextJune21 = new Date(currentYear + 1, 5, 21);
}
return nextJune21;
};
export default function CosmicFestHome() {
const nextFestivalDate = getNextJune21st();
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalImageSrc, setModalImageSrc] = useState('');
const [modalImageAlt, setModalImageAlt] = useState('');
const openModal = (src, alt) => {
setModalImageSrc(src);
setModalImageAlt(alt);
setIsModalOpen(true);
document.body.style.overflow = 'hidden'; // Prevent background scrolling
};
const closeModal = () => { const LINEUP = [
setIsModalOpen(false); { name: 'ParVagues', real: 'PLN', role: 'live coding · breakbeat → techno nujazz', emoji: '💻' },
setModalImageSrc(''); { name: 'Nesta Flex', real: 'Antoine', role: 'dj set', emoji: '🎛️' },
setModalImageAlt(''); { name: 'shipow', real: 'Kévin', role: 'projection visuelle', emoji: '🖼️' },
document.body.style.overflow = 'auto'; // Restore background scrolling ];
};
const PROGRAMME = [
{ e: '🌞', t: 'chillance absolue' },
{ e: '🍖', t: 'bbq' },
{ e: '🏖️', t: 'plage' },
{ e: '🛹', t: 'skate' },
{ e: '🏄', t: 'surf' },
{ e: '🎵', t: 'musique' },
];
const PACKING = [
{ e: '🏕️', t: 'tente & camping (si besoin)' },
{ e: '🩳', t: 'maillot de bain' },
{ e: '🧖', t: 'serviette' },
{ e: '🛹', t: 'skate / surf' },
{ e: '🎸', t: 'instruments' },
];
const SOUVENIRS = [
{ src: '/images/cosmicfest/v0_parvagues_lineup.jpg', alt: 'v0 · ParVagues en live coding' },
{ src: '/images/cosmicfest/v0_hugo.jpg', alt: 'v0 · Hugo à la guitare' },
{ src: '/images/cosmicfest/v0_off_01.jpg', alt: 'v0 · le off, au bunker' },
{ src: '/images/cosmicfest/v0_off_03.jpg', alt: 'v0 · nuit sur la plage' },
];
const MAILTO =
'mailto:cosmic@nech.pl' +
'?subject=' +
encodeURIComponent('je suis chaud · cosmicfest_v2.67Hz') +
'&body=' +
encodeURIComponent('salut ! je viens.\n\nmon côté cosmique : ...\n+1 : oui / non\narrivée prévue : ...\n');
export default function CosmicFest() {
const [modal, setModal] = useState(null); // { src, alt }
const rootRef = useRef(null);
const closeBtnRef = useRef(null);
const lastFocusRef = useRef(null);
const openModal = useCallback((img) => {
lastFocusRef.current = document.activeElement;
setModal(img);
document.body.style.overflow = 'hidden';
}, []);
const closeModal = useCallback(() => {
setModal(null);
document.body.style.overflow = '';
// Return focus to the thumbnail that opened the lightbox.
if (lastFocusRef.current?.focus) lastFocusRef.current.focus();
}, []);
// Esc closes the lightbox; focus moves into the dialog on open.
useEffect(() => { useEffect(() => {
const handleEsc = (event) => { if (!modal) return undefined;
if (event.key === 'Escape' && isModalOpen) { closeBtnRef.current?.focus();
closeModal(); const onKey = (e) => e.key === 'Escape' && closeModal();
} window.addEventListener('keydown', onKey);
}; return () => window.removeEventListener('keydown', onKey);
window.addEventListener('keydown', handleEsc); }, [modal, closeModal]);
// Cleanup function to remove listener // Scroll reveals — only armed when JS is ready AND motion is allowed, so content
return () => { // is never gated behind a transition that fails to fire on headless / no-JS renders.
window.removeEventListener('keydown', handleEsc); useEffect(() => {
// Ensure body overflow is reset if component unmounts while modal is open const root = rootRef.current;
if (isModalOpen) { if (!root) return undefined;
document.body.style.overflow = 'auto'; const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce || !('IntersectionObserver' in window)) return undefined;
root.classList.add(styles.jsReady);
const targets = root.querySelectorAll(`.${styles.reveal}`);
const reveal = (el) => el.classList.add(styles.revealIn);
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
reveal(entry.target);
io.unobserve(entry.target);
} }
}
},
{ threshold: 0.12, rootMargin: '0px 0px -8% 0px' }
);
targets.forEach((t) => io.observe(t));
// Safety net: if the observer never fires (background tab, headless render, edge
// browsers), nothing must stay hidden. Reveal everything after a beat regardless.
const safety = setTimeout(() => targets.forEach(reveal), 2500);
return () => {
io.disconnect();
clearTimeout(safety);
}; };
}, [isModalOpen]); // Dependency array includes isModalOpen }, []);
const lineup2024 = [
{ artist: "Hugo", description: "live guitare, compos et reprises", emojis:"🎸🎙️" },
{ artist: "Flo", description: "live machines downtempo", emojis:"⚙️🎛" },
{ artist: "ParVagues", description: "livecoding breakbeat to techno nujazz", emojis:"💻🎛️" },
{ artist: "DJ @marion.lhn", description: "set drum-n-bass", emojis: "💻📀" },
{ artist: "Kevin & Pipou", description: "lights et visuels interactifs", emojis:"💻📽️" },
{ artist: "PLN", description: "visuels Hydra.js", emojis:"💻📽" },
];
// Placeholder data for v0
const v0_content = {
photos: [
{ src: '/images/cosmicfest/v0_hugo.jpg', alt: 'v0 ~ hugo à la guitare' },
{ src: '/images/cosmicfest/v0_parvagues_lineup.jpg', alt: 'v0 ~ parvagues ~ line-up' },
{ src: '/images/cosmicfest/hero.gif', alt: 'v0 ~ le off au bunker' },
],
videos: [
{ id: '6uoO282xJXM', src: '/images/cosmicfest/v0_parvagues_lineup_crop.jpg', title: 'scene v0 - parvagues livecoding' },
// { id: 'YOUTUBE_VIDEO_ID_V0_PERFORMANCE_2', title: 'live performance v0 - artiste y' },
],
aftermovie: { id: 'YOUTUBE_VIDEO_ID_V0_AFTERMOVIE', title: 'aftermovie cosmicfest v0', released: false },
lineup: [
"20h ~ hugo ~ guitare",
"21h ~ flo ~ downtempo",
"22h ~ parvagues ~ livecoding",
"23h ~ marion.lhn ~ set dnb",
"00+ ~ jam"
]
};
return ( return (
<div className={styles.container}> <div ref={rootRef} className={`${styles.page} ${sans.variable} ${mono.variable}`}>
<Head> <Head>
<title>cosmicfest</title> <title>cosmicfest_v2.67Hz</title>
<meta name="description" content="cosmicfest - festival indé de musique et création numérique à Labenne Océan. 21 juin." /> <meta
<link rel="icon" href="/favicon.ico" /> {/* Consider a specific cosmicfest favicon */} name="description"
content="cosmicfest_v2.67Hz · 20–23 août 2026, Labenne-Océan. Deux états, un seul système : on s'intrique. Live coding ParVagues, dj set Nesta Flex, visuels shipow. Sur invitation."
/>
<meta name="theme-color" content="#1a1330" />
<meta property="og:title" content="cosmicfest_v2.67Hz" />
<meta property="og:description" content="20–23 août 2026, Labenne-Océan. On s'intrique sur 67 hertz. Sur invitation." />
<meta property="og:type" content="website" />
<meta property="og:image" content="/images/cosmicfest/hero.jpg" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="cosmicfest_v2.67Hz" />
<meta name="twitter:description" content="20–23 août 2026, Labenne-Océan. On s'intrique sur 67 hertz. Sur invitation." />
<meta name="twitter:image" content="/images/cosmicfest/hero.jpg" />
<link rel="icon" href="/favicon.ico" />
</Head> </Head>
<main className={styles.main}> <main className={styles.main}>
{/* ── HERO ── */}
<header className={styles.hero}> <header className={styles.hero}>
{/* Hero image is set via CSS background for now */} <EntanglementField className={styles.heroCanvas} />
<h1>🌠 cosmicfest 🌌</h1> <div className={styles.heroInner}>
<h3 style={{ paddingTop: '1em' }}> <span className={styles.freq}>
ondes de rythmes, plages sonores, et code source. <span className={styles.freqDot} aria-hidden="true" />
</h3> cosmicfest <b>v2.67Hz</b> · 2026
<h2>21 juin, labenne océan.<br />🪐</h2> </span>
<h1 className={styles.wordmark}>
<em>cosmicfest</em>
</h1>
<p className={styles.heroTagline}>
deux états, un seul système. on s&rsquo;intrique sur 67&nbsp;hertz, le temps d&rsquo;un été à
Labenne-Océan.
</p>
<div className={styles.heroMeta}>
<span>20 23 août 2026</span>
<span>Labenne-Océan</span>
<span>jour J · samedi 22</span>
</div>
<div className={styles.heroActions}>
<a className={styles.ctaButton} href={MAILTO}>
réponds à l&rsquo;invitation
</a>
<a className={styles.ctaGhost} href="#sejour">
le séjour
</a>
</div>
</div>
<span className={styles.scrollCue}>défile</span>
</header> </header>
<section className={styles.section} id="lineup"> {/* ── INTRICATION ── */}
<h2>lineup {new Date().getFullYear()}</h2> <section className={`${styles.section} ${styles.concept} ${styles.reveal}`}>
<div className={styles.grid}> <p className={styles.stamp}>// intrication</p>
{lineup2024.map((item, index) => ( <p className={styles.conceptQuote}>
<div key={index} className={styles.card}> deux <span className={styles.a}>entités</span> séparées par la distance, liées si fort
<h3>{item.artist} <span className={styles.cardEmojis}>~ {item.emojis} ~</span></h3> qu&rsquo;elles communiquent <span className={styles.b}>à l&rsquo;instant</span> et ne forment plus
<p>{item.description}</p> qu&rsquo;un.
{/* The emoji line is now part of the h3, the extra <p> for emojis and <br/> are removed */} </p>
</div> </section>
{/* ── LINEUP ── */}
<section className={`${styles.section} ${styles.reveal}`}>
<p className={styles.stamp}>// point d&rsquo;orgue</p>
<h2 className={styles.sectionTitle}>le 22 août, dans le jardin</h2>
<ul className={styles.lineup}>
{LINEUP.map((act) => (
<li key={act.name} className={styles.lineupRow}>
<span className={styles.lineupName}>
{act.name} <span className={styles.lineupReal}>~ {act.real}</span>
</span>
<span className={styles.lineupRole}>
{act.emoji} {act.role}
</span>
</li>
))} ))}
</div> </ul>
<p className={styles.lineupGuests}>
<b>+ special guests</b> — inscriptions ouvertes (ramène ton set, ton instrument, ton chaos)
</p>
</section> </section>
<section className={styles.section} id="v0"> {/* ── SÉJOUR ── */}
<h2>souvenirs de la v0 ({nextFestivalDate.getFullYear() -1 })</h2> <section id="sejour" className={`${styles.section} ${styles.reveal}`}>
<h2 className={styles.sectionTitle}>quatre jours pleins</h2>
<p className={styles.sectionLede}>
Du 20 au 23 août 2026 à Labenne-Océan, plus chaude et plus cosmique que la v0. Arrivée
possible dès le 15 pour les plus pressés. Point d&rsquo;orgue le samedi 22.
</p>
<div className={styles.chips}>
{PROGRAMME.map((p) => (
<span key={p.t} className={styles.chip}>
{p.e} {p.t}
</span>
))}
</div>
</section>
<h3>photos v0</h3> {/* ── HÉBERGEMENT ── */}
<div className={styles.gallery}> <section className={`${styles.section} ${styles.reveal}`}>
{v0_content.photos.map((photo, index) => ( <h2 className={styles.sectionTitle}>où poser ses ondes</h2>
<div <div className={styles.defs}>
key={photo.src} // Using src as key for stability if array order changes but src is unique <div className={styles.def}>
className={`${styles.galleryItem} ${styles.galleryPhotoItem}`} // Ensure both classes are applied <span className={styles.defKey}>maison</span>
style={{backgroundImage: `url(${photo.src})`}} <span className={styles.defVal}>
onClick={() => openModal(photo.src, photo.alt)} <p>10 places à l&rsquo;intérieur, premiers arrivés premiers couchés.</p>
role="button" </span>
tabIndex={0}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openModal(photo.src, photo.alt); } }}
aria-label={`View image: ${photo.alt}`}
>
<span>{photo.alt}</span>
</div> </div>
))} <div className={styles.def}>
<span className={styles.defKey}>jardin</span>
<span className={styles.defVal}>
<p>Plante ta tente. Apporte ton matériel de camping dans ce cas.</p>
</span>
</div>
<div className={styles.def}>
<span className={styles.defKey}>van</span>
<span className={styles.defVal}>
<p>Gare-le dans le jardin, branche-toi à la vibe.</p>
</span>
</div>
<div className={styles.def}>
<span className={styles.defKey}>capacité</span>
<span className={styles.defVal}>
<p>16 personnes au total.</p>
<p className={styles.sub}>+1 bienvenue : partenaires, copines, copains.</p>
</span>
</div> </div>
</div>
</section>
<h3 style={{marginTop: '2rem'}}>vidéos v0</h3> {/* ── VENIR ── */}
<div className={styles.gallery}> <section className={`${styles.section} ${styles.reveal}`}>
{/* Aftermovie */} <h2 className={styles.sectionTitle}>rejoindre la vague</h2>
{v0_content.aftermovie.released && ( <div className={styles.defs}>
<div> <div className={styles.def}>
<a href={`https://www.youtube.com/watch?v=${v0_content.aftermovie.id}`} target="_blank" rel="noopener noreferrer"> <span className={styles.defKey}>gare de Labenne</span>
{v0_content.aftermovie.title} (placeholder: {v0_content.aftermovie.id}) <span className={styles.defVal}>
</a> <p>Arrive ici, je viens te chercher en voiture.</p>
</span>
</div>
<div className={styles.def}>
<span className={styles.defKey}>gare de Bayonne</span>
<span className={styles.defVal}>
<p>Prends le train Bayonne Labenne (15 min), je te récupère.</p>
<p className={styles.sub}>Des taxis attendent aussi à la gare de Bayonne.</p>
</span>
</div> </div>
)}
{/* Other videos */}
{v0_content.videos.map((video, index) => (
<a
key={index}
href={`https://www.youtube.com/watch?v=${video.id}`}
target="_blank"
rel="noopener noreferrer"
className={`${styles.galleryItem} ${styles.galleryVideoLink}`}
style={video.src ? {backgroundImage: `url(${video.src})`} : {}} // Apply BG only if src exists
aria-label={`Watch video: ${video.title}`}
>
<span>{video.title}</span> {/* Span for styling the text overlay */}
</a>
))}
</div> </div>
</section>
<h3 style={{marginTop: '2rem'}}>lineup v0</h3> {/* ── À PRÉVOIR ── */}
<ul> <section className={`${styles.section} ${styles.reveal}`}>
{v0_content.lineup.map((artist, index) => ( <h2 className={styles.sectionTitle}>dans le sac</h2>
<li key={index}>{artist}</li> <div className={styles.packGrid}>
{PACKING.map((p) => (
<div key={p.t} className={styles.packItem}>
<span aria-hidden="true">{p.e}</span>
<span>{p.t}</span>
</div>
))} ))}
</ul> </div>
</section>
<h3 style={{marginTop: '2rem'}}>audio ~ ParVagues</h3> {/* ── SOUVENIRS V0 ── */}
<p> <section className={`${styles.section} ${styles.souvenirs} ${styles.reveal}`}>
ecoute chaque piste ~ ecoute le set entier ~ telecharge le tout <p className={styles.stamp}>// souvenirs</p>
<h2 className={styles.sectionTitle}>l&rsquo;an dernier, la v0</h2>
<p className={styles.sectionLede}>
On s&rsquo;était déjà bien intriqués. Le set complet de ParVagues, et quelques images du off
au bunker.
</p> </p>
<div className={styles.audioPlayersContainer}> <div className={styles.audioPlayersContainer}>
<div className={`${styles.audioEmbed} ${styles.bandcampPlayer}`}> <div className={`${styles.audioEmbed} ${styles.bandcampPlayer}`}>
<iframe title="Bandcamp player for Parvagues live at Cosmicfest" style={{border: 0, width: "100%", maxWidth: "350px", height: "470px"}} src="https://bandcamp.com/EmbeddedPlayer/album=644862623/size=large/bgcol=333333/linkcol=0f91ff/tracklist=true/artwork=small/transparent=true/" seamless><a href="https://parvagues.bandcamp.com/album/live-cosmicfest">live@cosmicfest by ParVagues</a></iframe> <iframe
title="ParVagues · live@cosmicfest (Bandcamp)"
style={{ border: 0, width: '100%', height: '470px' }}
src="https://bandcamp.com/EmbeddedPlayer/album=644862623/size=large/bgcol=181320/linkcol=2eb8d6/tracklist=true/artwork=small/transparent=true/"
seamless
loading="lazy"
/>
</div> </div>
<div className={`${styles.audioEmbed} ${styles.soundcloudPlayer}`}> <div className={`${styles.audioEmbed} ${styles.soundcloudPlayer}`}>
<iframe title="Soundcloud player for Parvagues live at Cosmicfest" width="450" height="450" scrolling="no" frameBorder="no" allow="autoplay" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/2121089043&color=%233e00f7&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true"></iframe><div style={{fontSize: "10px", color: "#cccccc",lineBreak: "anywhere",wordBreak: "normal",overflow: "hidden",whiteSpace: "nowrap",textOverflow: "ellipsis", fontFamily: "Interstate,Lucida Grande,Lucida Sans Unicode,Lucida Sans,Garuda,Verdana,Tahoma,sans-serif",fontWeight: "100"}}><a href="https://soundcloud.com/parvagues" title="ParVagues" target="_blank" rel="noopener noreferrer" style={{color: "#cccccc", textDecoration: "none"}}>ParVagues</a> · <a href="https://soundcloud.com/parvagues/cosmicfest" title="Live@cosmicfest 🌊🌅" target="_blank" rel="noopener noreferrer" style={{color: "#cccccc", textDecoration: "none"}}>Live@cosmicfest 🌊🌅</a></div> <iframe
title="ParVagues · Live@cosmicfest (SoundCloud)"
width="100%"
height="380"
scrolling="no"
frameBorder="no"
allow="autoplay"
loading="lazy"
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/2121089043&color=%23b5179e&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true"
/>
</div> </div>
</div> </div>
</section>
<section className={styles.section} id="next-edition"> <div className={styles.gallery}>
<h2>prochaine édition:<br/>{nextFestivalDate.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}</h2> {SOUVENIRS.map((photo) => (
<div className={styles.countdownContainer}> {/* Added class for styling countdown section */} <button
<Countdown targetDate={nextFestivalDate.toISOString()} /> key={photo.src}
</div> type="button"
<div style={{textAlign: 'center', marginTop: '2rem'}}> className={styles.galleryItem}
<a onClick={() => openModal(photo)}
href="mailto:cosmic@nech.pl?subject=je%20suis%20cosmic%20car...&body=voila%20mon%20cot%C3%A9%20cosmique%20%3A%20..." aria-label={`Agrandir : ${photo.alt}`}
className={styles.ctaButton}
> >
prends la prochaine vague <Image src={photo.src} alt={photo.alt} fill sizes="(max-width: 640px) 50vw, 25vw" style={{ objectFit: 'cover' }} />
</a> <span className={styles.galleryCaption}>{photo.alt}</span>
<p className={styles.waitlistMessage}> </button>
pas de billetterie - cosmicfest c'est sur invitation. <br/> ))}
riders, come to the storm.
</p>
</div> </div>
</section> </section>
<section className={styles.section} id="off-festival"> {/* ── RSVP ── */}
<h2>🏄🏻‍♀️ et le off? 🏄🏽</h2> <section className={`${styles.section} ${styles.rsvp} ${styles.reveal}`}>
<p style={{textAlign: 'center', fontStyle: 'italic'}}> <p className={styles.stamp}>// jour J · samedi 22 août 2026</p>
🌊 RDV sur la plage ~ le dimanche soir après le démontage 🌊<br /> <h2 className={styles.sectionTitle}>tu es chaud ?</h2>
🔊 pour le <b>dernier</b> live ~ au <b>bunker</b> 🔊 <div className={styles.countdownContainer}>
<Countdown targetDate={JOUR_J} />
</div>
<a className={styles.ctaButton} href={MAILTO}>
réponds à l&rsquo;invitation
</a>
<p className={styles.waitlistMessage}>
Une réponse avant le <b>1er août</b>, ce serait top. Pas de billetterie : cosmicfest, c&rsquo;est
sur invitation. <b>Riders, come to the storm.</b>
</p> </p>
</section> </section>
</main> </main>
{isModalOpen && ( {/* ── LIGHTBOX ── */}
<div className={styles.modalOverlay} onClick={closeModal} role="dialog" aria-modal="true" aria-label={modalImageAlt || 'Image viewer'}> {modal && (
<div
className={styles.modalOverlay}
onClick={closeModal}
role="dialog"
aria-modal="true"
aria-label={modal.alt}
>
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}> <div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
<button className={styles.modalCloseButton} onClick={closeModal} aria-label="Close image viewer"> <button ref={closeBtnRef} className={styles.modalCloseButton} onClick={closeModal} aria-label="Fermer">
&times; &times;
</button> </button>
{/* For Next/Image, provide intrinsic image width/height if known, or use layout fill with sized parent.
Using layout="responsive" with some default aspect ratio here.
Actual display size will be controlled by CSS for modalImage container. */}
<div className={styles.modalImageContainer}> <div className={styles.modalImageContainer}>
<Image src={modalImageSrc} alt={modalImageAlt} layout="intrinsic" width={1200} height={900} objectFit="contain" /> {/* Full-size view of a local asset: plain img is more reliable here than the
optimizer, which chokes on the large source at full width. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={modal.src} alt={modal.alt} />
</div> </div>
{modalImageAlt && <p className={styles.modalCaption}>{modalImageAlt}</p>} <p className={styles.modalCaption}>{modal.alt}</p>
</div> </div>
</div> </div>
)} )}
<footer className={styles.footer}> <footer className={styles.footer}>
<a <a href="/parvagues/">
href="/parvagues/" vibe ~ <b>parvagues</b>
>
vibe ~
<span style={{fontWeight: 'bold'}}>parvagues</span>
{/* <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} /> You can use ParVagues logo here */}
</a> </a>
</footer> </footer>
{/* <ParVaguesFooter /> */} {/* Or use the existing footer */}
</div> </div>
); );
} }
/* Styles for cosmicfest */ /* cosmicfest_v2.67Hz — intrication
/* vibe: Surfers cross Techno; années 60 cross cyberpunk, Biarritz cross Paris cross Berlin cross Bayonne, Waves of rhythms, ocean and code, VibeCoding meets LiveCoding meets SkateBoarding meets Rock Climbing on former german bunkers meets long nights of chats and fun and drinks and dance and love and freedom */ Concept: two states, one system. A dark cosmic ground, two accents (cyan + magenta)
/* aesthetic: lowercase, light theme, chill et oceanique, mais pas cliche baba cool, juste posé. */ that always travel together the way entangled states do, resolving to white where they meet.
Fonts: Archivo (variable, wdth axis) for display+body, Martian Mono for instrument/data.
See DESIGN.md at repo root. */
.page {
/* ── tokens ── */
--cf-bg: oklch(0.15 0.03 275);
--cf-bg-deep: oklch(0.11 0.025 275);
--cf-surface: oklch(0.19 0.035 278);
--cf-surface-2: oklch(0.23 0.04 280);
--cf-border: oklch(1 0 0 / 0.10);
--cf-border-strong: oklch(1 0 0 / 0.18);
--cf-ink: oklch(0.97 0.01 270);
--cf-muted: oklch(0.80 0.02 272);
--cf-cyan: oklch(0.84 0.14 200);
--cf-magenta: oklch(0.72 0.21 350);
--cf-white: oklch(0.99 0 0);
--cf-amber: oklch(0.82 0.15 75);
--cf-cyan-soft: oklch(0.84 0.14 200 / 0.14);
--cf-magenta-soft: oklch(0.72 0.21 350 / 0.14);
--cf-font-sans: var(--font-cf-sans), system-ui, sans-serif;
--cf-font-mono: var(--font-cf-mono), ui-monospace, monospace;
--cf-z-canvas: 0;
--cf-z-content: 1;
--cf-z-overlay: 50;
--cf-z-modal: 100;
--cf-maxw: 64rem;
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;700&family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap');
/*
font-family: 'Montserrat', sans-serif; // For general text, headings
font-family: 'Space Mono', monospace; // For techy bits, or accent text
*/
.container {
padding: 0;
margin: 0; margin: 0;
background-color: #fafafa; /* Light, slightly off-white for a softer feel */ padding: 0;
color: #2c3e50; /* Dark blue-grey, less harsh than black */
font-family: 'Montserrat', sans-serif;
font-weight: 300;
line-height: 1.6;
text-transform: lowercase; /* Global lowercase aesthetic */
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--cf-bg);
color: var(--cf-ink);
font-family: var(--cf-font-sans);
font-weight: 400;
font-stretch: 100%;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
} }
.main { .main {
flex: 1; /* Ensures footer stays at bottom */ flex: 1;
/* padding: 4rem 0; remove default padding to allow full-width hero */ }
/* ── shared display / mono helpers ── */
.page :where(h1, h2, h3) {
font-family: var(--cf-font-sans);
font-stretch: 125%;
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.02;
text-wrap: balance;
margin: 0;
} }
.mono {
font-family: var(--cf-font-mono);
font-stretch: 100%;
letter-spacing: -0.01em;
}
/* ════════════════════════════════════════════ HERO ══ */
.hero { .hero {
width: 100%; position: relative;
height: 80vh; /* Taller hero */ min-height: 100svh;
min-height: 400px;
background-color: #3498db; /* Placeholder - vibrant blue, like deep ocean */
background-image: url('/images/cosmicfest/hero.jpg');
background-size: cover;
background-position: center 60%; /* Focus slightly lower than true center */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; /* Center content vertically */ justify-content: center;
align-items: center; /* Center content horizontally */ align-items: flex-start;
text-align: center; overflow: hidden;
color: #ffffff; padding: clamp(1.5rem, 5vw, 4rem);
padding: 2rem; background:
position: relative; /* For potential overlay or pseudo-elements */ radial-gradient(120% 80% at 80% 0%, var(--cf-magenta-soft), transparent 55%),
radial-gradient(120% 90% at 10% 100%, var(--cf-cyan-soft), transparent 55%),
var(--cf-bg-deep);
} }
.hero::before { /* Subtle vignette or color overlay */ .heroCanvas {
content: '';
position: absolute; position: absolute;
top: 0; inset: 0;
left: 0; width: 100%;
right: 0; height: 100%;
bottom: 0; z-index: var(--cf-z-canvas);
background: rgba(0, 0, 0, 0.2); /* Dark overlay to help text pop */ display: block;
/* background: linear-gradient(to bottom, rgba(52, 152, 219, 0.1), rgba(26, 82, 118, 0.5)); /* Ocean gradient overlay */
}
.hero h1 {
font-family: 'Space Mono', monospace;
font-size: clamp(2.5rem, 8vw, 5.5rem); /* Responsive font size */
margin-bottom: 0.5rem;
text-shadow: 0px 2px 8px rgba(0,0,0,0.7);
font-weight: 700;
letter-spacing: 0.05em;
z-index: 1; /* Above overlay */
} }
.hero p { .heroInner {
font-size: clamp(1rem, 3vw, 1.5rem); position: relative;
text-shadow: 0px 1px 4px rgba(0,0,0,0.6); z-index: var(--cf-z-content);
max-width: 600px; /* Keep tagline readable */ max-width: var(--cf-maxw);
font-weight: 400; width: 100%;
z-index: 1; /* Above overlay */ margin: 0 auto;
} }
.section { .freq {
margin: 3rem auto; /* More vertical space between sections */ display: inline-flex;
padding: 2rem clamp(1rem, 5vw, 3rem); /* Responsive horizontal padding */ align-items: center;
max-width: 1000px; /* Max content width */ gap: 0.55rem;
background-color: rgba(255, 255, 255, 0.9); /* Slightly transparent white for depth */ font-family: var(--cf-font-mono);
border-radius: 12px; /* Softer corners */ font-size: clamp(0.75rem, 1.6vw, 0.95rem);
box-shadow: 0 8px 25px rgba(44, 62, 80, 0.08); /* Softer, more diffused shadow */ letter-spacing: 0.04em;
} color: var(--cf-cyan);
border: 1px solid var(--cf-border-strong);
.section h2 { border-radius: 999px;
font-family: 'Space Mono', monospace; padding: 0.35rem 0.9rem;
font-size: clamp(1.8rem, 5vw, 2.8rem); background: oklch(0.15 0.03 275 / 0.5);
margin-bottom: 2rem; /* More space after heading */ backdrop-filter: blur(6px);
color: #2980b9; /* Ocean blue, slightly darker than hero for contrast */ }
text-align: center; .freq b { color: var(--cf-magenta); font-weight: 600; }
font-weight: 700; .freqDot {
letter-spacing: 0.03em; width: 0.5rem; height: 0.5rem; border-radius: 50%;
background: var(--cf-amber);
box-shadow: 0 0 10px var(--cf-amber);
}
.wordmark {
font-size: clamp(2.8rem, 13vw, 7rem);
font-stretch: 125%;
font-weight: 900;
letter-spacing: -0.04em;
line-height: 0.9;
margin: 1.2rem 0 0;
color: var(--cf-white);
}
.wordmark em {
font-style: normal;
/* the two entangled states meeting: cyan + magenta glow, solid white core */
color: var(--cf-white);
text-shadow:
-0.04em 0 0 oklch(0.84 0.14 200 / 0.55),
0.04em 0 0 oklch(0.72 0.21 350 / 0.55);
}
.heroTagline {
font-size: clamp(1.05rem, 2.6vw, 1.5rem);
max-width: 42ch;
margin: 1.4rem 0 0;
color: var(--cf-ink);
text-wrap: pretty;
font-weight: 500;
}
.heroMeta {
display: flex;
flex-wrap: wrap;
gap: 0.6rem 1.1rem;
margin: 1.6rem 0 0;
font-family: var(--cf-font-mono);
font-size: clamp(0.8rem, 1.8vw, 0.95rem);
color: var(--cf-muted);
} }
.heroMeta span { display: inline-flex; align-items: center; gap: 0.5rem; }
.grid { .heroMeta span::before {
display: grid; /* Using grid for more control */ content: "";
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); /* Responsive columns */ width: 0.4rem; height: 0.4rem; border-radius: 50%;
gap: 1.5rem; /* Space between cards */ background: var(--cf-cyan);
} }
.heroMeta span:nth-child(even)::before { background: var(--cf-magenta); }
.card { .heroActions { margin-top: 2.2rem; display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
background-color: #ffffff;
padding: 1rem; /* Reduced padding */
border-radius: 8px;
box-shadow: 0 4px 15px rgba(44, 62, 80, 0.05);
transition: transform 0.3s ease-out, box-shadow 0.3s ease-out;
text-align: left;
}
.card:hover { .scrollCue {
transform: translateY(-5px); position: absolute;
box-shadow: 0 8px 25px rgba(44, 62, 80, 0.1); bottom: 1.4rem;
left: 50%;
transform: translateX(-50%);
z-index: var(--cf-z-content);
font-family: var(--cf-font-mono);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--cf-muted);
animation: cuePulse 2.4s ease-in-out infinite;
}
@keyframes cuePulse {
0%, 100% { opacity: 0.35; transform: translateX(-50%) translateY(0); }
50% { opacity: 1; transform: translateX(-50%) translateY(4px); }
}
/* ════════════════════════════════════════════ SECTIONS ══ */
.section {
max-width: var(--cf-maxw);
margin: 0 auto;
padding: clamp(3.5rem, 9vw, 7rem) clamp(1.5rem, 5vw, 3rem);
} }
.card h3 { .sectionTitle {
font-family: 'Montserrat', sans-serif; font-size: clamp(1.8rem, 5.5vw, 3.4rem);
font-size: 1.25rem; /* Reduced font size */ color: var(--cf-white);
margin-bottom: 0.5rem; /* Reduced margin */
color: #16a085; /* Teal/Turquoise for a fresh vibe */
font-weight: 700; /* Bolder artist names */
} }
.sectionLede {
.card p { margin: 1.1rem 0 0;
font-size: 0.9rem; /* Reduced font size */ max-width: 60ch;
line-height: 1.4; /* Added for better readability with smaller font */ color: var(--cf-muted);
color: #34495e; /* Slightly lighter text for description */ font-size: clamp(1rem, 2.2vw, 1.18rem);
text-wrap: pretty;
} }
/* Specific styling for the emoji line in the card */ .stamp {
.card p:last-child { font-family: var(--cf-font-mono);
margin-top: 0.5rem; /* Add some space above the emoji line */ font-size: 0.78rem;
margin-bottom: 0; /* Remove bottom margin if it's the last element */ letter-spacing: 0.02em;
color: var(--cf-cyan);
} }
.countdownContainer { /* Renamed to avoid conflict if .countdown is too generic */ /* ── concept / intrication ── */
.concept {
text-align: center; text-align: center;
margin: 2.5rem 0; border-top: 1px solid var(--cf-border);
padding: 2rem; border-bottom: 1px solid var(--cf-border);
background-color: rgba(236, 240, 241, 0.7); /* Light grey, slightly transparent */
border-radius: 8px;
}
/* Style for individual countdown items is in Countdown.js for now, can be moved here */
.countdownContainer span div:first-child { /* The number */
font-family: 'Space Mono', monospace;
color: #e67e22; /* Warm orange, like sunset */
font-size: clamp(2rem, 6vw, 3.5rem);
}
.countdownContainer span div:last-child { /* The label (jours, heures) */
color: #7f8c8d; /* Muted grey */
font-size: clamp(0.7rem, 2vw, 0.9rem);
} }
.conceptQuote {
font-size: clamp(1.5rem, 4.5vw, 2.7rem);
.ctaButton { font-stretch: 110%;
display: inline-block; font-weight: 700;
background-color: #007ef9; /* Coral/Red for strong CTA */ line-height: 1.18;
color: white; letter-spacing: -0.02em;
padding: 0.8rem 2.5rem; /* Adjusted padding */ max-width: 22ch;
border-radius: 50px; /* Pill shape */ margin: 0 auto;
text-decoration: none; color: var(--cf-ink);
font-size: 1.2rem; text-wrap: balance;
font-weight: 700; /* Bolder CTA text */ }
transition: background-color 0.2s ease, transform 0.2s ease; .conceptQuote .a { color: var(--cf-cyan); }
border: none; .conceptQuote .b { color: var(--cf-magenta); }
cursor: pointer;
box-shadow: 0 4px 10px rgba(231, 76, 60, 0.3); /* ── lineup ── */
font-family: 'Montserrat', sans-serif; /* Consistent font */ .lineup { list-style: none; padding: 0; margin: 2.5rem 0 0; }
.lineupRow {
display: grid;
grid-template-columns: 1fr auto;
align-items: baseline;
gap: 0.4rem 1.5rem;
padding: 1.4rem 0;
border-top: 1px solid var(--cf-border);
/* transform, not padding — keeps the hover slide off the layout thread */
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.lineupRow:last-child { border-bottom: 1px solid var(--cf-border); }
.lineupRow:hover { transform: translateX(0.5rem); }
.lineupName {
font-size: clamp(1.5rem, 5vw, 2.6rem);
font-stretch: 120%;
font-weight: 800;
letter-spacing: -0.03em;
color: var(--cf-white);
line-height: 1;
}
.lineupRow:nth-child(odd) .lineupName:hover { color: var(--cf-cyan); }
.lineupRow:nth-child(even) .lineupName:hover { color: var(--cf-magenta); }
.lineupReal { color: var(--cf-muted); font-weight: 500; font-size: 0.5em; font-stretch: 100%; }
.lineupRole {
font-family: var(--cf-font-mono);
font-size: clamp(0.78rem, 1.8vw, 0.92rem);
color: var(--cf-muted);
text-align: right;
}
.lineupGuests {
margin-top: 1.6rem;
font-family: var(--cf-font-mono);
font-size: 0.9rem;
color: var(--cf-muted);
} }
.lineupGuests b { color: var(--cf-amber); font-weight: 600; }
.ctaButton:hover { /* ── programme chips ── */
background-color: #c0392b; /* Darker red on hover */ .chips { display: flex; flex-wrap: wrap; gap: 0.7rem; margin: 2.2rem 0 0; }
transform: translateY(-2px); .chip {
box-shadow: 0 6px 15px rgba(231, 76, 60, 0.4); font-family: var(--cf-font-mono);
font-size: 0.92rem;
padding: 0.5rem 1rem;
border: 1px solid var(--cf-border);
border-radius: 999px;
background: var(--cf-surface);
color: var(--cf-ink);
} }
.waitlistMessage { /* ── definition-style logistics blocks ── */
text-align: center; .defs { margin: 2.2rem 0 0; display: grid; gap: 0; }
margin-top: 1.5rem; /* More space */ .def {
font-style: normal; /* Less italic, more direct */ display: grid;
color: #7f8c8d; /* Muted grey */ grid-template-columns: minmax(8rem, 14rem) 1fr;
font-size: 0.9rem; gap: 0.4rem 2rem;
line-height: 1.4; padding: 1.2rem 0;
border-top: 1px solid var(--cf-border);
align-items: start;
}
.def:last-child { border-bottom: 1px solid var(--cf-border); }
.defKey {
font-family: var(--cf-font-mono);
font-size: 0.85rem;
letter-spacing: 0.01em;
color: var(--cf-cyan);
padding-top: 0.15rem;
}
.def:nth-child(even) .defKey { color: var(--cf-magenta); }
.defVal { color: var(--cf-ink); }
.defVal p { margin: 0; }
.defVal p + p { margin-top: 0.5rem; }
.defVal .sub { color: var(--cf-muted); font-size: 0.95rem; }
/* ── packing grid ── */
.packGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 0.8rem;
margin: 2.2rem 0 0;
} }
.packItem {
.footer {
width: 100%;
padding: 2.5rem 1rem; /* More padding */
border-top: 1px solid #ecf0f1; /* Lighter border */
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
background-color: #ffffff; /* Clean white footer */ gap: 0.7rem;
color: #95a5a6; /* Muted text color for footer */ padding: 0.9rem 1rem;
font-size: 0.9rem; border: 1px solid var(--cf-border);
} border-radius: 10px;
background: var(--cf-surface);
.audioEmbed { /* This class is now on the individual player wrappers */ color: var(--cf-ink);
display: flex;
justify-content: center;
/* margin: 1rem 0; Retained for stacking, but might be overridden by flex container gap or item margins */
} }
.packItem span:first-child { font-size: 1.2rem; }
/* ════════════════════════════════════════════ SOUVENIRS V0 ══ */
.souvenirs { border-top: 1px solid var(--cf-border); }
.audioPlayersContainer { .audioPlayersContainer {
display: flex; display: flex;
flex-direction: row; flex-wrap: wrap;
flex-wrap: wrap; /* Allow players to wrap to next line if not enough space */ gap: 1.5rem;
justify-content: space-around; /* Distribute space around players */ margin: 2.2rem 0 0;
align-items: flex-start; /* Align items to the top if heights differ slightly */ align-items: flex-start;
gap: 2rem; /* Spacing between players when side-by-side */ }
margin-top: 1.5rem; /* Space above the container */ .audioEmbed { display: flex; justify-content: center; }
.bandcampPlayer { flex: 1 1 320px; min-width: 280px; }
.soundcloudPlayer { flex: 1 1 380px; min-width: 280px; max-width: 460px; }
/* Surface + border behind the third-party iframes so they read as a deliberate
panel while loading (and on slow networks), not a black void. */
.bandcampPlayer iframe,
.soundcloudPlayer iframe {
max-width: 100%;
border-radius: 10px;
background: var(--cf-surface);
border: 1px solid var(--cf-border);
} }
.soundcloudPlayer iframe { width: 100%; max-width: 460px; height: 380px; }
.bandcampPlayer { .gallery {
flex: 1 1 350px; display: grid;
min-width: 300px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
margin: 0.5rem; gap: 0.9rem;
margin: 2rem 0 0;
} }
.soundcloudPlayer { .galleryItem {
flex: 1 1 450px; /* Adjusted flex-basis for SoundCloud player */ position: relative;
min-width: 300px; /* Still allow shrinking if necessary */ width: 100%;
max-width: 450px; /* Ensure it doesn't grow beyond 450px unless flex grow allows */ aspect-ratio: 4 / 3;
margin: 0.5rem; border-radius: 10px;
overflow: hidden;
border: 1px solid var(--cf-border);
background: var(--cf-surface);
cursor: pointer;
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.3s ease;
} }
.galleryItem:hover, .galleryItem:focus-visible {
.bandcampPlayer iframe { transform: translateY(-4px);
max-width: 100%; /* Responsive within its container */ border-color: var(--cf-cyan);
/* width and height are set inline on the iframe by Bandcamp's embed usually, or by our style attribute */ outline: none;
} }
.galleryItem img { object-fit: cover; }
.soundcloudPlayer iframe { .galleryCaption {
width: 450px; position: absolute;
height: 450px; left: 0; right: 0; bottom: 0;
max-width: 100%; /* Allows it to shrink if container is smaller than 450px */ font-family: var(--cf-font-mono);
font-size: 0.74rem;
color: var(--cf-white);
padding: 0.5rem 0.7rem;
background: linear-gradient(to top, oklch(0.11 0.025 275 / 0.92), transparent);
z-index: 1;
} }
/* ════════════════════════════════════════════ RSVP ══ */
/* Media query for smaller screens to stack players */ .rsvp {
@media (max-width: 768px) { /* Breakpoint can be adjusted based on when it looks best */ text-align: center;
.audioPlayersContainer { border-top: 1px solid var(--cf-border);
flex-direction: column;
align-items: center; /* Center players when stacked */
}
.bandcampPlayer, .soundcloudPlayer {
flex-basis: auto; /* Reset flex-basis for stacked layout */
width: 100%; /* Make players take full width when stacked */
max-width: 400px; /* Optional: constrain max width when stacked, e.g. for SoundCloud visual player */
margin: 1rem 0; /* Adjust margin for vertical stacking */
}
.hero {
height: 70vh; /* Adjust hero height */
}
.section {
margin: 2rem auto;
padding: 1.5rem clamp(0.5rem, 3vw, 1.5rem);
}
.grid {
grid-template-columns: 1fr; /* Stack cards on smaller screens */
}
.card {
width: auto; /* Full width */
}
.card h3 {
font-size: 1.1rem; /* Further reduce font size on small screens for title/emojis */
}
.countdownContainer span div:first-child { /* The number */
font-size: clamp(1.8rem, 5vw, 3rem);
}
} }
.countdownContainer { margin: 2.4rem auto; max-width: 40rem; }
.countdown {
display: flex;
justify-content: center;
gap: clamp(1rem, 5vw, 2.5rem);
flex-wrap: wrap;
}
.countUnit { text-align: center; }
.countNum {
font-family: var(--cf-font-mono);
font-size: clamp(2rem, 8vw, 3.6rem);
font-weight: 600;
line-height: 1;
color: var(--cf-white);
font-variant-numeric: tabular-nums;
}
.countUnit:nth-child(odd) .countNum { color: var(--cf-cyan); }
.countUnit:nth-child(even) .countNum { color: var(--cf-magenta); }
.countLabel {
display: block;
margin-top: 0.5rem;
font-family: var(--cf-font-mono);
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--cf-muted);
}
.countDone { font-size: 1.6rem; font-weight: 700; color: var(--cf-amber); }
.footer a { .ctaButton {
display: inline-flex;
align-items: center;
gap: 0.6rem;
background: var(--cf-white);
color: oklch(0.13 0.03 275);
padding: 0.95rem 2.2rem;
border-radius: 999px;
text-decoration: none; text-decoration: none;
color: #3498db; /* Link color for parvagues */ font-weight: 700;
transition: color 0.2s ease; font-stretch: 110%;
font-size: 1.05rem;
border: none;
cursor: pointer;
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s ease;
box-shadow: 0 0 0 0 oklch(0.84 0.14 200 / 0.5);
} }
.footer a:hover { .ctaButton:hover {
color: #2980b9; /* Darker blue on hover */ transform: translateY(-2px);
box-shadow: 0 8px 30px oklch(0.72 0.21 350 / 0.35), 0 0 0 1px var(--cf-border-strong);
} }
.footer span { .ctaButton:active { transform: translateY(0); transition-duration: 0.08s; }
margin-left: 0.3rem; .ctaGhost {
font-weight: 400; /* Normal weight for 'parvagues' in footer */ display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--cf-ink);
text-decoration: none;
font-family: var(--cf-font-mono);
font-size: 0.92rem;
padding: 0.95rem 0.4rem;
border-bottom: 1px solid var(--cf-border-strong);
transition: color 0.2s ease, border-color 0.2s ease;
} }
.ctaGhost:hover { color: var(--cf-cyan); border-color: var(--cf-cyan); }
.waitlistMessage {
/* Image gallery styles */ margin: 1.8rem auto 0;
.gallery { max-width: 44ch;
display: grid; color: var(--cf-muted);
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); /* Increased min item width */ font-size: 0.95rem;
gap: 1rem; line-height: 1.6;
} }
.waitlistMessage b { color: var(--cf-ink); font-weight: 600; }
/* Common styles for all gallery items (photos and video links) */ /* ════════════════════════════════════════════ FOOTER ══ */
.galleryItem { .footer {
width: 100%; width: 100%;
height: 200px; /* Increased height */ padding: 3rem 1.5rem;
border-radius: 8px; /* Consistent rounding */ border-top: 1px solid var(--cf-border);
background-color: #ecf0f1; /* Light placeholder color for items without BG image */ display: flex;
display: flex; /* Enables flexbox for content alignment */ justify-content: center;
overflow: hidden; /* Ensure content fits */ align-items: center;
position: relative; /* For absolute positioning of children or pseudo-elements */ gap: 0.4rem;
transition: transform 0.3s ease, box-shadow 0.3s ease; background: var(--cf-bg-deep);
text-decoration: none; /* Remove underline from links if .galleryItem is an <a> */ font-family: var(--cf-font-mono);
color: #7f8c8d; /* Default text color, useful for video links */
}
.galleryItem:hover {
transform: scale(1.03);
box-shadow: 0 5px 15px rgba(44, 62, 80, 0.1);
}
/* Specific styles for photo items (divs with background images) */
.galleryPhotoItem {
background-size: cover;
background-position: center;
justify-content: center; /* Center content horizontally (span) */
align-items: flex-end; /* Align content (span) to the bottom */
min-height: 150px; /* Ensure a minimum height as per user feedback, though .galleryItem height should cover this */
}
/* Styling for the text overlay (alt text) on photo items */
.galleryPhotoItem span {
color: #fff;
background-color: rgba(0,0,0,0.7); /* Slightly darker for better contrast */
padding: 0.4em 0.8em; /* Increased padding */
border-radius: 4px 4px 0 0; /* Rounded top corners if at bottom edge */
width: 100%; /* Make span take full width of the item */
text-align: center;
font-size: 0.85rem; /* Slightly smaller font for alt text */
z-index: 1;
box-sizing: border-box; /* Ensure padding doesn't increase width beyond 100% */
}
/* Specific styles for video link items (<a> tags) */
.galleryVideoLink { /* This will be used for the <a> tags for videos */
justify-content: center; /* Center text horizontally */
align-items: center; /* Center text vertically */
text-align: center;
font-size: 0.9rem; font-size: 0.9rem;
background-color: #333; /* Darker for video links */ color: var(--cf-muted);
color: #fff; /* Text color for video links */
/* height: 180px; Inherits from galleryItem. If different height is needed, uncomment and set. */
}
.galleryVideoLink:hover {
background-color: #444;
/* Potentially darken the background image or text background on hover too */
} }
.footer a { color: var(--cf-muted); text-decoration: none; transition: color 0.2s ease; }
.footer a:hover { color: var(--cf-cyan); }
.footer a b { color: var(--cf-ink); font-weight: 700; }
/* Styles for Photo Modal */ /* ════════════════════════════════════════════ LIGHTBOX ══ */
.modalOverlay { .modalOverlay {
position: fixed; position: fixed;
top: 0; inset: 0;
left: 0; background: oklch(0.08 0.02 275 / 0.9);
width: 100%; backdrop-filter: blur(8px);
height: 100%;
background-color: rgba(0, 0, 0, 0.85); /* Darker overlay */
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1000; /* Ensure it's on top */ z-index: var(--cf-z-modal);
cursor: pointer; /* Indicate overlay can be clicked to close */ cursor: pointer;
padding: 1.5rem;
animation: fadeIn 0.25s ease;
} }
.modalContent { .modalContent {
background-color: #2c3e50; /* Dark theme for modal content box */ position: relative;
padding: 20px; max-width: 92vw;
border-radius: 8px; max-height: 90vh;
box-shadow: 0 5px 25px rgba(0,0,0,0.5); cursor: default;
position: relative; /* For close button positioning */
cursor: default; /* Reset cursor for content area */
max-width: 90vw; /* Max width relative to viewport width */
max-height: 90vh; /* Max height relative to viewport height */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} gap: 0.8rem;
}
.modalImageContainer { .modalImageContainer { display: flex; justify-content: center; }
width: 100%; /* Container takes width of modalContent or image's intrinsic */ .modalImageContainer img {
max-height: calc(85vh - 80px); /* Adjust based on padding and caption height */ max-width: 92vw;
display: flex; /* Helps with centering the image if it's smaller than container */ max-height: 80vh;
justify-content: center; width: auto;
align-items: center; height: auto;
overflow: hidden; /* In case image is larger than this container */ border-radius: 10px;
} border: 1px solid var(--cf-border-strong);
}
/* next/image specific styling might need to target its internal structure if needed, .modalCaption { color: var(--cf-muted); font-family: var(--cf-font-mono); font-size: 0.82rem; text-align: center; }
but objectFit="contain" and layout="intrinsic" should handle most cases.
The parent .modalImageContainer will constrain its max size. */
.modalImageContainer img { /* Targets the actual img tag from Next/Image */
max-width: 100%;
max-height: 100%; /* Ensure image scales down to fit container */
object-fit: contain; /* Redundant with next/image prop but good fallback */
}
.modalCloseButton { .modalCloseButton {
position: absolute; position: absolute;
top: -10px; /* Position outside slightly or adjust padding */ top: -1rem; right: -1rem;
right: -10px; width: 2.4rem; height: 2.4rem;
background: #e74c3c;
color: white;
border: none;
border-radius: 50%; border-radius: 50%;
width: 35px; border: 1px solid var(--cf-border-strong);
height: 35px; background: var(--cf-surface);
font-size: 1.8rem; color: var(--cf-ink);
line-height: 30px; /* Center the '×' */ font-size: 1.4rem;
text-align: center; line-height: 1;
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.3); transition: background 0.2s ease, color 0.2s ease;
transition: background-color 0.2s ease;
z-index: 1001; /* Above modal content */
} }
.modalCloseButton:hover { background: var(--cf-magenta); color: oklch(0.13 0.03 275); }
.modalCloseButton:hover { @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
background-color: #c0392b;
}
.modalCaption { /* ════════════════════════════════════════════ REVEAL ══
color: #ecf0f1; /* Light text for caption */ Content is visible by default. Only once JS has mounted (.jsReady is added to the
text-align: center; page root, and never under reduced-motion) do reveals start hidden and animate in.
margin-top: 10px; This keeps headless renders, hidden tabs, and no-JS from shipping blank sections. */
font-size: 0.9rem; .jsReady .reveal {
max-width: 100%; opacity: 0;
transform: translateY(24px);
transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
} }
.jsReady .reveal.revealIn { opacity: 1; transform: none; }
/* Styling for the text overlay on video links (if they have a background image) */ /* ════════════════════════════════════════════ RESPONSIVE ══ */
.galleryVideoLink span { @media (max-width: 640px) {
color: #fff; .def { grid-template-columns: 1fr; gap: 0.3rem; }
background-color: rgba(0,0,0,0.7); /* Consistent with photo item caption background */ .lineupRow { grid-template-columns: 1fr; }
padding: 0.4em 0.8em; .lineupRole { text-align: left; }
border-radius: 4px; .heroMeta { gap: 0.5rem 0.9rem; }
z-index: 1;
box-sizing: border-box;
max-width: 90%; /* Prevent text from touching edges if too long */
text-align: center; /* Ensure text within span is centered */
}
/* Remove the old generic .gallery a and .gallery div span if they are fully replaced */
/* The old .gallery a styling for height:30px is now incorporated or decided by .galleryVideoLink with .galleryItem height */
/* Section specific styling for V0 */
#v0 h3 {
font-family: 'Montserrat', sans-serif;
font-size: 1.2rem;
color: #2c3e50;
margin-top: 2.5rem;
margin-bottom: 1rem;
text-align: left;
font-weight: 700;
border-bottom: 1px solid #ecf0f1;
padding-bottom: 0.5rem;
}
#v0 ul {
list-style: none;
padding-left: 0;
text-align: left;
}
#v0 li {
padding: 0.3rem 0;
color: #34495e;
} }
/* Festival Off */ /* ════════════════════════════════════════════ REDUCED MOTION ══ */
#off-festival p { @media (prefers-reduced-motion: reduce) {
font-family: 'Space Mono', monospace; .page *, .page *::before, .page *::after {
color: #9b59b6; /* Mysterious purple */ animation-duration: 0.001ms !important;
font-size: 1.1rem; animation-iteration-count: 1 !important;
line-height: 1.7; transition-duration: 0.001ms !important;
}
.reveal { opacity: 1; transform: none; }
.scrollCue { animation: none; }
} }
/* Accessibility: Focus indicator */ /* ════════════════════════════════════════════ FOCUS ══ */
/*:global(a:focus), :global(button:focus), :global(input:focus) {*/ .page a:focus-visible,
/* outline: 2px solid #3498db;*/ .page button:focus-visible {
/* outline-offset: 2px;*/ outline: 2px solid var(--cf-cyan);
/*}*/ outline-offset: 3px;
border-radius: 2px;
/* Responsive adjustments for smaller screens */
@media (max-width: 768px) {
.hero {
height: 70vh; /* Adjust hero height */
}
.section {
margin: 2rem auto;
padding: 1.5rem clamp(0.5rem, 3vw, 1.5rem);
}
.grid {
grid-template-columns: 1fr; /* Stack cards on smaller screens */
}
.card {
width: auto; /* Full width */
}
.countdownContainer span div:first-child { /* The number */
font-size: clamp(1.8rem, 5vw, 3rem);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment