/* Cloudflare direct-upload runtime bundle.
   Generated because this environment has no Node/npm available to run Vite.
   It keeps your existing React source structure but loads React from CDN. */

const {
  StrictMode, Suspense, useCallback, useEffect, useMemo, useRef, useState,
  isValidElement
} = React;
const { createRoot } = ReactDOM;
const {
  BrowserRouter, Routes, Route, Navigate, Link, NavLink, Outlet,
  useLocation, useParams, useSearchParams
} = ReactRouterDOM;



/* ===== src/data/images.js ===== */
/* ===========================================================================
   CENTRAL IMAGE REGISTRY
   ---------------------------------------------------------------------------
   ⚠️  ALL IMAGES BELOW ARE TEMPORARY PLACEHOLDERS (free-to-use Unsplash stock).
       Replace them with real Eventuur photography before launch.

   HOW TO REPLACE AN IMAGE
   -----------------------
   1. Drop the file into /public/images/  (e.g. /public/images/bootcamp-hero.jpg)
   2. Change the entry below:
          bootcampHero: local('/images/bootcamp-hero.jpg', 'Alt text here'),
   3. Done. Nothing else in the codebase needs to change.

   Remote (Unsplash) entries automatically get a responsive srcset.
   Local entries are served as-is — pre-size them (see README → Images).
   ========================================================================= */

const UNSPLASH = 'https://images.unsplash.com/photo-'

/** Remote, resizable source (Unsplash CDN supports ?w= &q=). */
function remote(id, alt, focal = 'center') {
  return { kind: 'remote', src: `${UNSPLASH}${id}`, alt, focal, credit: 'Unsplash (placeholder)' }
}

/** Local file in /public. No automatic resizing — optimise before upload. */
function local(path, alt, focal = 'center') {
  return { kind: 'local', src: path, alt, focal, credit: '' }
}

/** Widths generated for remote images. */
const IMAGE_WIDTHS = [480, 768, 1024, 1440, 1920]

/** Build { src, srcSet, sizes } for an <img>. Used by <SmartImage />. */
function resolveImage(image, { width = 1440, sizes } = {}) {
  if (!image) return null
  if (image.kind === 'local') return { src: image.src, srcSet: undefined, sizes }
  const q = 68
  const base = `${image.src}?auto=format&fit=crop&q=${q}`
  return {
    src: `${base}&w=${width}`,
    srcSet: IMAGE_WIDTHS.map((w) => `${base}&w=${w} ${w}w`).join(', '),
    sizes: sizes || '100vw',
  }
}

/* ---------------------------------------------------------------------------
   THE REGISTRY
   Keys are referenced by name from data/events.js, data/news.js and pages.
   ------------------------------------------------------------------------- */
const images = {
  /* --- Hero / brand ----------------------------------------------------- */
  heroMain: remote('1521737604893-d14cc237f11d',
    'A team collaborating around a table during a company workshop'),
  heroAlt: remote('1517048676732-d65bc937f952',
    'Colleagues discussing ideas together in a bright workspace'),

  introCollage: remote('1543269865-cbf427effbad',
    'A group of colleagues working together at a long table'),
  introSecondary: remote('1552664730-d307ca884978',
    'Team members planning together with notes and sketches'),

  /* --- Signature experiences -------------------------------------------- */
  bootcampCover: remote('1552674605-db6ffd4facb5',
    'Group taking part in an outdoor bootcamp-style team session'),
  bootcampHero: remote('1534438327276-14e5300c3a48',
    'Team working out together during an outdoor training session'),
  bootcampAlt1: remote('1571019613454-1cb2f99b2d8b',
    'Participants encouraging each other during a physical challenge'),
  bootcampAlt2: remote('1461896836934-ffe607ba8211',
    'People running together outdoors as a group'),

  miniGamesCover: remote('1531058020387-3be344556be6',
    'Colleagues taking part in a creative indoor challenge'),
  miniGamesHero: remote('1517245386807-bb43f82c33c4',
    'Team gathered around a table solving a challenge together'),
  miniGamesAlt1: remote('1600880292203-757bb62b4baf',
    'Two colleagues working through a problem together'),
  miniGamesAlt2: remote('1522071820081-009f0129c71c',
    'A group in discussion during a collaborative session'),

  scavengerCover: remote('1530549387789-4c1017266635',
    'A team navigating outdoors together during a hunt'),
  scavengerHero: remote('1551632811-561732d1e306',
    'Group exploring an outdoor route as a team'),
  scavengerAlt1: remote('1488646953014-85cb44e25828',
    'Hands pointing at a map while planning a route'),
  scavengerAlt2: remote('1476514525535-07fb3b4ae5f1',
    'A winding road through a landscape at golden hour'),

  /* --- Other events ------------------------------------------------------ */
  networkingCover: remote('1511578314322-379afb476865',
    'People networking at a corporate evening event'),
  drinksCover: remote('1543007630-9710e4a00a20',
    'Colleagues raising glasses at an informal company gathering'),
  celebrationCover: remote('1519671482749-fd09be7ccebf',
    'A team celebrating together at a company event'),
  dinnerCover: remote('1414235077428-338989a2e8c0',
    'A long dinner table set for a group gathering'),
  customCover: remote('1540575467063-178a50c2df87',
    'A large audience at a professionally produced company event'),

  /* --- Section / editorial ---------------------------------------------- */
  whyBackdrop: remote('1470071459604-3b5ec3a7fe05',
    'Soft morning light through a forest'),
  customBackdrop: remote('1500534314209-a25ddb2bd429',
    'A wide natural landscape at sunrise'),
  ctaBackdrop: remote('1523580494863-6f3031224c94',
    'A large group gathered at an event venue'),
  aboutHero: remote('1505373877841-8d25f7d46678',
    'Colleagues in conversation in a relaxed office setting'),
  aboutSecondary: remote('1441974231531-c6227db76b6e',
    'Sunlight filtering through tall trees in a forest'),
  informationHero: remote('1497366216548-37526070297c',
    'An open-plan office where teams are working together'),
  loginAside: remote('1531482615713-2afd69097998',
    'A team working together at a shared desk'),

  /* --- News placeholders -------------------------------------------------- */
  newsSustainability: remote('1441974231531-c6227db76b6e',
    'Forest canopy seen from below'),
  newsBootcampLaunch: remote('1534438327276-14e5300c3a48',
    'Outdoor team training session'),
  newsCommunication: remote('1556761175-b413da4baf72',
    'Two colleagues in conversation in an office'),
  newsScavenger: remote('1530549387789-4c1017266635',
    'A group exploring outdoors as a team'),
  newsHybrid: remote('1517048676732-d65bc937f952',
    'Colleagues collaborating in a meeting space'),
}



/* ===== src/data/site.js ===== */
/* ===========================================================================
   SITE CONFIG — company info, navigation, CTAs, footer, SEO defaults.
   ✏️  EDIT HERE: anything global. Nothing below is hard-coded in components.
   ⚠️  PLACEHOLDER values are marked with `// PLACEHOLDER`.
   ========================================================================= */
const company = {
  name: 'Eventuur',
  tagline: 'Experiences with purpose.',
  legalName: 'Eventuur B.V.',                        // PLACEHOLDER
  foundedYear: 2025,                                  // PLACEHOLDER
  email: 'hello@eventuur.com',                        // PLACEHOLDER
  phone: '+31 (0)6 00 00 00 00',                      // PLACEHOLDER
  phoneHref: '+31600000000',                          // PLACEHOLDER
  address: {
    street: 'Address line 1',                         // PLACEHOLDER
    city: 'Amsterdam',                                // PLACEHOLDER
    country: 'The Netherlands',                       // PLACEHOLDER
  },
  chamberOfCommerce: 'KVK 00000000',                  // PLACEHOLDER
  vat: 'VAT NL000000000B00',                          // PLACEHOLDER
  socials: [
    { label: 'LinkedIn',  href: 'https://linkedin.com/company/eventuur' },  // PLACEHOLDER
    { label: 'Instagram', href: 'https://instagram.com/eventuur' },          // PLACEHOLDER
  ],
  serviceArea: 'The Netherlands & Europe',            // PLACEHOLDER
}

/** Absolute site URL — used for canonical + OG tags. Set VITE_SITE_URL in .env */
const siteUrl =
  'https://eventuur.com'

/** Primary navigation (header + footer). Order = display order. */
const navigation = [
  { label: 'Home',            to: '/' },
  { label: 'Events',          to: '/events' },
  { label: 'Get to Know Us',  to: '/get-to-know-us' },
  { label: 'News',            to: '/news' },
  { label: 'Information',     to: '/information' },
]

/** Reusable call-to-action labels + destinations. */
const cta = {
  primary:   { label: 'Plan Your Event',    to: '/contact' },
  secondary: { label: 'Explore Our Events', to: '/events' },
  contact:   { label: 'Contact Us',         to: '/contact' },
  contactLong: { label: 'Contact Eventuur', to: '/contact' },
  login:     { label: 'Login',              to: '/login' },
  custom:    { label: 'Create Your Event',  to: '/contact?type=custom' },
  request:   { label: 'Request This Event', to: '/contact' },
  discover:  { label: 'Discover Event' },
  allEvents: { label: 'View All Events',    to: '/events' },
  allNews:   { label: 'Read All News',      to: '/news' },
  planning:  { label: 'Start Planning' },
}

/** Footer link columns. */
const footerColumns = [
  {
    title: 'Experiences',
    links: [
      { label: 'Bootcamp',       to: '/events/bootcamp' },
      { label: 'Mini Games',     to: '/events/mini-games' },
      { label: 'Scavenger Hunt', to: '/events/scavenger-hunt' },
      { label: 'All Events',     to: '/events' },
    ],
  },
  {
    title: 'Company',
    links: [
      { label: 'Get to Know Us', to: '/get-to-know-us' },
      { label: 'Information',    to: '/information' },
      { label: 'News',           to: '/news' },
      { label: 'Contact',        to: '/contact' },
    ],
  },
  {
    title: 'Account',
    links: [
      { label: 'Login',        to: '/login' },
      { label: 'Request Event', to: '/contact' },
    ],
  },
]

/** Shown in the footer bottom bar. Pages do not exist yet — see README. */
const legalLinks = [
  { label: 'Privacy',  to: '/contact' },   // PLACEHOLDER → create /privacy later
  { label: 'Terms',    to: '/contact' },   // PLACEHOLDER → create /terms later
]

/** Default SEO — per-page overrides live in each page component. */
const seoDefaults = {
  titleTemplate: '%s | Eventuur',
  defaultTitle: 'Eventuur — More Than an Event. An Experience With Purpose.',
  description:
    'Eventuur designs tailored corporate experiences that bring teams together, strengthen communication and turn company events into something meaningful.',
  ogImage: '/og-default.jpg',              // PLACEHOLDER — add to /public
  locale: 'en',
}

/** Rotating strip of value words (homepage marquee). */
const marqueeWords = [
  'Communication', 'Connection', 'Collaboration', 'Efficiency',
  'Problem Solving', 'Sustainability', 'Energy', 'Trust', 'Fun with Purpose',
]


/* ===== src/data/content.js ===== */
/* ===========================================================================
   PAGE CONTENT
   Section copy for the homepage and the static pages, kept out of components.
   ✏️  Edit any headline, paragraph or list item here.
   ========================================================================= */

/* ------------------------------ HOMEPAGE --------------------------------- */
const hero = {
  eyebrow: 'Corporate experiences with purpose',
  title: 'More Than an Event. An Experience With Purpose.',
  lead:
    'We create tailored experiences that bring teams together, strengthen communication, and turn company events into something meaningful.',
  stats: [
    { value: 'Fun', label: 'that people actually enjoy' },
    { value: 'Purpose', label: 'built into every format' },
    { value: 'Tailored', label: 'to your team, not a catalogue' },
  ],
}
const intro = {
  eyebrow: 'Who we are',
  title: 'Events that bring people together.',
  paragraphs: [
    'Eventuur designs corporate experiences around five things: communication, connection, efficiency, teamwork and sustainability.',
    'We are not a catalogue you pick from. We start with what your team needs, then build the day around it.',
  ],
  cta: { label: 'Get to know us', to: '/get-to-know-us' },
}
const whatWeDo = {
  eyebrow: 'What we do',
  title: 'Five things every Eventuur experience is built around.',
  lead: 'Not themes on a slide — these are the design constraints we work to.',
  pillars: [
    { id: 'connect',     n: '01', title: 'Connect',     desc: 'Bring people together, especially the ones who never cross paths at work.' },
    { id: 'communicate', n: '02', title: 'Communicate', desc: 'Put teams in situations where communicating well is the only way through.' },
    { id: 'collaborate', n: '03', title: 'Collaborate', desc: 'Challenges that cannot be solved alone, by design.' },
    { id: 'improve',     n: '04', title: 'Improve',     desc: 'Show teams where their process costs them time — and what to do about it.' },
    { id: 'sustain',     n: '05', title: 'Sustain',     desc: 'Run events responsibly, and build sustainable thinking into the experience.' },
  ],
}
const signature = {
  eyebrow: 'Signature experiences',
  title: 'Our Signature Experiences',
  lead: 'Three formats we have built end to end. Each one adapts to your group size, location and goal.',
}
const why = {
  eyebrow: 'Why Eventuur',
  title: 'Not Just Something To Do. Something To Take Away.',
  lead:
    'Your team does not just spend a day together. They learn how to work better together — because the day was designed for it.',
  outcomes: [
    { n: '01', title: 'Communication',  desc: 'Activities that only work when people say what they mean, quickly and clearly.' },
    { n: '02', title: 'Team Connection', desc: 'Deliberate mixing of departments, seniority levels and offices.' },
    { n: '03', title: 'Efficiency',      desc: 'Challenges where a better process visibly beats more effort.' },
    { n: '04', title: 'Problem Solving', desc: 'Open-ended problems with more than one right answer.' },
    { n: '05', title: 'Sustainability',  desc: 'Responsible operations, and sustainable thinking inside the activity itself.' },
    { n: '06', title: 'Fun',             desc: 'The part that makes all of the above possible. Non-negotiable.' },
  ],
  /* ⚠️ PLACEHOLDER STATS — replace with real figures before launch, or remove. */
  stats: [
    { value: 3,   suffix: '',  label: 'Signature formats, built end to end' },
    { value: 200, suffix: '+', label: 'Participants a single event can handle' },
    { value: 100, suffix: '%', label: 'Of experiences tailored to a stated goal' },
    { value: 5,   suffix: '',  label: 'Design principles behind every event' },
  ],
  statsNote: 'Placeholder figures — replace in src/data/content.js before launch.',
}
const custom = {
  eyebrow: 'Custom events',
  title: 'Your Team Is Different. Your Event Should Be Too.',
  lead:
    'Every format we run gets rebuilt around the group in front of us. Tell us the constraints and the goal — we handle the rest.',
  factors: [
    'Company size',
    'Team goals',
    'Location',
    'Budget',
    'Desired atmosphere',
    'Sustainability goals',
    'Time available',
  ],
  questions: [
    { q: 'Need better communication?', a: 'We build the day around challenges that fail without it.' },
    { q: 'Want employees to connect?', a: 'We mix teams deliberately so people meet colleagues they never work with.' },
    { q: 'Want to improve teamwork?',  a: 'We design problems no individual can solve alone.' },
    { q: 'Want to encourage sustainable thinking?', a: 'We write it into both the logistics and the content.' },
  ],
}
const newsTeaser = {
  eyebrow: 'News & stories',
  title: 'What we have been working on.',
  lead: 'New formats, behind-the-scenes notes, sustainability updates and practical team-building thinking.',
}
const finalCta = {
  title: 'Ready to Bring Your Team Together?',
  lead:
    "Tell us what you have in mind. We'll help turn it into an experience your team won't forget.",
}

/* --------------------------- GET TO KNOW US ------------------------------ */
const about = {
  eyebrow: 'Get to know us',
  title: 'We build days that are still useful on Monday.',
  lead:
    'Eventuur exists because there is a gap between "we had a great day out" and "something actually changed".',
  story: [
    'We started Eventuur after watching the same thing happen over and over: a company spends real money on a team day, everyone enjoys it, and then nothing carries over into the work.',
    'The events themselves were rarely bad. They just were not designed to do anything beyond fill an afternoon. Nobody asked what should be different afterwards, so nothing was.',
    'So we do that first. Before we propose a format, we ask what you actually want to change — and then we build the experience around that answer. The fun is not decoration; it is the thing that makes people open up enough for the rest to work.',
  ],
  mission: {
    title: 'Our mission',
    text: 'To turn company events from a line in the budget into something teams genuinely benefit from — without making them feel like training.',
  },
  vision: {
    title: 'Our vision',
    text: 'A world where "team building" stops being a word people roll their eyes at, because the events behind it are actually worth their time.',
  },
  values: [
    { title: 'Purpose first',    desc: 'If we cannot explain what an activity is for, we do not run it.' },
    { title: 'Genuinely fun',    desc: 'Nothing works if people are not enjoying themselves. That is the mechanism, not the garnish.' },
    { title: 'Everyone plays',   desc: 'Formats are inclusive by default — fitness, language and personality should never sideline anyone.' },
    { title: 'Honest about impact', desc: 'We do what we can operationally on sustainability and we do not overstate it.' },
    { title: 'Adapt, always',    desc: 'No two companies get the same day, because no two companies have the same problem.' },
  ],
}

/* ---------------------------- INFORMATION -------------------------------- */
const information = {
  eyebrow: 'For decision makers',
  title: 'Why should a company organise an event?',
  lead:
    'A straight answer for the person who has to justify the budget line.',
  intro: [
    'The honest version: a single event will not restructure your organisation. What it does do — reliably, and cheaply relative to most alternatives — is remove friction between people who have to work together.',
    'Below is what that actually buys you.',
  ],
  benefits: [
    { title: 'Better communication', desc: 'Colleagues who have solved something together under time pressure talk to each other differently afterwards. The barrier to asking a question drops.' },
    { title: 'Stronger relationships', desc: 'Relationships built outside the org chart are the ones that get things unstuck later. Events create them faster than months of meetings.' },
    { title: 'Team cohesion', desc: 'Shared experience creates shared reference points. Teams with a common story coordinate with less overhead.' },
    { title: 'Employee engagement', desc: 'A well-designed day signals that the company invests in its people. Poorly designed ones signal the opposite — which is why design matters.' },
    { title: 'Problem solving', desc: 'Open-ended challenges surface how a team actually approaches problems, in a setting where being wrong costs nothing.' },
    { title: 'Collaboration across silos', desc: 'Deliberate team mixing puts people in contact with departments they only know by email address.' },
    { title: 'Creativity', desc: 'Constrained, playful problems get people out of their default professional patterns.' },
    { title: 'Company culture', desc: 'Culture is what people experience, not what is written on a wall. Events are one of the few levers you can pull directly.' },
    { title: 'Sustainable awareness', desc: 'Where it fits your goals, sustainable thinking can be built into the activity rather than mentioned in an opening speech.' },
  ],
  process: {
    title: 'How working with us goes',
    steps: [
      { n: '01', title: 'Tell us the goal', desc: 'A short call or a filled-in form. We want the objective, the group, the constraints.' },
      { n: '02', title: 'We propose a concept', desc: 'A concrete concept with format, flow, timing and cost. Before any commitment.' },
      { n: '03', title: 'We refine it with you', desc: 'You adjust; we adapt. Nothing is locked until you are happy.' },
      { n: '04', title: 'We run the day', desc: 'Full on-site delivery. Your only job is to show up.' },
      { n: '05', title: 'We close the loop', desc: 'A debrief that connects what happened to how the team works.' },
    ],
  },
  faqs: [
    { q: 'How far in advance should we book?', a: 'Four to six weeks is comfortable for most formats. We can work faster — ask us.' },
    { q: 'What group sizes do you handle?', a: 'From around 8 people up to several hundred, depending on the format.' },
    { q: 'Do you handle venue and catering?', a: 'Yes. We can work at your location or arrange a venue and catering partners.' },
    { q: 'What if people are not physically active?', a: 'Every format has a low-intensity variant. No activity depends on fitness to be enjoyable or winnable.' },
    { q: 'What does it cost?', a: 'It depends on format, group size, location and duration. Tell us your budget range and we will design to it honestly.' },
  ],
}

/* ------------------------------- CONTACT --------------------------------- */
const contact = {
  eyebrow: 'Contact',
  title: "Tell us what you have in mind.",
  lead:
    'The more you tell us, the more concrete our first response can be. Everything except name, email and message is optional.',
  responseTime: 'We reply to every enquiry within two working days.',
}


/* ===== src/data/events.js ===== */
/* ===========================================================================
   EVENTS / EXPERIENCES
   ---------------------------------------------------------------------------
   ➕ TO ADD A NEW EVENT: copy any object below, change the fields, done.
      The Events page, filters, detail page, homepage and sitemap all read
      from this file. No component changes required.

   REQUIRED FIELDS: slug, name, tagline, summary, categories, cover
   Everything else is optional and degrades gracefully.
   ========================================================================= */


/** Filter categories shown on /events. `id` must match values in event.categories */
const eventCategories = [
  { id: 'all',             label: 'All Experiences' },
  { id: 'team-building',   label: 'Team Building' },
  { id: 'outdoor',         label: 'Outdoor' },
  { id: 'indoor',          label: 'Indoor' },
  { id: 'communication',   label: 'Communication' },
  { id: 'problem-solving', label: 'Problem Solving' },
  { id: 'networking',      label: 'Networking' },
  { id: 'custom',          label: 'Custom' },
]
const events = [
  /* ======================= SIGNATURE 01 — BOOTCAMP ======================= */
  {
    slug: 'bootcamp',
    name: 'Bootcamp',
    tagline: 'Energy, teamwork and communication under pressure.',
    signature: true,
    order: 1,
    categories: ['team-building', 'outdoor', 'communication', 'problem-solving'],
    summary:
      'A team-based bootcamp built from mini-challenges rather than pure exercise. Teams have to talk, plan and adapt — the physical side is only the vehicle.',
    cover: images.bootcampCover,
    hero: images.bootcampHero,
    gallery: [images.bootcampAlt1, images.bootcampAlt2, images.bootcampCover],
    highlights: ['Teamwork', 'Communication', 'Leadership', 'Trust'],
    description: [
      'Bootcamp is a series of short, team-based challenges that put people in situations where communication is the only way forward. Some stations are physical, some are tactical, and most require a group to decide quickly who does what.',
      'It is deliberately built so that fitness is never the deciding factor. The strongest team is almost always the one that organises itself best — which is exactly the point we want participants to take back to the office.',
    ],
    objectives: [
      'Force clear, fast communication under mild pressure',
      'Reveal natural leadership and role-taking within a team',
      'Build trust through shared effort and small wins',
      'Break down hierarchy between departments and seniority levels',
    ],
    participantsDo: [
      'Rotate through a circuit of timed team stations',
      'Solve tactical challenges that require a plan before action',
      'Swap roles so everyone leads at least once',
      'Take part in a closing team challenge that combines the day',
    ],
    companyGains: [
      'A visible read on how your teams actually coordinate',
      'Cross-department relationships that survive the day',
      'A shared reference point people keep talking about afterwards',
      'A debrief linking what happened to how the team works',
    ],
    specs: {
      groupSize: '10 – 120 participants',
      duration: '2 – 4 hours',
      setting: 'Outdoor (indoor variant available)',
      intensity: 'Moderate — adaptable to all fitness levels',
      season: 'Year-round',
    },
    sustainability:
      'Run at local venues to reduce travel, with reusable equipment, no single-use giveaways and catering sourced locally where the venue allows.',
    seo: {
      title: 'Corporate Bootcamp — Team Challenges | Eventuur',
      description:
        'A team-based corporate bootcamp built around communication, leadership and trust. Adaptable to all fitness levels. 10–120 participants.',
    },
  },

  /* ====================== SIGNATURE 02 — MINI GAMES ====================== */
  {
    slug: 'mini-games',
    name: 'Mini Games',
    tagline: 'Short, sharp challenges that reward thinking together.',
    signature: true,
    order: 2,
    categories: ['team-building', 'indoor', 'outdoor', 'problem-solving', 'communication'],
    summary:
      'A modular collection of quick interactive challenges. Pick a handful for a one-hour session, or combine them into a full competitive event.',
    cover: images.miniGamesCover,
    hero: images.miniGamesHero,
    gallery: [images.miniGamesAlt1, images.miniGamesAlt2, images.miniGamesCover],
    highlights: ['Creativity', 'Cooperation', 'Efficiency', 'Problem Solving'],
    description: [
      'Mini Games is our most flexible format. Each game is a self-contained challenge lasting five to fifteen minutes, designed around a specific skill: describing something precisely, dividing work, negotiating, prototyping or deciding fast with incomplete information.',
      'Because the format is modular, we can weight the programme towards whatever you want to get out of the day — and we keep adding new games to the catalogue.',
    ],
    objectives: [
      'Practise precise communication in low-stakes settings',
      'Show teams how process, not effort, drives efficiency',
      'Mix people who rarely work together',
      'Keep energy high with fast wins and quick rotations',
    ],
    participantsDo: [
      'Rotate in small teams through a series of game stations',
      'Compete on a live scoreboard across the session',
      'Face a mix of creative, logical and physical challenges',
      'Finish with a final round where all teams play at once',
    ],
    companyGains: [
      'A format that fits any room, venue or time slot',
      'Inclusive by design — no fitness or specialist skill needed',
      'Easy to repeat with a different game selection next time',
      'Strong energy boost for kick-offs and company days',
    ],
    specs: {
      groupSize: '8 – 200 participants',
      duration: '1 – 4 hours',
      setting: 'Indoor or outdoor',
      intensity: 'Low to moderate',
      season: 'Year-round',
    },
    sustainability:
      'Built from reusable, repairable materials. Digital scoring instead of printed sheets, and games chosen to work in venues you already use.',
    seo: {
      title: 'Corporate Mini Games — Interactive Team Challenges | Eventuur',
      description:
        'A modular set of short interactive team challenges around creativity, cooperation and problem solving. Indoor or outdoor, 8–200 participants.',
    },
  },

  /* ==================== SIGNATURE 03 — SCAVENGER HUNT ==================== */
  {
    slug: 'scavenger-hunt',
    name: 'Scavenger Hunt',
    tagline: 'Strategy, discovery and a city that becomes the playing field.',
    signature: true,
    order: 3,
    categories: ['team-building', 'outdoor', 'communication', 'problem-solving'],
    summary:
      'Teams work through clues, tasks and checkpoints across a real location, deciding together how to spend limited time to reach a shared objective.',
    cover: images.scavengerCover,
    hero: images.scavengerHero,
    gallery: [images.scavengerAlt1, images.scavengerAlt2, images.scavengerCover],
    highlights: ['Strategy', 'Discovery', 'Collaboration', 'Creativity'],
    description: [
      'The Scavenger Hunt turns a city, park or venue into a board. Teams receive an objective and a limited amount of time, then decide their own route, priorities and division of labour.',
      'Challenges range from observation puzzles and photo tasks to short interactions with the environment. The scoring rewards planning: teams that sprint without a strategy consistently lose to teams that spend the first five minutes talking.',
    ],
    objectives: [
      'Turn strategy and prioritisation into a felt experience',
      'Give quieter team members an obvious way to contribute',
      'Encourage people to explore an area together',
      'Create a shared story the team retells for months',
    ],
    participantsDo: [
      'Plan a route and split responsibilities as a team',
      'Solve clue-based and observational challenges',
      'Complete creative tasks at checkpoints',
      'Converge at a final location for the closing challenge',
    ],
    companyGains: [
      'Works equally well for 12 people or 150',
      'A natural, unforced way to mix departments',
      'Photo and video material you can actually use internally',
      'Scales to any city or venue you choose',
    ],
    specs: {
      groupSize: '12 – 150 participants',
      duration: '2 – 3.5 hours',
      setting: 'Outdoor, urban or nature',
      intensity: 'Low — walking pace',
      season: 'Year-round (route adapted to weather)',
    },
    sustainability:
      'Fully on-foot or by public transport, paperless clues via participants\u2019 own phones, and routes designed to highlight local and independent places.',
    seo: {
      title: 'Corporate Scavenger Hunt — Team Experience | Eventuur',
      description:
        'A strategic team scavenger hunt around communication, discovery and collaboration. Urban or nature routes, 12–150 participants.',
    },
  },

  /* ========================= OTHER EXPERIENCES =========================== */
  {
    slug: 'networking-event',
    name: 'Networking Event',
    tagline: 'Conversations that actually start.',
    signature: false,
    order: 10,
    categories: ['networking', 'indoor', 'communication'],
    summary:
      'A structured networking format that removes the awkward first ten minutes and gets people into real conversations quickly.',
    cover: images.networkingCover,
    hero: images.networkingCover,
    gallery: [images.networkingCover, images.drinksCover],
    highlights: ['Connection', 'Communication', 'New Relationships'],
    description: [
      'Standard networking events leave people talking to the colleagues they already know. We use light structure — rotations, prompts and small shared tasks — so that new conversations start without anyone feeling forced.',
    ],
    objectives: [
      'Connect people across teams, offices or organisations',
      'Lower the barrier for quieter participants',
      'Give conversations a starting point beyond small talk',
    ],
    participantsDo: [
      'Take part in short guided rotations',
      'Use conversation prompts tied to your event theme',
      'Move into free-form networking once the energy is up',
    ],
    companyGains: [
      'Measurably more new connections than an open-bar format',
      'Works as a standalone evening or after a main activity',
    ],
    specs: {
      groupSize: '20 – 250 participants',
      duration: '2 – 3 hours',
      setting: 'Indoor',
      intensity: 'Low',
      season: 'Year-round',
    },
    sustainability: 'Local catering, no printed name-card waste, reusable signage.',
  },
  {
    slug: 'company-celebration',
    name: 'Company Celebration',
    tagline: 'Milestones that feel like they mattered.',
    signature: false,
    order: 11,
    categories: ['networking', 'indoor', 'outdoor', 'custom'],
    summary:
      'Anniversaries, launches, record quarters and end-of-year events — produced end to end, with an activity woven in if you want one.',
    cover: images.celebrationCover,
    hero: images.celebrationCover,
    gallery: [images.celebrationCover, images.drinksCover, images.dinnerCover],
    highlights: ['Culture', 'Recognition', 'Energy'],
    description: [
      'We handle venue, flow, catering partners and programme. Where it fits, we build in a short Eventuur activity so the evening has a moment people remember beyond the speeches.',
    ],
    objectives: [
      'Mark a milestone in a way that feels genuine',
      'Reinforce company culture and recognition',
    ],
    participantsDo: ['Enjoy a produced evening with optional built-in activities'],
    companyGains: ['A single point of contact for the whole event', 'Optional activity module'],
    specs: {
      groupSize: '25 – 500 participants',
      duration: 'Half day to full evening',
      setting: 'Indoor or outdoor',
      intensity: 'Low',
      season: 'Year-round',
    },
    sustainability: 'Reusable decor, surplus-food donation where possible, local suppliers.',
  },
  {
    slug: 'team-dinner',
    name: 'Team Dinner & Drinks',
    tagline: 'The informal half of team building.',
    signature: false,
    order: 12,
    categories: ['networking', 'indoor'],
    summary:
      'Curated dinners and drinks, arranged around your group instead of a standard restaurant booking — often paired with an afternoon activity.',
    cover: images.dinnerCover,
    hero: images.dinnerCover,
    gallery: [images.dinnerCover, images.drinksCover],
    highlights: ['Connection', 'Relaxed', 'Easy to organise'],
    description: [
      'Sometimes a team just needs to sit down together. We arrange the venue, seating logic and timing, and can add light table-level games so people talk to more than their two neighbours.',
    ],
    objectives: ['Give teams unstructured time together', 'Round off an activity day'],
    participantsDo: ['Eat, talk, and optionally play short table challenges'],
    companyGains: ['Zero organisational overhead for you'],
    specs: {
      groupSize: '10 – 120 participants',
      duration: '2 – 4 hours',
      setting: 'Indoor',
      intensity: 'Low',
      season: 'Year-round',
    },
    sustainability: 'Seasonal menus, plant-forward options by default, local venues.',
  },
  {
    slug: 'custom-experience',
    name: 'Custom Experience',
    tagline: 'Built from your goal backwards.',
    signature: false,
    order: 13,
    categories: ['custom', 'team-building', 'communication', 'problem-solving', 'indoor', 'outdoor'],
    summary:
      'Tell us the outcome you want — better communication, a merged team that needs to gel, a sustainability push — and we design the experience around it.',
    cover: images.customCover,
    hero: images.customCover,
    gallery: [images.customCover, images.introCollage, images.celebrationCover],
    highlights: ['Tailored', 'Goal-driven', 'Any size'],
    description: [
      'Most of our work starts with a problem rather than a product. Two departments that never speak. A team that has gone fully remote. A new strategy nobody has internalised yet.',
      'We take that starting point, design an experience around it, and build in a debrief so the day connects back to the work.',
    ],
    objectives: ['Whatever you define — we design against your objective'],
    participantsDo: ['Depends entirely on the design'],
    companyGains: [
      'An experience that fits your size, budget, location and time',
      'A concept document before you commit to anything',
    ],
    specs: {
      groupSize: 'Any',
      duration: 'Any',
      setting: 'Indoor, outdoor or hybrid',
      intensity: 'Your choice',
      season: 'Year-round',
    },
    sustainability: 'Sustainability targets can be written into the brief from the start.',
  },
]

/* ------------------------------ SELECTORS -------------------------------- */
const getEventBySlug = (slug) => events.find((e) => e.slug === slug)
const signatureEvents = () =>
  events.filter((e) => e.signature).sort((a, b) => a.order - b.order)
const sortedEvents = () => [...events].sort((a, b) => a.order - b.order)
const filterEvents = (categoryId) =>
  categoryId === 'all'
    ? sortedEvents()
    : sortedEvents().filter((e) => e.categories.includes(categoryId))
const categoryLabel = (id) =>
  eventCategories.find((c) => c.id === id)?.label ?? id


/* ===== src/data/news.js ===== */
/* ===========================================================================
   NEWS / STORIES
   ➕ TO ADD AN ARTICLE: copy an object, put it at the TOP of the array.
      `body` is an array of blocks: { type: 'p' | 'h3' | 'ul', ... }
   ⚠️  All articles below are PLACEHOLDER copy written for launch.
   ========================================================================= */
const newsCategories = ['All', 'Company', 'Experiences', 'Sustainability', 'Insights']
const articles = [
  {
    slug: 'why-we-started-eventuur',
    title: 'Why we started Eventuur',
    category: 'Company',
    date: '2026-08-12',
    readingMinutes: 4,
    image: images.newsCommunication,
    excerpt:
      'Most company events are a nice day out and nothing more. We wanted to find out what happens when you design one around an actual objective.',
    body: [
      { type: 'p', text: 'We kept noticing the same pattern. A company books an activity, everyone has a decent afternoon, and on Monday nothing has changed. The budget was spent on entertainment, and entertainment is all it delivered.' },
      { type: 'p', text: 'That is not a criticism of fun. Fun is the mechanism — it is what makes people drop their professional armour long enough to actually talk to each other. The problem is stopping there.' },
      { type: 'h3', text: 'Designing backwards' },
      { type: 'p', text: 'Every Eventuur experience starts with a question: what should be different afterwards? Better communication between two teams. A new hire cohort that knows more than three people. A department that has never had to make a decision together under time pressure.' },
      { type: 'p', text: 'Once that answer exists, the activity almost designs itself.' },
    ],
  },
  {
    slug: 'bootcamp-format-launch',
    title: 'Our Bootcamp format is live',
    category: 'Experiences',
    date: '2026-07-28',
    readingMinutes: 3,
    image: images.newsBootcampLaunch,
    excerpt:
      'A team bootcamp where fitness is never the deciding factor. Here is how the circuit is built and why coordination beats effort every time.',
    body: [
      { type: 'p', text: 'Bootcamp is now bookable. It is a circuit of timed team stations mixing tactical and physical challenges, deliberately weighted so that the fittest group rarely wins.' },
      { type: 'h3', text: 'What makes it work' },
      { type: 'ul', items: [
        'Every station needs a plan before anyone can start',
        'Roles rotate, so everyone leads at least once',
        'Scoring rewards coordination over raw speed',
        'A closing debrief ties the day back to how the team works',
      ] },
      { type: 'p', text: 'Suitable for 10 to 120 participants, indoor variant available for winter bookings.' },
    ],
  },
  {
    slug: 'sustainability-commitments',
    title: 'The sustainability rules we hold ourselves to',
    category: 'Sustainability',
    date: '2026-07-05',
    readingMinutes: 5,
    image: images.newsSustainability,
    excerpt:
      'No greenwashing, no tree-planting badge. Just a short list of operational rules we apply to every event we run.',
    body: [
      { type: 'p', text: 'Sustainability is part of how Eventuur operates, but we are not going to pretend a corporate scavenger hunt saves the planet. What we can control is how the event itself is run.' },
      { type: 'h3', text: 'Our operating rules' },
      { type: 'ul', items: [
        'No single-use giveaways, ever',
        'Equipment is reusable and repaired rather than replaced',
        'Routes designed for foot or public transport first',
        'Digital scoring and clues instead of printed material',
        'Local and seasonal catering partners by default',
        'Surplus food redirected where the venue permits',
      ] },
      { type: 'p', text: 'If sustainability is an explicit goal for your company, we can also build it into the content of the activity itself — not just the logistics.' },
    ],
  },
  {
    slug: 'five-signs-team-communication',
    title: 'Five signs your team has a communication problem',
    category: 'Insights',
    date: '2026-06-18',
    readingMinutes: 6,
    image: images.newsHybrid,
    excerpt:
      'It rarely shows up as an argument. Usually it looks like duplicated work, long meetings and decisions that quietly get remade.',
    body: [
      { type: 'p', text: 'Communication problems almost never announce themselves. They show up as symptoms that get blamed on workload or tooling.' },
      { type: 'ul', items: [
        'The same decision gets made more than once',
        'Two people discover they have built the same thing',
        'Meetings run long but end without an owner for anything',
        'Information moves through one person who becomes a bottleneck',
        'New joiners take months to know who to ask',
      ] },
      { type: 'p', text: 'A single event will not fix a structural issue. What it can do is make the pattern visible to the team in a setting where nobody is defensive about it — which is usually the hard part.' },
    ],
  },
  {
    slug: 'scavenger-hunt-city-routes',
    title: 'Behind the scenes: building a scavenger hunt route',
    category: 'Experiences',
    date: '2026-05-30',
    readingMinutes: 4,
    image: images.newsScavenger,
    excerpt:
      'Every route we build gets walked at least three times before a client ever sees it. Here is what we are looking for.',
    body: [
      { type: 'p', text: 'A good route is not a list of landmarks. It is a set of decisions the team has to make with incomplete information and a clock running.' },
      { type: 'h3', text: 'What we test for' },
      { type: 'ul', items: [
        'At least two viable routes, so strategy matters',
        'Checkpoints that work in rain as well as sun',
        'Tasks solvable by any team member, not just the extroverts',
        'A natural convergence point for the finale',
      ] },
      { type: 'p', text: 'We walk each route three times, including once at the actual event time of day.' },
    ],
  },
]

/* ------------------------------ SELECTORS -------------------------------- */
const sortedArticles = () =>
  [...articles].sort((a, b) => new Date(b.date) - new Date(a.date))
const getArticleBySlug = (slug) => articles.find((a) => a.slug === slug)
const latestArticles = (n = 3) => sortedArticles().slice(0, n)
const filterArticles = (category) =>
  category === 'All' ? sortedArticles() : sortedArticles().filter((a) => a.category === category)
const formatDate = (iso, locale = 'en-GB') =>
  new Date(iso).toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' })


/* ===== src/data/forms.js ===== */
/* ===========================================================================
   FORM SCHEMA — the contact/enquiry form is generated from this array.
   ➕ Add, remove or reorder a field here and the UI + validation follow.
   ========================================================================= */
const enquiryFields = [
  { name: 'name',         label: 'Your name',            type: 'text',     required: true,  autoComplete: 'name',         half: true,  placeholder: 'Jane Doe' },
  { name: 'company',      label: 'Company',              type: 'text',     required: false, autoComplete: 'organization', half: true,  placeholder: 'Company name' },
  { name: 'email',        label: 'Work email',           type: 'email',    required: true,  autoComplete: 'email',        half: true,  placeholder: 'jane@company.com' },
  { name: 'phone',        label: 'Phone',                type: 'tel',      required: false, autoComplete: 'tel',          half: true,  placeholder: 'Optional' },
  { name: 'participants', label: 'Number of participants', type: 'select', required: false, half: true,
    options: ['Not sure yet', '8 – 20', '20 – 50', '50 – 100', '100 – 200', '200+'] },
  { name: 'date',         label: 'Preferred date',       type: 'date',     required: false, half: true, hint: 'Approximate is fine.' },
  { name: 'eventType',    label: 'Type of event',        type: 'select',   required: false, half: true,
    options: ['Not sure yet', ...events.map((e) => e.name), 'Something else entirely'] },
  { name: 'budget',       label: 'Budget range',         type: 'select',   required: false, half: true,
    options: ['Prefer not to say', 'Under €2,500', '€2,500 – €5,000', '€5,000 – €10,000', '€10,000 – €25,000', '€25,000+'] },
  { name: 'message',      label: 'What do you have in mind?', type: 'textarea', required: true, half: false,
    placeholder: 'Tell us about your team, what you want the day to achieve, and anything else that matters.' },
]

/** Pure validation — returns { fieldName: 'error message' }. */
function validateEnquiry(values) {
  const errors = {}
  for (const field of enquiryFields) {
    const value = (values[field.name] ?? '').toString().trim()
    if (field.required && !value) errors[field.name] = 'This field is required.'
  }
  if (values.email && !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(values.email.trim())) {
    errors.email = 'Enter a valid email address.'
  }
  if (values.message && values.message.trim().length < 10) {
    errors.message = 'A little more detail helps us reply properly.'
  }
  if (!values.consent) {
    errors.consent = 'Please confirm we may contact you about this enquiry.'
  }
  return errors
}
const emptyEnquiry = () =>
  Object.fromEntries(enquiryFields.map((f) => [f.name, ''])) 


/* ===== src/lib/auth.js ===== */
/* ===========================================================================
   AUTHENTICATION — MODULAR PROVIDER INTERFACE
   ---------------------------------------------------------------------------
   ⚠️  THERE IS NO WORKING AUTHENTICATION IN THIS BUILD, BY DESIGN.
       A fake login that "works" is worse than none. The UI is finished; the
       provider is a stub that reports NOT_CONFIGURED.

   TO ENABLE LOGIN LATER
   ---------------------
   1. Implement the AuthProvider interface below (Cloudflare Access, Auth0,
      Clerk, Supabase, or your own Worker + JWT).
   2. Swap the export at the bottom of this file.
   3. No component changes are needed — LoginPage and useAuth already consume
      this interface.
   ========================================================================= */
class AuthNotConfiguredError extends Error {
  constructor(message = 'Authentication is not connected yet.') {
    super(message)
    this.name = 'AuthNotConfiguredError'
    this.code = 'NOT_CONFIGURED'
  }
}

/**
 * @typedef {Object} AuthProvider
 * @property {string}  id
 * @property {boolean} isConfigured
 * @property {() => Promise<object|null>} getSession
 * @property {(credentials: {email:string,password:string}) => Promise<object>} signIn
 * @property {() => Promise<void>} signOut
 */

/** Stub provider — reports honestly that nothing is wired up. */
const stubProvider = {
  id: 'stub',
  isConfigured: false,
  async getSession() {
    return null
  },
  async signIn() {
    throw new AuthNotConfiguredError(
      'Client accounts are not live yet. Contact us and we will set you up manually in the meantime.'
    )
  },
  async signOut() {},
}

/* ---------------------------------------------------------------------------
   EXAMPLE of a real implementation (left commented on purpose):
const workerProvider = {
     id: 'cf-worker',
     isConfigured: true,
     async getSession() {
       const r = await fetch('/api/auth/session', { credentials: 'include' })
       return r.ok ? r.json() : null
     },
     async signIn({ email, password }) {
       const r = await fetch('/api/auth/login', {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         credentials: 'include',
         body: JSON.stringify({ email, password }),
       })
       if (!r.ok) throw new Error('Invalid email or password.')
       return r.json()
     },
     async signOut() {
       await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' })
     },
   }
   ------------------------------------------------------------------------- */

/** 👉 Swap this line when a real provider exists. */
const auth = stubProvider


/* ===== src/hooks/useReducedMotion.js ===== */

/** Tracks the user's OS-level reduced-motion preference, live. */
function useReducedMotion() {
  const [reduced, setReduced] = useState(
    () => typeof window !== 'undefined'
      && window.matchMedia('(prefers-reduced-motion: reduce)').matches
  )
  useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
    const onChange = (e) => setReduced(e.matches)
    mq.addEventListener('change', onChange)
    return () => mq.removeEventListener('change', onChange)
  }, [])
  return reduced
}


/* ===== src/hooks/useCountUp.js ===== */

/** Animates 0 → target once `active` becomes true. Snaps instantly if reduced motion. */
function useCountUp(target, active, duration = 1600) {
  const reduced = useReducedMotion()
  const [value, setValue] = useState(0)

  useEffect(() => {
    if (!active) return
    if (reduced) { setValue(target); return }

    let raf
    const start = performance.now()
    const easeOut = (t) => 1 - Math.pow(1 - t, 3)

    const tick = (now) => {
      const t = Math.min((now - start) / duration, 1)
      setValue(Math.round(easeOut(t) * target))
      if (t < 1) raf = requestAnimationFrame(tick)
    }
    raf = requestAnimationFrame(tick)
    return () => cancelAnimationFrame(raf)
  }, [target, active, duration, reduced])

  return value
}


/* ===== src/hooks/useInView.js ===== */

/**
 * IntersectionObserver hook powering all scroll reveals.
 * One observer per element, disconnected after first entry (once: true).
 */
function useInView({ threshold = 0.15, rootMargin = '0px 0px -8% 0px', once = true } = {}) {
  const ref = useRef(null)
  const [inView, setInView] = useState(false)

  useEffect(() => {
    const el = ref.current
    if (!el) return

    // No IO support (or SSR/crawler): show content immediately.
    if (typeof IntersectionObserver === 'undefined') {
      setInView(true)
      return
    }

    const io = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setInView(true)
          if (once) io.disconnect()
        } else if (!once) {
          setInView(false)
        }
      },
      { threshold, rootMargin }
    )
    io.observe(el)
    return () => io.disconnect()
  }, [threshold, rootMargin, once])

  return [ref, inView]
}


/* ===== src/hooks/useParallax.js ===== */

/**
 * Subtle transform-only parallax. Disabled for reduced motion and on
 * coarse-pointer devices (mobile) where it costs more than it gives.
 */
function useParallax(strength = 0.18) {
  const ref = useRef(null)
  const reduced = useReducedMotion()

  useEffect(() => {
    const el = ref.current
    if (!el || reduced) return
    if (window.matchMedia('(pointer: coarse)').matches) return

    let ticking = false
    const update = () => {
      const y = window.scrollY
      const rect = el.parentElement?.getBoundingClientRect()
      if (rect && rect.bottom > 0 && rect.top < window.innerHeight) {
        el.style.transform = `translate3d(0, ${y * strength}px, 0)`
      }
      ticking = false
    }
    const onScroll = () => {
      if (!ticking) { ticking = true; requestAnimationFrame(update) }
    }
    update()
    window.addEventListener('scroll', onScroll, { passive: true })
    return () => {
      window.removeEventListener('scroll', onScroll)
      if (el) el.style.transform = ''
    }
  }, [strength, reduced])

  return ref
}


/* ===== src/hooks/useScrolled.js ===== */

/** True once the page has scrolled past `offset` px. Passive + rAF throttled. */
function useScrolled(offset = 24) {
  const [scrolled, setScrolled] = useState(false)

  useEffect(() => {
    let ticking = false
    const update = () => {
      setScrolled(window.scrollY > offset)
      ticking = false
    }
    const onScroll = () => {
      if (!ticking) {
        ticking = true
        requestAnimationFrame(update)
      }
    }
    update()
    window.addEventListener('scroll', onScroll, { passive: true })
    return () => window.removeEventListener('scroll', onScroll)
  }, [offset])

  return scrolled
}


/* ===== src/hooks/useAuth.js ===== */

/**
 * Thin React binding over the auth provider in lib/auth.js.
 * Works unchanged once a real provider is swapped in.
 */
function useAuth() {
  const [session, setSession] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    let alive = true
    auth
      .getSession()
      .then((s) => alive && setSession(s))
      .catch(() => {})
      .finally(() => alive && setLoading(false))
    return () => { alive = false }
  }, [])

  const signIn = useCallback(async (credentials) => {
    setError(null)
    setLoading(true)
    try {
      const s = await auth.signIn(credentials)
      setSession(s)
      return s
    } catch (e) {
      setError(e)
      throw e
    } finally {
      setLoading(false)
    }
  }, [])

  const signOut = useCallback(async () => {
    await auth.signOut()
    setSession(null)
  }, [])

  return { session, loading, error, signIn, signOut, isConfigured: auth.isConfigured }
}


/* ===== src/lib/api.js ===== */
/* ===========================================================================
   API CLIENT
   ---------------------------------------------------------------------------
   The UI never contains submission logic. It calls submitEnquiry() and reacts
   to the returned status. The endpoint itself is a Cloudflare Pages Function
   (functions/api/contact.js) which is INTENTIONALLY not wired to an email
   provider yet — it returns 501 until you configure one.

   This means the form is honest: it will tell the user it is not live rather
   than pretending to have sent something.
   ========================================================================= */
const ENQUIRY_ENDPOINT = '/api/contact'
const Status = {
  IDLE: 'idle',
  SUBMITTING: 'submitting',
  SUCCESS: 'success',
  NOT_CONFIGURED: 'not_configured',
  ERROR: 'error',
}

/**
 * @param {object} payload  Validated enquiry fields.
 * @returns {Promise<{status: string, message: string}>}
 */
async function submitEnquiry(payload) {
  try {
    const res = await fetch(ENQUIRY_ENDPOINT, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ...payload, submittedAt: new Date().toISOString() }),
    })

    // 501 = backend deliberately not configured yet.
    if (res.status === 501) {
      const data = await res.json().catch(() => ({}))
      return {
        status: Status.NOT_CONFIGURED,
        message:
          data.message ||
          'The enquiry form is not connected to an email service yet.',
      }
    }

    if (!res.ok) {
      const data = await res.json().catch(() => ({}))
      return { status: Status.ERROR, message: data.message || `Request failed (${res.status}).` }
    }

    return { status: Status.SUCCESS, message: 'Thanks — your enquiry is on its way.' }
  } catch (err) {
    // Also happens in `vite dev`, where Pages Functions are not running.
    return {
      status: Status.NOT_CONFIGURED,
      message:
        'No enquiry endpoint is reachable from this environment. Run `npm run pages:dev` or deploy to Cloudflare Pages to enable it.',
    }
  }
}


/* ===== src/lib/seo.js ===== */
/* ===========================================================================
   SEO — dependency-free document head management.
   Usage:  useSeo({ title, description, image, type, jsonLd })
   Every page calls this once. Values come from data/, never hard-coded twice.
   ========================================================================= */

function setMeta(attr, key, content) {
  if (!content) return
  let el = document.head.querySelector(`meta[${attr}="${key}"]`)
  if (!el) {
    el = document.createElement('meta')
    el.setAttribute(attr, key)
    document.head.appendChild(el)
  }
  el.setAttribute('content', content)
}

function setLink(rel, href) {
  let el = document.head.querySelector(`link[rel="${rel}"]`)
  if (!el) {
    el = document.createElement('link')
    el.setAttribute('rel', rel)
    document.head.appendChild(el)
  }
  el.setAttribute('href', href)
}

const JSONLD_ID = 'eventuur-jsonld'
function useSeo({
  title,
  description,
  image = seoDefaults.ogImage,
  type = 'website',
  path,
  noindex = false,
  jsonLd = null,
} = {}) {
  useEffect(() => {
    const fullTitle = title
      ? seoDefaults.titleTemplate.replace('%s', title)
      : seoDefaults.defaultTitle

    const url = siteUrl + (path ?? window.location.pathname)
    const absImage = image?.startsWith('http') ? image : siteUrl + image

    document.title = fullTitle
    document.documentElement.lang = seoDefaults.locale

    const desc = description || seoDefaults.description
    setMeta('name', 'description', desc)
    setMeta('name', 'robots', noindex ? 'noindex,nofollow' : 'index,follow')
    setMeta('property', 'og:title', fullTitle)
    setMeta('property', 'og:description', desc)
    setMeta('property', 'og:type', type)
    setMeta('property', 'og:url', url)
    setMeta('property', 'og:image', absImage)
    setMeta('name', 'twitter:card', 'summary_large_image')
    setMeta('name', 'twitter:title', fullTitle)
    setMeta('name', 'twitter:description', desc)
    setMeta('name', 'twitter:image', absImage)
    setLink('canonical', url)

    // Structured data
    document.getElementById(JSONLD_ID)?.remove()
    if (jsonLd) {
      const script = document.createElement('script')
      script.type = 'application/ld+json'
      script.id = JSONLD_ID
      script.textContent = JSON.stringify(jsonLd)
      document.head.appendChild(script)
    }
    return () => document.getElementById(JSONLD_ID)?.remove()
  }, [title, description, image, type, path, noindex, JSON.stringify(jsonLd)])
}

/* ------------------------ Structured-data builders ----------------------- */
const organizationSchema = (company) => ({
  '@context': 'https://schema.org',
  '@type': 'Organization',
  name: company.name,
  url: siteUrl,
  email: company.email,
  telephone: company.phone,
  description: seoDefaults.description,
  address: {
    '@type': 'PostalAddress',
    addressLocality: company.address.city,
    addressCountry: company.address.country,
  },
  sameAs: company.socials.map((s) => s.href),
})
const eventServiceSchema = (event) => ({
  '@context': 'https://schema.org',
  '@type': 'Service',
  name: event.name,
  serviceType: 'Corporate team experience',
  description: event.summary,
  provider: { '@type': 'Organization', name: 'Eventuur', url: siteUrl },
  areaServed: 'NL',
  url: `${siteUrl}/events/${event.slug}`,
})
const articleSchema = (article) => ({
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: article.title,
  datePublished: article.date,
  description: article.excerpt,
  articleSection: article.category,
  author: { '@type': 'Organization', name: 'Eventuur' },
  publisher: { '@type': 'Organization', name: 'Eventuur', url: siteUrl },
  mainEntityOfPage: `${siteUrl}/news/${article.slug}`,
})
const breadcrumbSchema = (items) => ({
  '@context': 'https://schema.org',
  '@type': 'BreadcrumbList',
  itemListElement: items.map((it, i) => ({
    '@type': 'ListItem',
    position: i + 1,
    name: it.label,
    item: siteUrl + it.to,
  })),
})


/* ===== src/components/ui/Button.jsx ===== */

const Arrow = () => (
  <svg className="btn__arrow" width="15" height="10" viewBox="0 0 15 10" fill="none" aria-hidden="true">
    <path d="M1 5h12M9 1l4 4-4 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
  </svg>
)

/**
 * One button for the whole site.
 * Renders <Link> for internal `to`, <a> for external `href`, <button> otherwise.
 * variant: primary | accent | glass | ghost | outline
 */
function Button({
  children,
  to,
  href,
  variant = 'outline',
  size,
  arrow = false,
  block = false,
  className = '',
  ...rest
}) {
  const cls = [
    'btn',
    variant !== 'outline' && `btn--${variant}`,
    size && `btn--${size}`,
    block && 'btn--block',
    className,
  ].filter(Boolean).join(' ')

  const inner = (<>{children}{arrow && <Arrow />}</>)

  if (to) return <Link to={to} className={cls} {...rest}>{inner}</Link>
  if (href) {
    return (
      <a href={href} className={cls} target={href.startsWith('http') ? '_blank' : undefined}
         rel={href.startsWith('http') ? 'noopener noreferrer' : undefined} {...rest}>
        {inner}
      </a>
    )
  }
  return <button type="button" className={cls} {...rest}>{inner}</button>
}


/* ===== src/components/ui/SmartImage.jsx ===== */

/**
 * The only <img> wrapper in the project.
 * - Responsive srcset for remote images (see data/images.js)
 * - Native lazy loading + async decoding
 * - Fade/scale-in on load
 * - Graceful gradient fallback if a source 404s (placeholder URLs may rot)
 */
function SmartImage({
  image,
  sizes = '100vw',
  width = 1440,
  priority = false,
  className = '',
  ratio = '',
  zoom = false,
  scrim = false,
  fill = false,
  style,
  children,
}) {
  const [loaded, setLoaded] = useState(false)
  const [failed, setFailed] = useState(false)
  const resolved = resolveImage(image, { width, sizes })

  return (
    <div
      className={`media ${ratio} ${zoom ? 'media--zoom' : ''} ${fill ? 'media--fill' : ''} ${className}`.trim()}
      style={style}
    >
      {!failed && resolved && (
        <img
          className={`media__img ${loaded ? 'is-loaded' : ''}`}
          src={resolved.src}
          srcSet={resolved.srcSet}
          sizes={resolved.sizes}
          alt={image?.alt || ''}
          loading={priority ? 'eager' : 'lazy'}
          fetchPriority={priority ? 'high' : 'auto'}
          decoding="async"
          draggable="false"
          style={image?.focal ? { objectPosition: image.focal } : undefined}
          onLoad={() => setLoaded(true)}
          onError={() => setFailed(true)}
        />
      )}
      {(failed || !resolved) && (
        <div className="media__fallback" aria-hidden="true">
          <span>{image?.alt ? 'Image placeholder' : 'Eventuur'}</span>
        </div>
      )}
      {scrim && <div className="media__scrim" aria-hidden="true" />}
      {children}
    </div>
  )
}


/* ===== src/components/ui/Reveal.jsx ===== */

/**
 * Scroll-triggered reveal wrapper.
 * <Reveal variant="up" delay={120}>…</Reveal>
 * Variants: up | down | left | right | scale | fade
 */
function Reveal({
  children,
  variant = 'up',
  delay = 0,
  as: Tag = 'div',
  className = '',
  threshold,
  style,
  ...rest
}) {
  const [ref, inView] = useInView({ threshold })
  const cls = `reveal reveal--${variant} ${inView ? 'is-in' : ''} ${className}`.trim()
  const merged = delay ? { transitionDelay: `${delay}ms`, ...style } : style

  return (
    <Tag ref={ref} className={cls} style={merged} {...rest}>
      {children}
    </Tag>
  )
}

/** Staggers direct children by `step` ms without extra markup. */
function RevealGroup({ children, step = 90, variant = 'up', className = '', as: Tag = 'div', ...rest }) {
  const items = Array.isArray(children) ? children : [children]
  return (
    <Tag className={className} {...rest}>
      {items.filter(Boolean).map((child, i) =>
        isValidElement(child) ? (
          <Reveal key={child.key ?? i} variant={variant} delay={i * step}>
            {child}
          </Reveal>
        ) : (
          child
        )
      )}
    </Tag>
  )
}

/** Word-by-word headline reveal. Falls back to plain text with reduced motion. */
function RevealText({ text, as: Tag = 'h2', className = '', delay = 0, step = 55 }) {
  const [ref, inView] = useInView({ threshold: 0.3 })
  return (
    <Tag ref={ref} className={`${inView ? 'is-in' : ''} ${className}`.trim()}>
      {text.split(' ').map((word, i) => (
        <span className="tr" key={`${word}-${i}`}>
          <span className="tr__w" style={{ transitionDelay: `${delay + i * step}ms` }}>
            {word}
          </span>
          {i < text.split(' ').length - 1 && '\u00A0'}
        </span>
      ))}
    </Tag>
  )
}


/* ===== src/components/ui/Section.jsx ===== */

/** Page section with consistent rhythm + optional ambient aura. */
function Section({ id, children, tight = false, aura = false, narrow = false, className = '', style }) {
  return (
    <section id={id} className={`section ${tight ? 'section--tight' : ''} ${className}`.trim()} style={style}>
      {aura && <div className="aura aura--drift" aria-hidden="true" />}
      <div className={`shell ${narrow ? 'shell--narrow' : ''}`} style={{ position: 'relative', zIndex: 1 }}>
        {children}
      </div>
    </section>
  )
}

/** Standard eyebrow + heading + lead block. */
function SectionHead({ eyebrow, title, lead, center = false, as = 'h2', children }) {
  const Heading = as
  return (
    <div className={`section-head ${center ? 'section-head--center' : ''}`}>
      {eyebrow && (
        <Reveal variant="fade">
          <span className={`eyebrow ${center ? 'eyebrow--plain' : ''}`}>{eyebrow}</span>
        </Reveal>
      )}
      {title && (
        <Reveal variant="up" delay={80}>
          <Heading style={{ marginTop: eyebrow ? '1rem' : 0 }}>{title}</Heading>
        </Reveal>
      )}
      {lead && (
        <Reveal variant="up" delay={160}>
          <p className="lead">{lead}</p>
        </Reveal>
      )}
      {children}
    </div>
  )
}


/* ===== src/components/ui/PageHero.jsx ===== */

/** Compact hero used on every inner page. */
function PageHero({ eyebrow, title, lead, image, children }) {
  return (
    <header className="pagehero">
      {image ? (
        <>
          <div style={{ position: 'absolute', inset: 0, zIndex: 0 }}>
            <SmartImage image={image} priority width={1920} sizes="100vw" fill scrim />
          </div>
          <div style={{
            position: 'absolute', inset: 0, zIndex: 1,
            background: 'linear-gradient(to top, var(--c-bg) 2%, rgba(6,8,10,.72) 55%, rgba(6,8,10,.8))',
          }} aria-hidden="true" />
        </>
      ) : (
        <div className="aura aura--drift" aria-hidden="true" />
      )}

      <div className="shell pagehero__inner">
        {eyebrow && (
          <Reveal variant="fade"><span className="eyebrow">{eyebrow}</span></Reveal>
        )}
        <RevealText as="h1" text={title} className="display" delay={60} />
        {lead && (
          <Reveal variant="up" delay={220}><p className="lead">{lead}</p></Reveal>
        )}
        {children && <Reveal variant="up" delay={320}>{children}</Reveal>}
      </div>
    </header>
  )
}


/* ===== src/components/ui/Breadcrumbs.jsx ===== */
function Breadcrumbs({ items = [] }) {
  return (
    <nav aria-label="Breadcrumb" style={{ marginBottom: '1.25rem' }}>
      <ol style={{ display: 'flex', flexWrap: 'wrap', gap: '.5rem', alignItems: 'center' }}>
        {items.map((item, i) => (
          <li key={item.to} style={{ display: 'flex', gap: '.5rem', alignItems: 'center' }}>
            {i > 0 && <span className="tiny muted" aria-hidden="true">/</span>}
            {i === items.length - 1 ? (
              <span className="tiny muted" aria-current="page">{item.label}</span>
            ) : (
              <Link className="tiny soft" to={item.to}>{item.label}</Link>
            )}
          </li>
        ))}
      </ol>
    </nav>
  )
}


/* ===== src/components/ui/Marquee.jsx ===== */
/** Decorative looping word strip. Duplicated track = seamless loop. */
function Marquee({ items = [] }) {
  const doubled = [...items, ...items]
  return (
    <div className="marquee" aria-hidden="true">
      <div className="marquee__track">
        {doubled.map((item, i) => (
          <span className="marquee__item" key={`${item}-${i}`}>{item}</span>
        ))}
      </div>
    </div>
  )
}


/* ===== src/components/ui/EventCard.jsx ===== */

/** Compact event card — used on the Events page and related-event rails. */
function EventCard({ event }) {
  return (
    <article className="card">
      <Link to={`/events/${event.slug}`} aria-label={`${event.name} — ${cta.discover.label}`}>
        <SmartImage
          image={event.cover}
          ratio="card__media"
          sizes="(max-width: 640px) 100vw, (max-width: 980px) 50vw, 33vw"
          width={768}
          zoom
        />
      </Link>
      <div className="card__body">
        <div className="card__meta">
          {event.signature && <span className="chip chip--accent">Signature</span>}
          <span>{event.specs?.setting}</span>
        </div>
        <h3 className="card__t">
          <Link to={`/events/${event.slug}`}>{event.name}</Link>
        </h3>
        <p className="card__d">{event.summary}</p>
        <div className="card__foot">
          <Link className="link-u" to={`/events/${event.slug}`}>
            {cta.discover.label}
            <svg width="14" height="9" viewBox="0 0 15 10" fill="none" aria-hidden="true">
              <path d="M1 5h12M9 1l4 4-4 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </Link>
        </div>
      </div>
    </article>
  )
}


/* ===== src/components/ui/NewsCard.jsx ===== */
function NewsCard({ article }) {
  return (
    <article className="card">
      <Link to={`/news/${article.slug}`} tabIndex={-1} aria-hidden="true">
        <SmartImage
          image={article.image}
          ratio="card__media"
          sizes="(max-width: 640px) 100vw, (max-width: 980px) 50vw, 33vw"
          width={768}
          zoom
        />
      </Link>
      <div className="card__body">
        <div className="card__meta">
          <span className="chip">{article.category}</span>
          <time dateTime={article.date}>{formatDate(article.date)}</time>
        </div>
        <h3 className="card__t">
          <Link to={`/news/${article.slug}`}>{article.title}</Link>
        </h3>
        <p className="card__d">{article.excerpt}</p>
        <div className="card__foot">
          <Link className="link-u" to={`/news/${article.slug}`}>Read more</Link>
        </div>
      </div>
    </article>
  )
}


/* ===== src/components/ui/Counter.jsx ===== */

/** Animated statistic. Number is real text, so it stays accessible + indexable. */
function Counter({ value, suffix = '', label, duration = 1600 }) {
  const [ref, inView] = useInView({ threshold: 0.4 })
  const isNumeric = typeof value === 'number'
  const n = useCountUp(isNumeric ? value : 0, inView, duration)

  return (
    <div ref={ref} className="glass stat">
      <div className="stat__n">
        <span>{isNumeric ? n : value}</span>
        {suffix && <span className="stat__suffix">{suffix}</span>}
      </div>
      {label && <p className="stat__l">{label}</p>}
    </div>
  )
}


/* ===== src/components/ui/ScrollToTop.jsx ===== */

/** Restores scroll position to top on route change (skips in-page #anchors). */
function ScrollToTop() {
  const { pathname, hash } = useLocation()
  useEffect(() => {
    if (hash) return
    window.scrollTo({ top: 0, left: 0, behavior: 'auto' })
  }, [pathname, hash])
  return null
}


/* ===== src/components/layout/Footer.jsx ===== */
function Footer() {
  const year = new Date().getFullYear()

  return (
    <footer className="footer">
      <div className="shell">
        <div className="footer__top">
          <div>
            <Link to="/" className="logo" aria-label={`${company.name} — home`}>
              <span>{company.name}</span>
              <span className="logo__dot" aria-hidden="true" />
            </Link>
            <p className="small soft" style={{ marginTop: '1rem', maxWidth: '34ch' }}>
              {company.tagline} Corporate experiences designed around communication,
              connection, efficiency and sustainability.
            </p>
            <address className="small soft" style={{ marginTop: '1.5rem', fontStyle: 'normal', display: 'grid', gap: '.35rem' }}>
              <a href={`mailto:${company.email}`}>{company.email}</a>
              <a href={`tel:${company.phoneHref}`}>{company.phone}</a>
              <span className="muted">{company.address.city}, {company.address.country}</span>
            </address>
          </div>

          {footerColumns.map((col) => (
            <nav key={col.title} aria-label={col.title}>
              <h2 className="footer__h">{col.title}</h2>
              <ul className="footer__list">
                {col.links.map((l) => (
                  <li key={l.label + l.to}><Link to={l.to}>{l.label}</Link></li>
                ))}
              </ul>
            </nav>
          ))}
        </div>

        <div className="footer__bottom">
          <span>© {year} {company.legalName}. All rights reserved.</span>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.2rem' }}>
            {company.socials.map((s) => (
              <a key={s.label} href={s.href} target="_blank" rel="noopener noreferrer">{s.label}</a>
            ))}
            {legalLinks.map((l) => (<Link key={l.label} to={l.to}>{l.label}</Link>))}
            <span className="muted">{company.chamberOfCommerce}</span>
          </div>
        </div>

        {/* Hidden crawlable duplicate of the main nav for very small footers */}
        <nav className="sr-only" aria-label="Footer site map">
          <ul>{navigation.map((n) => (<li key={n.to}><Link to={n.to}>{n.label}</Link></li>))}</ul>
        </nav>
      </div>
    </footer>
  )
}


/* ===== src/components/layout/Header.jsx ===== */

function Logo({ onClick }) {
  return (
    <NavLink to="/" className="logo" onClick={onClick} aria-label={`${company.name} — home`}>
      <span>{company.name}</span>
      <span className="logo__dot" aria-hidden="true" />
    </NavLink>
  )
}
function Header() {
  const scrolled = useScrolled(20)
  const [open, setOpen] = useState(false)
  const { pathname } = useLocation()

  // Close the drawer on navigation.
  useEffect(() => { setOpen(false) }, [pathname])

  // Lock body scroll + close on Escape while the drawer is open.
  useEffect(() => {
    document.body.classList.toggle('is-locked', open)
    const onKey = (e) => e.key === 'Escape' && setOpen(false)
    window.addEventListener('keydown', onKey)
    return () => {
      document.body.classList.remove('is-locked')
      window.removeEventListener('keydown', onKey)
    }
  }, [open])

  return (
    <>
      <header className={`header ${scrolled || open ? 'is-stuck' : ''}`}>
        <div className="header__inner">
          <Logo />

          <nav className="nav" aria-label="Main navigation">
            {navigation.map((item) => (
              <NavLink key={item.to} to={item.to} end={item.to === '/'} className="nav__link">
                {item.label}
              </NavLink>
            ))}
          </nav>

          <div className="header__actions">
            <Button to={cta.contact.to} variant="glass" size="sm">{cta.contact.label}</Button>
            <Button to={cta.login.to} variant="primary" size="sm">{cta.login.label}</Button>
          </div>

          <button
            type="button"
            className={`burger ${open ? 'is-open' : ''}`}
            aria-expanded={open}
            aria-controls="mobile-drawer"
            aria-label={open ? 'Close menu' : 'Open menu'}
            onClick={() => setOpen((v) => !v)}
          >
            <span className="burger__box" aria-hidden="true">
              <span className="burger__bar" />
              <span className="burger__bar" />
              <span className="burger__bar" />
            </span>
          </button>
        </div>
      </header>

      <div id="mobile-drawer" className={`drawer ${open ? 'is-open' : ''}`} aria-hidden={!open}>
        <nav aria-label="Mobile navigation">
          <ul className="drawer__list">
            {navigation.map((item, i) => (
              <li key={item.to}>
                <NavLink
                  to={item.to}
                  end={item.to === '/'}
                  className="drawer__link"
                  style={{ transitionDelay: open ? `${120 + i * 60}ms` : '0ms' }}
                  tabIndex={open ? 0 : -1}
                >
                  <span className="drawer__idx">0{i + 1}</span>
                  {item.label}
                </NavLink>
              </li>
            ))}
          </ul>
        </nav>
        <div className="drawer__actions">
          <Button to={cta.contact.to} variant="accent" block tabIndex={open ? 0 : -1}>{cta.contact.label}</Button>
          <Button to={cta.login.to} variant="glass" block tabIndex={open ? 0 : -1}>{cta.login.label}</Button>
        </div>
      </div>
    </>
  )
}


/* ===== src/components/layout/Layout.jsx ===== */

function RouteFallback() {
  return <div className="loader" role="status" aria-live="polite">Loading…</div>
}
function Layout() {
  const { pathname } = useLocation()

  return (
    <>
      <a className="skip-link" href="#main">Skip to content</a>
      <ScrollToTop />
      <Header />
      <main id="main" key={pathname} className="route-fade">
        <Suspense fallback={<RouteFallback />}>
          <Outlet />
        </Suspense>
      </main>
      <Footer />
    </>
  )
}

/** Layout without chrome — used by /login. */
function BareLayout() {
  const { pathname } = useLocation()
  return (
    <>
      <a className="skip-link" href="#main">Skip to content</a>
      <ScrollToTop />
      <Header />
      <main id="main" key={pathname}>
        <Suspense fallback={<RouteFallback />}>
          <Outlet />
        </Suspense>
      </main>
    </>
  )
}


/* ===== src/components/sections/Hero.jsx ===== */
function Hero() {
  const bgRef = useParallax(0.14)

  return (
    <section className="hero" aria-labelledby="hero-title">
      <div className="hero__bg" ref={bgRef}>
        <SmartImage
          image={images.heroMain}
          priority
          width={1920}
          sizes="100vw"
          fill
        />
      </div>
      <div className="hero__scrim" aria-hidden="true" />
      <div className="hero__grain" aria-hidden="true" />

      <div className="shell hero__inner">
        <Reveal variant="fade"><span className="eyebrow">{hero.eyebrow}</span></Reveal>

        <RevealText as="h1" text={hero.title} className="display hero__title" delay={120} />

        <Reveal variant="up" delay={520}>
          <p className="lead hero__lead">{hero.lead}</p>
        </Reveal>

        <Reveal variant="up" delay={640} className="row hero__cta">
          <Button to={cta.primary.to} variant="accent" size="lg" arrow>{cta.primary.label}</Button>
          <Button to={cta.secondary.to} variant="glass" size="lg">{cta.secondary.label}</Button>
        </Reveal>

        <Reveal variant="up" delay={760} className="hero__meta">
          {hero.stats.map((s) => (
            <div className="hero__meta-item" key={s.value}>
              <div className="hero__meta-n">{s.value}</div>
              <div className="small muted">{s.label}</div>
            </div>
          ))}
        </Reveal>
      </div>

      <div className="hero__scroll" aria-hidden="true">
        <span />
        Scroll
      </div>
    </section>
  )
}


/* ===== src/components/sections/Intro.jsx ===== */
function Intro() {
  return (
    <Section>
      <div className="split split--wide-left">
        <div>
          <Reveal variant="fade"><span className="eyebrow">{intro.eyebrow}</span></Reveal>
          <Reveal variant="up" delay={80}>
            <h2 style={{ marginTop: '1rem' }}>{intro.title}</h2>
          </Reveal>
          {intro.paragraphs.map((p, i) => (
            <Reveal variant="up" delay={160 + i * 80} key={i}>
              <p className="lead" style={{ marginTop: '1.2rem' }}>{p}</p>
            </Reveal>
          ))}
          <Reveal variant="up" delay={340} style={{ marginTop: '2rem' }}>
            <Button to={intro.cta.to} arrow>{intro.cta.label}</Button>
          </Reveal>
        </div>

        <div style={{ display: 'grid', gap: '1rem' }}>
          <Reveal variant="scale">
            <SmartImage image={images.introCollage} ratio="ratio-4-3"
              sizes="(max-width: 900px) 100vw, 40vw" width={1024} zoom />
          </Reveal>
          <Reveal variant="scale" delay={140} style={{ width: '78%', marginLeft: 'auto' }}>
            <SmartImage image={images.introSecondary} ratio="ratio-16-9"
              sizes="(max-width: 900px) 78vw, 32vw" width={768} zoom />
          </Reveal>
        </div>
      </div>

      <div style={{ marginTop: 'clamp(3rem, 7vw, 5.5rem)' }}>
        <Marquee items={marqueeWords} />
      </div>
    </Section>
  )
}


/* ===== src/components/sections/WhatWeDo.jsx ===== */
function WhatWeDo() {
  return (
    <Section id="what-we-do" tight>
      <SectionHead eyebrow={whatWeDo.eyebrow} title={whatWeDo.title} lead={whatWeDo.lead} />

      <ul className="pillars">
        {whatWeDo.pillars.map((p, i) => (
          <Reveal as="li" key={p.id} variant="up" delay={i * 80} className="pillar">
            <span className="pillar__n">{p.n}</span>
            <div>
              <h3 className="pillar__t">{p.title}</h3>
              <p className="pillar__d">{p.desc}</p>
            </div>
          </Reveal>
        ))}
      </ul>
    </Section>
  )
}


/* ===== src/components/sections/SignatureEvents.jsx ===== */
function SignatureEvents() {
  const list = signatureEvents()

  return (
    <Section id="signature" aura>
      <SectionHead eyebrow={signature.eyebrow} title={signature.title} lead={signature.lead} />

      <div className="sig">
        {list.map((event, i) => (
          <Reveal key={event.slug} variant="up" delay={i * 110}>
            <article className="sigcard">
              <Link to={`/events/${event.slug}`} className="sigcard__media" tabIndex={-1} aria-hidden="true">
                <SmartImage
                  image={event.cover}
                  fill
                  sizes="(max-width: 980px) 100vw, 33vw"
                  width={1024}
                  zoom
                  scrim
                />
                <span className="chip chip--accent sigcard__num">0{i + 1}</span>
              </Link>

              <div className="sigcard__body">
                <h3 className="sigcard__t">
                  <Link to={`/events/${event.slug}`}>{event.name}</Link>
                </h3>
                <p className="small accent">{event.tagline}</p>
                <p className="card__d">{event.summary}</p>

                <div className="sigcard__tags">
                  {event.highlights.map((h) => (<span className="chip" key={h}>{h}</span>))}
                </div>

                <div className="sigcard__foot">
                  <span className="tiny muted">{event.specs.duration} · {event.specs.groupSize}</span>
                  <Button to={`/events/${event.slug}`} size="sm" variant="glass" arrow>
                    {cta.discover.label}
                  </Button>
                </div>
              </div>
            </article>
          </Reveal>
        ))}
      </div>

      <Reveal variant="fade" delay={200} style={{ marginTop: '2.5rem', textAlign: 'center' }}>
        <Button to={cta.allEvents.to} variant="ghost" arrow>{cta.allEvents.label}</Button>
      </Reveal>
    </Section>
  )
}


/* ===== src/components/sections/WhyEventuur.jsx ===== */
function WhyEventuur() {
  return (
    <Section id="why" className="section--why">
      {/* Decorative backdrop */}
      <div aria-hidden="true" style={{ position: 'absolute', inset: 0, zIndex: 0, opacity: 0.16 }}>
        <SmartImage image={images.whyBackdrop} fill sizes="100vw" width={1440} />
        <div style={{
          position: 'absolute', inset: 0,
          background: 'linear-gradient(to bottom, var(--c-bg), rgba(10,12,15,.75) 40%, var(--c-bg))',
        }} />
      </div>

      <SectionHead eyebrow={why.eyebrow} title={why.title} lead={why.lead} />

      <ul className="outcomes">
        {why.outcomes.map((o, i) => (
          <Reveal as="li" key={o.n} variant="up" delay={i * 60} className="outcome">
            <span className="outcome__n">{o.n}</span>
            <div>
              <h3 className="outcome__t">{o.title}</h3>
              <p className="outcome__d">{o.desc}</p>
            </div>
            <span aria-hidden="true" className="muted">
              <svg width="16" height="10" viewBox="0 0 15 10" fill="none">
                <path d="M1 5h12M9 1l4 4-4 4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
            </span>
          </Reveal>
        ))}
      </ul>

      <div className="stats" style={{ marginTop: 'clamp(2.5rem, 5vw, 4rem)' }}>
        {why.stats.map((s, i) => (
          <Reveal key={s.label} variant="up" delay={i * 90}>
            <Counter value={s.value} suffix={s.suffix} label={s.label} />
          </Reveal>
        ))}
      </div>
      {why.statsNote && (
        <p className="tiny muted" style={{ marginTop: '1rem' }}>{why.statsNote}</p>
      )}
    </Section>
  )
}


/* ===== src/components/sections/CustomEvents.jsx ===== */
function CustomEvents() {
  return (
    <Section id="custom">
      <div className="split">
        <div>
          <Reveal variant="fade"><span className="eyebrow">{custom.eyebrow}</span></Reveal>
          <Reveal variant="up" delay={80}><h2 style={{ marginTop: '1rem' }}>{custom.title}</h2></Reveal>
          <Reveal variant="up" delay={160}><p className="lead" style={{ marginTop: '1.2rem' }}>{custom.lead}</p></Reveal>

          <Reveal variant="up" delay={240} className="row" style={{ marginTop: '1.8rem' }}>
            {custom.factors.map((f) => (<span className="chip" key={f}>{f}</span>))}
          </Reveal>

          <Reveal variant="up" delay={320} style={{ marginTop: '2rem' }}>
            <Button to={cta.custom.to} variant="accent" arrow>{cta.custom.label}</Button>
          </Reveal>
        </div>

        <div style={{ display: 'grid', gap: '.9rem' }}>
          {custom.questions.map((item, i) => (
            <Reveal key={item.q} variant="left" delay={i * 90}>
              <div className="glass glass--hover" style={{ padding: '1.35rem 1.5rem', borderRadius: 'var(--radius)' }}>
                <h3 style={{ fontSize: '1.05rem' }}>{item.q}</h3>
                <p className="small soft" style={{ marginTop: '.4rem' }}>{item.a}</p>
              </div>
            </Reveal>
          ))}
          <Reveal variant="scale" delay={400}>
            <SmartImage image={images.customBackdrop} ratio="ratio-16-9"
              sizes="(max-width: 900px) 100vw, 45vw" width={1024} zoom />
          </Reveal>
        </div>
      </div>
    </Section>
  )
}


/* ===== src/components/sections/NewsTeaser.jsx ===== */
function NewsTeaser() {
  const items = latestArticles(3)
  if (!items.length) return null

  return (
    <Section id="news">
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.5rem', justifyContent: 'space-between', alignItems: 'flex-end' }}>
        <SectionHead eyebrow={newsTeaser.eyebrow} title={newsTeaser.title} lead={newsTeaser.lead} />
        <Reveal variant="fade" delay={160} style={{ marginBottom: 'clamp(2.2rem, 4vw, 3.5rem)' }}>
          <Button to={cta.allNews.to} variant="glass" size="sm" arrow>{cta.allNews.label}</Button>
        </Reveal>
      </div>

      <div className="cards">
        {items.map((a, i) => (
          <Reveal key={a.slug} variant="up" delay={i * 100}>
            <NewsCard article={a} />
          </Reveal>
        ))}
      </div>
    </Section>
  )
}


/* ===== src/components/sections/FinalCTA.jsx ===== */
function FinalCTA() {
  return (
    <Section>
      <div className="cta-band">
        <div className="cta-band__bg">
          <SmartImage image={images.ctaBackdrop} fill sizes="100vw" width={1440} />
        </div>
        <div className="cta-band__veil" aria-hidden="true" />

        <RevealText as="h2" text={finalCta.title} className="display" />

        <Reveal variant="up" delay={260}>
          <p className="lead mx-auto" style={{ marginTop: '1.4rem', textAlign: 'center' }}>{finalCta.lead}</p>
        </Reveal>

        <Reveal variant="up" delay={360} className="row" style={{ justifyContent: 'center', marginTop: '2.2rem' }}>
          <Button to={cta.contactLong.to} variant="primary" size="lg" arrow>{cta.contactLong.label}</Button>
          <Button href={`mailto:${company.email}`} variant="glass" size="lg">{company.email}</Button>
        </Reveal>
      </div>
    </Section>
  )
}


/* ===== src/pages/HomePage.jsx ===== */
function HomePage() {
  useSeo({
    title: null, // uses defaultTitle
    description: seoDefaults.description,
    path: '/',
    jsonLd: organizationSchema(company),
  })

  return (
    <>
      <Hero />
      <Intro />
      <WhatWeDo />
      <SignatureEvents />
      <WhyEventuur />
      <CustomEvents />
      <NewsTeaser />
      <FinalCTA />
    </>
  )
}


/* ===== src/pages/EventsPage.jsx ===== */
function EventsPage() {
  const [params, setParams] = useSearchParams()
  const initial = params.get('category') ?? 'all'
  const [active, setActive] = useState(
    eventCategories.some((c) => c.id === initial) ? initial : 'all'
  )

  const list = useMemo(() => filterEvents(active), [active])

  const select = (id) => {
    setActive(id)
    if (id === 'all') setParams({}, { replace: true })
    else setParams({ category: id }, { replace: true })
  }

  useSeo({
    title: 'Corporate Events & Team Experiences',
    description:
      'Browse every Eventuur experience: bootcamps, mini games, scavenger hunts, networking events and fully custom corporate activities.',
    path: '/events',
  })

  return (
    <>
      <PageHero
        eyebrow="Experiences"
        title="Every experience we run."
        lead="Filter by what you want the day to achieve. Every format below can be scaled and rebuilt around your team."
        image={images.miniGamesHero}
      />

      <Section>
        <div className="filters" role="group" aria-label="Filter experiences by category">
          {eventCategories.map((c) => (
            <button
              key={c.id}
              type="button"
              className="filter"
              aria-pressed={active === c.id}
              onClick={() => select(c.id)}
            >
              {c.label}
            </button>
          ))}
        </div>

        <p className="sr-only" aria-live="polite">
          {list.length} experience{list.length === 1 ? '' : 's'} shown.
        </p>

        {list.length ? (
          <div className="cards">
            {list.map((event, i) => (
              <Reveal key={event.slug} variant="up" delay={Math.min(i, 5) * 80}>
                <EventCard event={event} />
              </Reveal>
            ))}
          </div>
        ) : (
          <div className="glass glass--pad center">
            <p className="soft">No experiences match that filter yet — but we can build one.</p>
          </div>
        )}
      </Section>

      <FinalCTA />
    </>
  )
}


/* ===== src/pages/EventDetailPage.jsx ===== */

function List({ title, items }) {
  if (!items?.length) return null
  return (
    <div className="rich" style={{ marginTop: '2.5rem' }}>
      <h3>{title}</h3>
      <ul>{items.map((i) => (<li key={i}>{i}</li>))}</ul>
    </div>
  )
}
function EventDetailPage() {
  const { slug } = useParams()
  const event = getEventBySlug(slug)

  // Hooks must run unconditionally — SEO is a no-op for an unknown slug.
  useSeo({
    title: event ? (event.seo?.title?.replace(' | Eventuur', '') ?? event.name) : 'Page not found',
    description: event?.seo?.description ?? event?.summary,
    path: event ? `/events/${event.slug}` : undefined,
    type: 'article',
    noindex: !event,
    jsonLd: event ? eventServiceSchema(event) : null,
  })

  // Unknown slug → real 404, not a blank page.
  if (!event) return <Navigate to="/404" replace />

  const related = sortedEvents().filter((e) => e.slug !== event.slug).slice(0, 3)

  return (
    <>
      <PageHero
        eyebrow={event.signature ? 'Signature experience' : 'Experience'}
        title={event.name}
        lead={event.tagline}
        image={event.hero || event.cover}
      >
        <div className="row" style={{ marginTop: '1.8rem' }}>
          {event.categories.map((c) => (
            <Link key={c} to={`/events?category=${c}`} className="chip">{categoryLabel(c)}</Link>
          ))}
        </div>
      </PageHero>

      <Section>
        <Breadcrumbs
          items={[
            { label: 'Home', to: '/' },
            { label: 'Events', to: '/events' },
            { label: event.name, to: `/events/${event.slug}` },
          ]}
        />

        <div className="detail-grid">
          {/* ---------------- Main column ---------------- */}
          <div>
            <Reveal variant="up">
              <p className="lead">{event.summary}</p>
            </Reveal>

            <Reveal variant="up" delay={80}>
              <div className="rich" style={{ marginTop: '1.6rem' }}>
                {event.description?.map((p, i) => (<p key={i} className="soft">{p}</p>))}
              </div>
            </Reveal>

            <Reveal variant="up" delay={120}><List title="Main objectives" items={event.objectives} /></Reveal>
            <Reveal variant="up" delay={140}><List title="What participants do" items={event.participantsDo} /></Reveal>
            <Reveal variant="up" delay={160}><List title="What your company gains" items={event.companyGains} /></Reveal>

            {event.sustainability && (
              <Reveal variant="up" delay={180}>
                <div className="glass glass--pad" style={{ marginTop: '2.5rem' }}>
                  <span className="eyebrow eyebrow--plain">Sustainability</span>
                  <p className="soft" style={{ marginTop: '.8rem' }}>{event.sustainability}</p>
                </div>
              </Reveal>
            )}
          </div>

          {/* ---------------- Sticky sidebar ---------------- */}
          <aside className="sticky-side">
            <Reveal variant="up" delay={100}>
              <div className="glass glass--pad">
                <span className="eyebrow eyebrow--plain">At a glance</span>
                <div className="spec" style={{ marginTop: '1.1rem' }}>
                  {event.specs?.groupSize && <Row k="Group size" v={event.specs.groupSize} />}
                  {event.specs?.duration && <Row k="Duration" v={event.specs.duration} />}
                  {event.specs?.setting && <Row k="Setting" v={event.specs.setting} />}
                  {event.specs?.intensity && <Row k="Intensity" v={event.specs.intensity} />}
                  {event.specs?.season && <Row k="Availability" v={event.specs.season} />}
                </div>

                {event.highlights?.length > 0 && (
                  <div className="row" style={{ marginTop: '1.4rem' }}>
                    {event.highlights.map((h) => (<span className="chip chip--accent" key={h}>{h}</span>))}
                  </div>
                )}

                <div style={{ display: 'grid', gap: '.6rem', marginTop: '1.8rem' }}>
                  <Button to={`/contact?event=${event.slug}`} variant="accent" block arrow>
                    {cta.request.label}
                  </Button>
                  <Button href={`mailto:${company.email}?subject=${encodeURIComponent(`Enquiry: ${event.name}`)}`}
                          variant="glass" block>
                    Email us directly
                  </Button>
                </div>
              </div>
            </Reveal>
          </aside>
        </div>
      </Section>

      {/* ---------------- Gallery ---------------- */}
      {event.gallery?.length > 0 && (
        <Section tight>
          <span className="eyebrow">Gallery</span>
          <p className="tiny muted" style={{ marginTop: '.6rem', marginBottom: '1.5rem' }}>
            Placeholder imagery — to be replaced with photography from real Eventuur events.
          </p>
          <div className="gallery">
            {event.gallery.map((img, i) => (
              <Reveal key={i} variant="scale" delay={i * 90}>
                <SmartImage image={img} ratio="ratio-4-3" zoom width={768}
                  sizes="(max-width: 700px) 50vw, 33vw" />
              </Reveal>
            ))}
          </div>
        </Section>
      )}

      {/* ---------------- Related ---------------- */}
      <Section tight>
        <h2 style={{ marginBottom: '1.8rem' }}>Other experiences</h2>
        <div className="cards">
          {related.map((e, i) => (
            <Reveal key={e.slug} variant="up" delay={i * 90}><EventCard event={e} /></Reveal>
          ))}
        </div>
      </Section>
    </>
  )
}

function Row({ k, v }) {
  return (
    <div className="spec__row">
      <span className="spec__k">{k}</span>
      <span className="spec__v">{v}</span>
    </div>
  )
}


/* ===== src/pages/AboutPage.jsx ===== */
function AboutPage() {
  useSeo({
    title: 'Get to Know Us',
    description:
      'Why Eventuur exists, what we believe about company events, and the values behind every experience we design.',
    path: '/get-to-know-us',
  })

  return (
    <>
      <PageHero eyebrow={about.eyebrow} title={about.title} lead={about.lead} image={images.aboutHero} />

      <Section>
        <div className="split split--wide-left">
          <div className="rich">
            {about.story.map((p, i) => (
              <Reveal key={i} variant="up" delay={i * 90}>
                <p className={i === 0 ? 'lead' : 'soft'}>{p}</p>
              </Reveal>
            ))}
          </div>
          <Reveal variant="scale" delay={120}>
            <SmartImage image={images.aboutSecondary} ratio="ratio-3-4" zoom width={768}
              sizes="(max-width: 900px) 100vw, 40vw" />
          </Reveal>
        </div>
      </Section>

      <Section tight>
        <div className="grid grid--2">
          {[about.mission, about.vision].map((b, i) => (
            <Reveal key={b.title} variant="up" delay={i * 120}>
              <div className="glass glass--pad" style={{ height: '100%' }}>
                <span className="eyebrow eyebrow--plain">{b.title}</span>
                <p className="lead" style={{ marginTop: '1rem' }}>{b.text}</p>
              </div>
            </Reveal>
          ))}
        </div>
      </Section>

      <Section>
        <SectionHead eyebrow="Our values" title="What we hold ourselves to." />
        <ul className="outcomes">
          {about.values.map((v, i) => (
            <Reveal as="li" key={v.title} variant="up" delay={i * 70} className="outcome">
              <span className="outcome__n">0{i + 1}</span>
              <div>
                <h3 className="outcome__t">{v.title}</h3>
                <p className="outcome__d">{v.desc}</p>
              </div>
              <span />
            </Reveal>
          ))}
        </ul>
      </Section>

      <Section tight>
        <SectionHead eyebrow="How we design" title={whatWeDo.title} lead={whatWeDo.lead} />
        <ul className="pillars">
          {whatWeDo.pillars.map((p, i) => (
            <Reveal as="li" key={p.id} variant="up" delay={i * 70} className="pillar">
              <span className="pillar__n">{p.n}</span>
              <div>
                <h3 className="pillar__t">{p.title}</h3>
                <p className="pillar__d">{p.desc}</p>
              </div>
            </Reveal>
          ))}
        </ul>
      </Section>

      <FinalCTA />
    </>
  )
}


/* ===== src/pages/NewsPage.jsx ===== */
function NewsPage() {
  const [category, setCategory] = useState('All')
  const list = filterArticles(category)

  useSeo({
    title: 'News & Stories',
    description:
      'New formats, behind-the-scenes notes, sustainability updates and practical thinking on corporate team building from Eventuur.',
    path: '/news',
  })

  return (
    <>
      <PageHero
        eyebrow="News & stories"
        title="What we have been working on."
        lead="New experiences, sustainability updates, and honest thinking about what makes a company event worth the budget."
        image={images.newsHybrid}
      />

      <Section>
        <div className="filters" role="group" aria-label="Filter articles by category">
          {newsCategories.map((c) => (
            <button key={c} type="button" className="filter"
                    aria-pressed={category === c} onClick={() => setCategory(c)}>
              {c}
            </button>
          ))}
        </div>

        <div className="cards">
          {list.map((a, i) => (
            <Reveal key={a.slug} variant="up" delay={Math.min(i, 5) * 80}>
              <NewsCard article={a} />
            </Reveal>
          ))}
        </div>

        {!list.length && (
          <div className="glass glass--pad center"><p className="soft">Nothing here yet.</p></div>
        )}
      </Section>

      <FinalCTA />
    </>
  )
}


/* ===== src/pages/ArticlePage.jsx ===== */

function Block({ block }) {
  if (block.type === 'h3') return <h3>{block.text}</h3>
  if (block.type === 'ul') return <ul>{block.items.map((i) => (<li key={i}>{i}</li>))}</ul>
  return <p className="soft">{block.text}</p>
}
function ArticlePage() {
  const { slug } = useParams()
  const article = getArticleBySlug(slug)

  // Hooks must run unconditionally.
  useSeo({
    title: article?.title ?? 'Page not found',
    description: article?.excerpt,
    path: article ? `/news/${article.slug}` : undefined,
    type: 'article',
    noindex: !article,
    jsonLd: article ? articleSchema(article) : null,
  })

  if (!article) return <Navigate to="/404" replace />

  const more = sortedArticles().filter((a) => a.slug !== slug).slice(0, 3)

  return (
    <>
      <Section style={{ paddingTop: 'calc(var(--header-h) + 3rem)' }} narrow>
        <Breadcrumbs items={[
          { label: 'Home', to: '/' },
          { label: 'News', to: '/news' },
          { label: article.title, to: `/news/${article.slug}` },
        ]} />

        <div className="row" style={{ marginBottom: '1.2rem' }}>
          <span className="chip chip--accent">{article.category}</span>
          <time className="tiny muted" dateTime={article.date}>{formatDate(article.date)}</time>
          {article.readingMinutes && <span className="tiny muted">{article.readingMinutes} min read</span>}
        </div>

        <Reveal variant="up"><h1>{article.title}</h1></Reveal>
        <Reveal variant="up" delay={90}>
          <p className="lead" style={{ marginTop: '1.2rem' }}>{article.excerpt}</p>
        </Reveal>
      </Section>

      <Section tight className="section--flush-top">
        <Reveal variant="scale">
          <SmartImage image={article.image} ratio="ratio-21-9" priority width={1440} sizes="100vw" />
        </Reveal>
      </Section>

      <Section narrow className="section--flush-top">
        <article className="rich">
          {article.body.map((b, i) => (
            <Reveal key={i} variant="up" delay={Math.min(i, 4) * 60}><Block block={b} /></Reveal>
          ))}
        </article>

        <div className="glass glass--pad" style={{ marginTop: '3rem', textAlign: 'center' }}>
          <h2 style={{ fontSize: 'var(--fs-h3)' }}>Thinking about an event for your team?</h2>
          <div className="row" style={{ justifyContent: 'center', marginTop: '1.2rem' }}>
            <Button to="/contact" variant="accent" arrow>Plan Your Event</Button>
            <Button to="/events" variant="glass">Explore Our Events</Button>
          </div>
        </div>
      </Section>

      <Section tight>
        <h2 style={{ marginBottom: '1.8rem' }}>More from Eventuur</h2>
        <div className="cards">
          {more.map((a, i) => (
            <Reveal key={a.slug} variant="up" delay={i * 90}><NewsCard article={a} /></Reveal>
          ))}
        </div>
      </Section>
    </>
  )
}


/* ===== src/pages/InformationPage.jsx ===== */

function Faq({ item, index }) {
  const [open, setOpen] = useState(index === 0)
  const id = `faq-${index}`
  return (
    <div style={{ borderBottom: '1px solid var(--hairline)' }}>
      <h3>
        <button
          type="button"
          onClick={() => setOpen((v) => !v)}
          aria-expanded={open}
          aria-controls={id}
          style={{
            width: '100%', display: 'flex', justifyContent: 'space-between', gap: '1.5rem',
            alignItems: 'center', padding: '1.15rem 0', textAlign: 'left',
            fontFamily: 'var(--font-display)', fontSize: '1.05rem', fontWeight: 400,
          }}
        >
          {item.q}
          <span aria-hidden="true" style={{
            color: 'var(--c-accent)', flex: 'none',
            transform: open ? 'rotate(45deg)' : 'none',
            transition: 'transform var(--dur) var(--ease)',
          }}>+</span>
        </button>
      </h3>
      <div id={id} hidden={!open}>
        <p className="soft small" style={{ paddingBottom: '1.2rem', maxWidth: '70ch' }}>{item.a}</p>
      </div>
    </div>
  )
}
function InformationPage() {
  useSeo({
    title: 'Why Organise a Company Event?',
    description:
      'A straight answer for decision makers: what a well-designed corporate event actually delivers, how the process works, and what it costs.',
    path: '/information',
  })

  return (
    <>
      <PageHero
        eyebrow={information.eyebrow}
        title={information.title}
        lead={information.lead}
        image={images.informationHero}
      />

      <Section>
        <div className="rich" style={{ maxWidth: '70ch' }}>
          {information.intro.map((p, i) => (
            <Reveal key={i} variant="up" delay={i * 90}>
              <p className={i === 0 ? 'lead' : 'soft'}>{p}</p>
            </Reveal>
          ))}
        </div>

        <div className="grid grid--3" style={{ marginTop: 'clamp(2.5rem, 5vw, 4rem)' }}>
          {information.benefits.map((b, i) => (
            <Reveal key={b.title} variant="up" delay={Math.min(i, 5) * 70}>
              <div className="glass glass--hover glass--pad" style={{ height: '100%' }}>
                <span className="tiny accent">{String(i + 1).padStart(2, '0')}</span>
                <h3 style={{ fontSize: '1.1rem', marginTop: '.7rem' }}>{b.title}</h3>
                <p className="small soft" style={{ marginTop: '.55rem' }}>{b.desc}</p>
              </div>
            </Reveal>
          ))}
        </div>
      </Section>

      <Section tight>
        <SectionHead eyebrow="Process" title={information.process.title} />
        <ul className="outcomes">
          {information.process.steps.map((s, i) => (
            <Reveal as="li" key={s.n} variant="up" delay={i * 70} className="outcome">
              <span className="outcome__n">{s.n}</span>
              <div>
                <h3 className="outcome__t">{s.title}</h3>
                <p className="outcome__d">{s.desc}</p>
              </div>
              <span />
            </Reveal>
          ))}
        </ul>
      </Section>

      <Section tight>
        <div className="stats">
          {why.stats.map((s, i) => (
            <Reveal key={s.label} variant="up" delay={i * 90}>
              <Counter value={s.value} suffix={s.suffix} label={s.label} />
            </Reveal>
          ))}
        </div>
        <p className="tiny muted" style={{ marginTop: '1rem' }}>{why.statsNote}</p>
      </Section>

      <Section narrow>
        <SectionHead eyebrow="FAQ" title="Practical questions." />
        <div>
          {information.faqs.map((f, i) => (<Faq key={f.q} item={f} index={i} />))}
        </div>
      </Section>

      <FinalCTA />
    </>
  )
}


/* ===== src/pages/ContactPage.jsx ===== */

/* --- One field renderer for every input type ------------------------------ */
function Field({ field, value, error, onChange }) {
  const id = `f-${field.name}`
  const errId = `${id}-err`
  const common = {
    id,
    name: field.name,
    value,
    required: field.required,
    autoComplete: field.autoComplete,
    placeholder: field.placeholder,
    'aria-invalid': error ? 'true' : undefined,
    'aria-describedby': error ? errId : field.hint ? `${id}-hint` : undefined,
    onChange: (e) => onChange(field.name, e.target.value),
  }

  return (
    <div className="field" style={{ gridColumn: field.half ? 'auto' : '1 / -1' }}>
      <label className="field__label" htmlFor={id}>
        {field.label}{field.required && <span className="field__req" aria-hidden="true"> *</span>}
      </label>

      {field.type === 'textarea' && <textarea className="textarea" {...common} />}
      {field.type === 'select' && (
        <select className="select" {...common}>
          <option value="">Select…</option>
          {field.options.map((o) => (<option key={o} value={o}>{o}</option>))}
        </select>
      )}
      {!['textarea', 'select'].includes(field.type) && (
        <input className="input" type={field.type} {...common} />
      )}

      {field.hint && !error && <span className="field__hint" id={`${id}-hint`}>{field.hint}</span>}
      {error && <span className="field__error" id={errId} role="alert">{error}</span>}
    </div>
  )
}
function ContactPage() {
  const [params] = useSearchParams()
  const [values, setValues] = useState(() => ({ ...emptyEnquiry(), consent: false }))
  const [errors, setErrors] = useState({})
  const [result, setResult] = useState({ status: Status.IDLE, message: '' })
  const noticeRef = useRef(null)

  // Prefill the event type from ?event=slug or ?type=custom
  useEffect(() => {
    const slug = params.get('event')
    const type = params.get('type')
    const evt = slug && getEventBySlug(slug)
    if (evt) setValues((v) => ({ ...v, eventType: evt.name }))
    else if (type === 'custom') setValues((v) => ({ ...v, eventType: 'Custom Experience' }))
  }, [params])

  useSeo({
    title: 'Contact',
    description:
      'Tell Eventuur about your team and what you want your event to achieve. We reply to every enquiry within two working days.',
    path: '/contact',
  })

  const update = (name, value) => {
    setValues((v) => ({ ...v, [name]: value }))
    setErrors((e) => (e[name] ? { ...e, [name]: undefined } : e))
  }

  const onSubmit = async (e) => {
    e.preventDefault()
    const found = validateEnquiry(values)
    setErrors(found)
    if (Object.keys(found).length) {
      document.getElementById(`f-${Object.keys(found)[0]}`)?.focus()
      return
    }

    setResult({ status: Status.SUBMITTING, message: '' })
    const res = await submitEnquiry(values)
    setResult(res)
    if (res.status === Status.SUCCESS) setValues({ ...emptyEnquiry(), consent: false })
    requestAnimationFrame(() => noticeRef.current?.focus())
  }

  const noticeClass =
    result.status === Status.SUCCESS ? 'notice notice--ok'
    : result.status === Status.NOT_CONFIGURED ? 'notice notice--warn'
    : 'notice notice--err'

  return (
    <>
      <PageHero
        eyebrow={contactCopy.eyebrow}
        title={contactCopy.title}
        lead={contactCopy.lead}
        image={images.heroAlt}
      />

      <Section>
        <div className="split split--wide-left" style={{ alignItems: 'start' }}>
          {/* ------------------------ FORM ------------------------ */}
          <Reveal variant="up">
            <form className="form" onSubmit={onSubmit} noValidate>
              <div className="form__grid">
                {enquiryFields.map((f) => (
                  <Field key={f.name} field={f} value={values[f.name] ?? ''}
                         error={errors[f.name]} onChange={update} />
                ))}
              </div>

              <div className="field">
                <label className="checkbox" htmlFor="f-consent">
                  <input
                    id="f-consent" type="checkbox" name="consent"
                    checked={!!values.consent}
                    aria-invalid={errors.consent ? 'true' : undefined}
                    onChange={(e) => update('consent', e.target.checked)}
                  />
                  <span>I agree that {company.name} may contact me about this enquiry.</span>
                </label>
                {errors.consent && <span className="field__error" role="alert">{errors.consent}</span>}
              </div>

              {/* Honeypot — bots fill this, humans never see it. */}
              <div aria-hidden="true" style={{ position: 'absolute', left: '-9999px' }}>
                <label htmlFor="f-website">Website</label>
                <input id="f-website" name="website" tabIndex={-1} autoComplete="off"
                       value={values.website ?? ''} onChange={(e) => update('website', e.target.value)} />
              </div>

              <div className="row" style={{ justifyContent: 'space-between', gap: '1rem' }}>
                <Button
                  type="submit"
                  variant="accent"
                  size="lg"
                  arrow
                  disabled={result.status === Status.SUBMITTING}
                >
                  {result.status === Status.SUBMITTING ? 'Sending…' : cta.planning.label}
                </Button>
                <span className="tiny muted">{contactCopy.responseTime}</span>
              </div>

              {result.status !== Status.IDLE && result.status !== Status.SUBMITTING && (
                <div className={noticeClass} ref={noticeRef} tabIndex={-1} role="status">
                  <p>{result.message}</p>
                  {result.status === Status.NOT_CONFIGURED && (
                    <p style={{ marginTop: '.6rem' }}>
                      In the meantime, email us at{' '}
                      <a href={`mailto:${company.email}`} className="link-u">{company.email}</a>{' '}
                      or call {company.phone}.
                    </p>
                  )}
                </div>
              )}
            </form>
          </Reveal>

          {/* ------------------------ ASIDE ------------------------ */}
          <Reveal variant="left" delay={120}>
            <div className="glass glass--pad" style={{ position: 'sticky', top: 'calc(var(--header-h) + 24px)' }}>
              <span className="eyebrow eyebrow--plain">Direct contact</span>
              <div className="spec" style={{ marginTop: '1.1rem' }}>
                <div className="spec__row">
                  <span className="spec__k">Email</span>
                  <a className="spec__v link-u" href={`mailto:${company.email}`}>{company.email}</a>
                </div>
                <div className="spec__row">
                  <span className="spec__k">Phone</span>
                  <a className="spec__v link-u" href={`tel:${company.phoneHref}`}>{company.phone}</a>
                </div>
                <div className="spec__row">
                  <span className="spec__k">Based in</span>
                  <span className="spec__v">{company.address.city}, {company.address.country}</span>
                </div>
                <div className="spec__row">
                  <span className="spec__k">We work across</span>
                  <span className="spec__v">{company.serviceArea}</span>
                </div>
              </div>

              <hr className="hairline" style={{ margin: '1.5rem 0' }} />

              <p className="small soft">
                Not sure what you want yet? That is fine — most conversations start with a problem,
                not a format. Tell us the problem.
              </p>

              <div className="row" style={{ marginTop: '1.2rem' }}>
                {company.socials.map((s) => (
                  <a key={s.label} className="chip" href={s.href} target="_blank" rel="noopener noreferrer">
                    {s.label}
                  </a>
                ))}
              </div>
            </div>
          </Reveal>
        </div>
      </Section>
    </>
  )
}


/* ===== src/pages/LoginPage.jsx ===== */

/**
 * Client login.
 * ⚠️  Authentication is NOT implemented — see src/lib/auth.js.
 *     This page is a finished UI wired to the real provider interface. When a
 *     provider is configured (`auth.isConfigured === true`), the banner
 *     disappears and the form starts working with zero changes here.
 */
function LoginPage() {
  const { signIn, loading, isConfigured, session } = useAuth()
  const [values, setValues] = useState({ email: '', password: '' })
  const [error, setError] = useState(null)

  useSeo({
    title: 'Client Login',
    description: 'Log in to your Eventuur client area.',
    path: '/login',
    noindex: true,
  })

  const onSubmit = async (e) => {
    e.preventDefault()
    setError(null)
    try {
      await signIn(values)
    } catch (err) {
      setError(err.message)
    }
  }

  return (
    <div className="auth">
      {/* ---------------------- Visual side ---------------------- */}
      <aside className="auth__aside">
        <SmartImage image={images.loginAside} fill sizes="50vw" width={1024} priority />
        <div aria-hidden="true" style={{
          position: 'absolute', inset: 0, zIndex: 1,
          background: 'linear-gradient(to top, rgba(6,8,10,.95), rgba(6,8,10,.5) 60%, rgba(6,8,10,.7))',
        }} />
        <div className="auth__aside-in">
          <Reveal variant="up">
            <span className="eyebrow">Client area</span>
            <h2 style={{ marginTop: '1rem', maxWidth: '18ch' }}>
              Your events, documents and history in one place.
            </h2>
            <p className="small soft" style={{ marginTop: '1rem', maxWidth: '42ch' }}>
              Coming soon: event proposals, participant lists, invoices and rebooking —
              all from one account.
            </p>
          </Reveal>
        </div>
      </aside>

      {/* ---------------------- Form side ---------------------- */}
      <div className="auth__panel">
        <div className="auth__panel-in">
          <Link to="/" className="logo" style={{ marginBottom: '2rem' }}>
            <span>{company.name}</span><span className="logo__dot" aria-hidden="true" />
          </Link>

          <h1 style={{ fontSize: 'var(--fs-h2)' }}>Log in</h1>
          <p className="small soft" style={{ marginTop: '.6rem' }}>
            Access your Eventuur client area.
          </p>

          {!isConfigured && (
            <div className="notice notice--warn" style={{ marginTop: '1.5rem' }}>
              <strong>Not live yet.</strong>
              <p style={{ marginTop: '.4rem' }}>
                Client accounts are not connected to an authentication provider yet, so this form
                cannot log anyone in. It is a real UI on a real interface — see{' '}
                <code>src/lib/auth.js</code> to connect one.
              </p>
            </div>
          )}

          <form className="form" style={{ marginTop: '1.75rem' }} onSubmit={onSubmit} noValidate>
            <div className="field">
              <label className="field__label" htmlFor="login-email">Email</label>
              <input
                id="login-email" className="input" type="email" name="email" required
                autoComplete="email" placeholder="you@company.com"
                value={values.email}
                onChange={(e) => setValues((v) => ({ ...v, email: e.target.value }))}
              />
            </div>

            <div className="field">
              <label className="field__label" htmlFor="login-password">Password</label>
              <input
                id="login-password" className="input" type="password" name="password" required
                autoComplete="current-password" placeholder="••••••••"
                value={values.password}
                onChange={(e) => setValues((v) => ({ ...v, password: e.target.value }))}
              />
            </div>

            {error && <div className="notice notice--err" role="alert">{error}</div>}
            {session && <div className="notice notice--ok" role="status">Signed in.</div>}

            <Button type="submit" variant="accent" size="lg" block disabled={loading}>
              {loading ? 'Checking…' : 'Log in'}
            </Button>
          </form>

          <p className="small muted" style={{ marginTop: '1.75rem' }}>
            No account yet?{' '}
            <Link to="/contact" className="link-u">Get in touch</Link> and we will set one up.
          </p>
          <p className="tiny muted" style={{ marginTop: '2.5rem' }}>
            <Link to="/" className="link-u">← Back to eventuur.com</Link>
          </p>
        </div>
      </div>
    </div>
  )
}


/* ===== src/pages/NotFoundPage.jsx ===== */
function NotFoundPage() {
  useSeo({
    title: 'Page not found',
    description: 'That page does not exist. Head back to the Eventuur homepage.',
    noindex: true,
  })

  return (
    <Section aura style={{ minHeight: '80svh', display: 'flex', alignItems: 'center', paddingTop: 'calc(var(--header-h) + 4rem)' }}>
      <div style={{ maxWidth: '60ch' }}>
        <Reveal variant="fade"><span className="eyebrow">Error 404</span></Reveal>
        <Reveal variant="up" delay={80}>
          <h1 className="display" style={{ marginTop: '1rem' }}>This page went off-route.</h1>
        </Reveal>
        <Reveal variant="up" delay={160}>
          <p className="lead" style={{ marginTop: '1.2rem' }}>
            Fitting, for a company that runs scavenger hunts. The page you asked for does not exist —
            here is the way back.
          </p>
        </Reveal>
        <Reveal variant="up" delay={240} className="row" style={{ marginTop: '2rem' }}>
          <Button to="/" variant="accent" arrow>Back to home</Button>
          <Button to="/contact" variant="glass">Contact us</Button>
        </Reveal>
        <Reveal variant="fade" delay={320} style={{ marginTop: '2.5rem' }}>
          <span className="tiny muted">Or jump to:</span>
          <div className="row" style={{ marginTop: '.7rem' }}>
            {navigation.map((n) => (<Link key={n.to} to={n.to} className="chip">{n.label}</Link>))}
          </div>
        </Reveal>
      </div>
    </Section>
  )
}

/* Alias required because ContactPage originally imported: { contact as contactCopy } */
const contactCopy = contact;

/* Non-lazy route map for direct upload runtime. */
function App() {
  return (
    <Routes>
      <Route element={<Layout />}>
        <Route index element={<HomePage />} />
        <Route path="events" element={<EventsPage />} />
        <Route path="events/:slug" element={<EventDetailPage />} />
        <Route path="get-to-know-us" element={<AboutPage />} />
        <Route path="news" element={<NewsPage />} />
        <Route path="news/:slug" element={<ArticlePage />} />
        <Route path="information" element={<InformationPage />} />
        <Route path="contact" element={<ContactPage />} />
        <Route path="404" element={<NotFoundPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Route>
      <Route element={<BareLayout />}>
        <Route path="login" element={<LoginPage />} />
      </Route>
      <Route path="about" element={<Navigate to="/get-to-know-us" replace />} />
      <Route path="blog" element={<Navigate to="/news" replace />} />
    </Routes>
  )
}

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>
);
