/* WOAH! NIGHT CLUB — Main App */
const { useState, useEffect, useRef, useCallback } = React;

/* Cache busting — uses window.v from extras.jsx (loaded first).
   To force reload of all assets after upload, bump window.ASSET_VERSION
   at the top of extras.jsx */
const v = window.v || ((path) => path);

const NAV_LINKS = [
  ['Staff','staff'],['Residents','djs'],['Events','events'],['Menu','cocktails'],
  ['VIP','vip'],['Merch','merch']
];

/* Bingo is rendered as the 7th nav slot — opens external in a new tab */
const NAV_BINGO = { label:'Bingo', url:'https://djgaiaworld.com/bingo/' };

/* Staff socials — Gaia fills these in; null = render gray placeholder */
const STAFF_SOCIALS = {
  'GAIA':     { instagram:'https://www.instagram.com/djgaia.ffxiv', twitter:null },
  'KAIEN':    { instagram:'https://www.instagram.com/kaiennanami_ffxiv/', twitter:null },
  'MYOUKI':   { instagram:null, twitter:null },
  'AEDRIC':   { instagram:'https://www.instagram.com/aedric.xiv/', twitter:null },
  'AXEL':     { instagram:'https://www.instagram.com/axelvesta_ffxiv/', twitter:null },
  'FELINA':   { instagram:'https://www.instagram.com/felina_fleur/', twitter:null },
  'FOX':      { instagram:'https://www.instagram.com/foxk.ffxiv/', twitter:null },
  'HYTSUKY':  { instagram:'https://www.instagram.com/hytsuky_xiv/', twitter:null },
  'LIVELIA':  { instagram:'https://www.instagram.com/livelia_vandern/', twitter:null },
  'MAYLIN':   { instagram:'https://www.instagram.com/maymay.ff14/', twitter:null },
  'SUGAR':    { instagram:'https://www.instagram.com/sugar.ffxiv/', twitter:null },
  'NOELLE':   { instagram:null, twitter:null },
  'MAY':      { instagram:null, twitter:null },
  'ZANE':     { instagram:null, twitter:null },
  'HEIZU':    { instagram:null, twitter:null },
  'KURO':     { instagram:null, twitter:null },
  'MIZUKI':   { instagram:null, twitter:null },
  'NOIR':     { instagram:null, twitter:null },
  'ELLANORE': { instagram:null, twitter:null },
  'AIKA':     { instagram:null, twitter:null },
  'KIM':      { instagram:null, twitter:null },
  'PEANUT':   { instagram:null, twitter:null },
  'RAVEN':    { instagram:null, twitter:null },
  'NERU':     { instagram:null, twitter:null },
  'ANGIE':    { instagram:'https://www.instagram.com/angie_ffxiv/', twitter:null },
  'MANA':     { instagram:'https://www.instagram.com/mana.heatherwind.ffxiv/', twitter:null },
};

const DJS = [
  { id:'gaia', name:'Gaia', tag:'The Anime Made DJ', jp:'ガイア',
    pills:['TECHNO','ROCK & METAL','ANIME'], status:'● RESIDENT DJ',
    bio:'50% Romanian, 50% Japanese DJ and music producer ruling the dance floors of Eorzea. Resident DJ at multiple top-tier FFXIV venues. Specializing in techno, EDM, and rock/metal — every set is a journey from melodic highs to chaotic drops. Follow on Discord, Spotify, and let\u2019s keep the party going. Woah!',
    twitch:'https://www.twitch.tv/dj_gaia', handle:'@dj_gaia', g:'#FF2E97' },
  { id:'aemilia', name:'Aemilia', tag:'The Bunterfly ✦ Full-Time Streamer', jp:'エミリア',
    pills:['HYPE STUFFS','EDM','DNB'], status:'● RESIDENT DJ',
    bio:'The Bunterfly. Full-time streamer, content creator, and one of the warmest presences in the WOAH! collective. Wherever Aemilia goes, the vibe follows — a true metamorphosis of energy, color, and heart.',
    twitch:'https://www.twitch.tv/aemilia', handle:'@aemilia', g:'#5DD3FF' },
  { id:'reyalex', name:'Rey Alex', tag:'Owner of Urban Night Club · Manager of DJ Gaia', jp:'レイ・アレックス',
    pills:['TRANCE','SYNTHWAVE','LO-FI'], status:'● RESIDENT DJ',
    badge:'👑 WOAH! MANAGEMENT', note:'Bookings via Discord: @reyalexffxiv',
    bio:'The mastermind behind the curtain. Owner of Urban Night Club and the manager who keeps DJ Gaia\u2019s career — and the entire collective — running smoothly. \u201CI do things and stuff happens.\u201D',
    twitch:'https://www.twitch.tv/reyalexxx', handle:'@reyalexxx', g:'#FF6BCB' },
  { id:'khangomon', name:'Khangomon XIV', tag:'Viet-Asia Mix · VTuber · Owner of Project XIV', jp:'カンゴモン',
    pills:['HYPE STUFFS','EDM','DNB'], status:'● RESIDENT DJ',
    bio:'Viet-Asia Mix Streamer, VTuber, occasional TCG enjoyer, and proud member of the FFXIV community. Owner of Project XIV. Every stream is a great time for everyone — Hype or Vibe, DUH!',
    twitch:'https://www.twitch.tv/khangomon', handle:'@khangomon', g:'#FF3B3B' },
  { id:'rekse', name:'Rekse Nintly', tag:'Hype Party Kitty · Owner of The Cherry Blossom', jp:'レクセ・ニントリー',
    pills:['TECHNO','EDM','TRANCE'], status:'● RESIDENT DJ',
    bio:'Hype Party Kitty. Resident DJ at The Cherry Blossom, Black Sapphire, WOAH!, End of the Line, and Sinners. Owner of The Cherry Blossom night club. Find her on Endeavor Promotions.',
    twitch:'https://www.twitch.tv/reksenintly', handle:'@reksenintly', g:'#FFB347' },
  { id:'alec', name:'Alec Marston', tag:'Dark, heavy, high-energy. Loud and made to move you.', jp:'アレック・マーストン',
    pills:['ROCK & METAL','TECHNO','EDM'], status:'● RESIDENT DJ',
    bio:'Rocking the DJ decks like he rocks his coffee — dark and questionable. Mixing dark, heavy, high-energy sounds from techno to metal. If it hits hard, it\u2019s in. Live sets full of intensity, chaos, and raw emotion.',
    twitch:'https://www.twitch.tv/alecmarston', handle:'@alecmarston', g:'#9D2EFF' },
  { id:'rin', name:'Rin Tsukii', tag:'Owner of Ignite Club · Rising in the FFXIV scene', jp:'リン・ツキ',
    pills:['HYPE STUFFS','EDM','VINTAGE'], status:'● RESIDENT DJ',
    bio:'Venturing into DJ\u2019ing in the FFXIV scene — still learning, still growing. Owner of Ignite Club. Thank you for your patience and for being part of the journey. ✨',
    twitch:'https://www.twitch.tv/rin_tsukii', handle:'@rin_tsukii', g:'#A4DD3A' },
  { id:'kiwi', name:'Kiwi', tag:'Co-Owner of Ignite Club · Former NA Raider', jp:'キウイ',
    pills:['EDM','ROCK & METAL','TRANCE'], status:'● RESIDENT DJ',
    bio:'Started out as a raider in NA. Went to \u201Cbriefly\u201D help a friend with their club, slipped, and has been stuck in the venue scene ever since. Co-Owner of Ignite Club. 🥝',
    twitch:'https://www.twitch.tv/kiwinjoyer', handle:'@kiwinjoyer', g:'#7DEB6E' },
  { id:'iris', name:'Iris Stellaris', tag:'Melodic DJ from Czechia ✨', jp:'アイリス・ステラリス',
    pills:['MELODIC TECHNO','PROGRESSIVE','HYPE STUFFS'], status:'● RESIDENT DJ',
    bio:'Melodic DJ from Czechia. Specializing in progressive, deep, and melodic house, melodic techno — with frequent voyages into trance, uplifting, hard, and mainstage. Recently exploring harder techno territory too. 🫶',
    twitch:'https://www.twitch.tv/iris_stellaris', handle:'@iris_stellaris', g:'#8B5CF6' },
];

const EVENT_NAMES = [
  { en:'NEON BLOOM',     jp:'ネオン ブルーム' },
  { en:'KITSUNE FIRE',   jp:'狐火' },
  { en:'CHROME HEARTS',  jp:'クロム・ハーツ' },
  { en:'MIDNIGHT SAKURA',jp:'夜桜' },
  { en:'ELECTRIC OKAMI', jp:'電気狼' },
  { en:'CYBER BLOSSOM',  jp:'サイバー桜' }
];
const FIRST_PARTY_DATE = '2026-05-18';
const TWO_WEEKS_MS = 14*24*60*60*1000;
function getUpcomingParties() {
  const now = new Date();
  const first = new Date(FIRST_PARTY_DATE+'T20:00:00Z');
  let cur = new Date(first), idx = 0;
  while (cur < now) { cur = new Date(cur.getTime()+TWO_WEEKS_MS); idx++; }
  const out = [];
  for (let i=0;i<3;i++) { out.push({date:new Date(cur), idx: idx+i }); cur = new Date(cur.getTime()+TWO_WEEKS_MS); }
  return out;
}

const EVENTS_LEGACY = [
  { tag:'GUEST DJ', name:'NEON BLOOM', jp:'ネオン ブルーム', date:'2026.05.16 · 22:00 EST',
    lineup:'DJ GAIA · IRIS STELLARIS · KIWI', hue:320 },
  { tag:'B2B SPECIAL', name:'KITSUNE FIRE', jp:'狐火', date:'2026.05.23 · 23:00 EST',
    lineup:'GAIA × ALEC MARSTON', hue:0 },
  { tag:'LADIES NIGHT', name:'CHROME HEARTS', jp:'クロム・ハーツ', date:'2026.05.30 · 22:00 EST',
    lineup:'AEMILIA · REKSE · RIN', hue:190 },
  { tag:'COSPLAY GACHA', name:'ULTRAVIOLET GACHA', jp:'ガチャ・ナイト', date:'2026.06.06 · 22:00 EST',
    lineup:'KHANGOMON · GAIA', hue:265 },
];

const STAFF_DIVISIONS = [
  { num:'01', name:'MANAGEMENT',         icon:'crown',    jp:'運営',      hue:50,
    members:[ {n:'GAIA', head:true, photo:v('assets/staff-gaia.png')}, {n:'KAIEN', photo:v('assets/staff-kaien.png')}, {n:'MYOUKI', photo:v('assets/staff-myouki.png')} ] },
  { num:'02', name:'PHOTOGRAPHERS',      icon:'camera',   jp:'撮影',      hue:200,
    members:[ {n:'AEDRIC', head:true, photo:v('assets/staff-aedric.png')}, {n:'AXEL', photo:v('assets/staff-axel.png')}, {n:'FELINA', photo:v('assets/staff-felina.png')}, {n:'FOX', photo:v('assets/staff-fox.png')}, {n:'HYTSUKY', photo:v('assets/staff-hytsuky.png')}, {n:'LIVELIA', photo:v('assets/staff-livelia.png')}, {n:'MAYLIN', photo:v('assets/staff-maylin.png')}, {n:'SUGAR', photo:v('assets/staff-sugar.png')} ] },
  { num:'03', name:'BARTENDERS',         icon:'wine',     jp:'バーテン',  hue:30,
    members:[ {n:'NOELLE', head:true, photo:v('assets/staff-noelle.png')}, {n:'MAY', photo:v('assets/staff-may.png')}, {n:'ZANE', photo:v('assets/staff-zane.png')} ] },
  { num:'04', name:'SECURITY / GREETER', icon:'shield',   jp:'警備',      hue:265,
    members:[ {n:'HEIZU', photo:v('assets/staff-heizu.png')}, {n:'KURO'}, {n:'MIZUKI', photo:v('assets/staff-mizuki.png')}, {n:'NOIR', photo:v('assets/staff-noir.png')} ] },
  { num:'05', name:'HOST / HOSTESS',     icon:'star',     jp:'ホステス',  hue:320,
    members:[ {n:'ELLANORE', head:true, photo:v('assets/staff-ellanore.png')}, {n:'AIKA', photo:v('assets/staff-aika.png')}, {n:'KIM'}, {n:'PEANUT', photo:v('assets/staff-peanut.png')}, {n:'RAVEN', photo:v('assets/staff-raven.png')} ] },
  { num:'06', name:'GAMBLERS',           icon:'dices',    jp:'賭博',      hue:280,
    members:[ {n:'NERU', photo:v('assets/staff-neru.png')}, {n:'KURO'}, {n:'KAIEN', photo:v('assets/staff-kaien.png')} ] },
  { num:'07', name:'PROMOTERS',          icon:'megaphone',jp:'広報',      hue:160,
    members:[ {n:'ANGIE', photo:v('assets/staff-angie.png')}, {n:'MANA', photo:v('assets/staff-mana.png')} ] },
];

const STAFF_LEGACY = [
  { name:'Mira Cole', role:'HOSTESS', jp:'ホステス', initials:'MC', hue:320 },
  { name:'Vex Lin', role:'PHOTOGRAPHER', jp:'撮影', initials:'VL', hue:190 },
  { name:'Kano Reed', role:'BARTENDER', jp:'バーテン', initials:'KR', hue:30 },
  { name:'Rey Alex', role:'MANAGER', jp:'マネージャー', initials:'RA', hue:50, mgr:true },
  { name:'Zero Vance', role:'SECURITY', jp:'警備', initials:'ZV', hue:265 },
];

const COCKTAIL_CATS = [
  { code:'01', name:'NEON COCKTAILS', sub:'signature alcoholic creations',
    drinks:[
      { code:'001', name:'WOAH! Overdrive', jp:'ウォア・オーバードライブ', ing:'vodka · grenadine · citrus', note:'intense and electric', abv:'18% ABV', price:'10,000 GIL', glass:'martini', c1:'#FF2E55', c2:'#FF8E1D', extras:['ice','spark'], featured:true, photo:v('assets/drink-001-overdrive.jpg') },
      { code:'002', name:'Chrome Desire',  jp:'クロム・デザイア',         ing:'gin · blue curaçao · tonic',     note:'cold metallic kiss', abv:'20% ABV', price:'12,000 GIL', glass:'highball', c1:'#5DD3FF', c2:'#C8C8E0', extras:['ice','bubbles'], photo:v('assets/drink-002-chrome.jpg') },
      { code:'003', name:'Midnight Upload',jp:'ミッドナイト・アップロード', ing:'black vodka · blackcurrant · lime',note:'darkness in a glass',  abv:'22% ABV', price:'14,000 GIL', glass:'coupe', c1:'#5B2D8C', c2:'#0a0a14', extras:['lime'], photo:v('assets/drink-003-midnight.jpg') },
    ]},
  { code:'02', name:'SYNTH SHOTS', sub:'short, high-impact',
    drinks:[
      { code:'004', name:'Firewall',     jp:'ファイアウォール', ing:'tequila · hot sauce · cinnamon', note:'burns like rebellion', abv:'40% ABV', price:'7,500 GIL',  glass:'shot', c1:'#FF3B3B', c2:'#FFD700', extras:['steam'], photo:v('assets/drink-004-firewall.jpg') },
      { code:'005', name:'Glitch Shot',  jp:'グリッチ・ショット', ing:'absinthe · blue layer · green layer', note:'reality breaks', abv:'38% ABV', price:'8,000 GIL', glass:'shot', c1:'#A4DD3A', c2:'#5DD3FF', extras:['glitch'], photo:v('assets/drink-005-glitch.jpg') },
      { code:'006', name:'Neural Kick',  jp:'ニューラル・キック', ing:'espresso vodka · kahlua · cream', note:'wake the system', abv:'30% ABV', price:'9,000 GIL', glass:'shot', c1:'#3D2418', c2:'#E8D4A8', extras:['ice'], photo:v('assets/drink-006-neural.jpg') },
    ]},
  { code:'03', name:'DIGITAL MOCKTAILS', sub:'premium non-alcoholic',
    drinks:[
      { code:'007', name:'Pixel Wave',  jp:'ピクセル・ウェーブ', ing:'coconut · butterfly pea · lemon', note:'colors that change', abv:'0%', price:'6,000 GIL', glass:'highball', c1:'#5DD3FF', c2:'#8B5CF6', extras:['ice','flower'], photo:v('assets/drink-007-pixelwave.jpg') },
      { code:'008', name:'Cyber Bloom', jp:'サイバー・ブルーム', ing:'rose · lychee · sparkling water', note:'floral data stream', abv:'0%', price:'6,500 GIL', glass:'coupe', c1:'#FF6BCB', c2:'#FF2E97', extras:['bubbles','flower'], photo:v('assets/drink-008-cyberbloom.jpg') },
      { code:'009', name:'Soft Reboot', jp:'ソフト・リブート', ing:'cucumber · mint · elderflower', note:'reset your night', abv:'0%', price:'5,500 GIL', glass:'martini', c1:'#A4DD3A', c2:'#E8F4E0', extras:['lime'], photo:v('assets/drink-009-softreboot.jpg') },
    ]},
  { code:'04', name:'POWA DRINKS', sub:'energy boost',
    drinks:[
      { code:'010', name:'Night Mode Energy', jp:'ナイト・モード', ing:'yuzu · taurine · ginseng', note:'unlock the dark', abv:'0%', price:'4,000 GIL', glass:'can', c1:'#0F4C81', c2:'#0a0a14', extras:['spark'], photo:v('assets/drink-010-nightmode.jpg') },
      { code:'011', name:'Electric Mate',    jp:'エレクトリック',  ing:'yerba mate · lemon · mint', note:'natural circuit', abv:'0%', price:'4,000 GIL', glass:'can', c1:'#7CD92F', c2:'#1A4A0F', extras:['spark'], photo:v('assets/drink-011-electric.jpg') },
      { code:'012', name:'Code Red Bull',    jp:'コード・レッド',  ing:'guarana · cherry · citrus', note:'max performance', abv:'0%', price:'4,500 GIL', glass:'can', c1:'#FF2E55', c2:'#FF6BCB', extras:['spark','glitch'], photo:v('assets/drink-012-codered.jpg') },
    ]},
];

const MERCH = [
  { code:'M-001', name:'WOAH! Tour Tee', price:'18,000 GIL', shape:0, hue:320, ph:'TEE' },
  { code:'M-002', name:'Kitsune Hoodie', price:'42,000 GIL', shape:0, hue:265, ph:'HOODIE' },
  { code:'M-003', name:'Eorzea Bucket Hat', price:'14,000 GIL', shape:1, hue:50, ph:'HAT' },
  { code:'M-004', name:'Holo Sticker Pack', price:'4,000 GIL', shape:0, hue:190, ph:'STICKERS' },
];

const MEMORIES = [
  { h:240, hue:320, cap:'NEON BLOOM · 2026.04', tags:['ALL','GUEST DJS'] },
  { h:300, hue:265, cap:'COSPLAY NIGHT · 2026.04', tags:['ALL','COSPLAY'] },
  { h:200, hue:190, cap:'KITSUNE FIRE · 2026.03', tags:['ALL','GUEST DJS'] },
  { h:280, hue:30, cap:'COMMUNITY GACHA · 2026.03', tags:['ALL','COMMUNITY'] },
  { h:220, hue:50, cap:'GAIA B-DAY · 2026.03', tags:['ALL','COMMUNITY'] },
  { h:260, hue:0, cap:'BLACK SAPPHIRE B2B · 2026.02', tags:['ALL','GUEST DJS'] },
  { h:200, hue:120, cap:'MASKED RAVE · 2026.02', tags:['ALL','COSPLAY'] },
  { h:320, hue:280, cap:'NIGHT 100 · 2026.01', tags:['ALL','COMMUNITY'] },
  { h:240, hue:340, cap:'HEART OF EORZEA · 2026.01', tags:['ALL','COMMUNITY'] },
  { h:260, hue:200, cap:'CHROME HEARTS · 2025.12', tags:['ALL','GUEST DJS'] },
  { h:220, hue:90, cap:'KIWI X RIN OPEN · 2025.12', tags:['ALL','GUEST DJS'] },
  { h:280, hue:230, cap:'COSPLAY CONTEST · 2025.11', tags:['ALL','COSPLAY'] },
];

/* ---------- Components ---------- */

/* Eorzea Time — Madrid time minus 2h, refreshes every minute */
const EorzeaTime = () => {
  const calc = () => {
    const madrid = new Date(new Date().toLocaleString('en-US',{timeZone:'Europe/Madrid'}));
    const t = new Date(madrid.getTime() - 2*60*60*1000);
    const h = String(t.getHours()).padStart(2,'0');
    const m = String(t.getMinutes()).padStart(2,'0');
    const days = ['SUN','MON','TUE','WED','THU','FRI','SAT'];
    // Calculate "cycle" — week of the year for added gacha flair
    const startOfYear = new Date(t.getFullYear(), 0, 1);
    const dayOfYear = Math.floor((t - startOfYear) / 86400000);
    const cycle = String(Math.floor(dayOfYear / 7) + 1).padStart(2,'0');
    return { h, m, day: days[t.getDay()], cycle };
  };
  const [t, setT] = useState(calc);
  useEffect(()=>{
    const tick = () => setT(calc());
    const ms = 60000 - (Date.now() % 60000);
    const to = setTimeout(()=>{ tick(); }, ms);
    const iv = setInterval(tick, 60000);
    return () => { clearTimeout(to); clearInterval(iv); };
  },[]);
  return (
    <div className="eorzea-clock" aria-label={`Eorzea time ${t.h}:${t.m}`}>
      {/* HUD corner brackets */}
      <span className="eorzea-clock__corner eorzea-clock__corner--tl"/>
      <span className="eorzea-clock__corner eorzea-clock__corner--tr"/>
      <span className="eorzea-clock__corner eorzea-clock__corner--bl"/>
      <span className="eorzea-clock__corner eorzea-clock__corner--br"/>
      {/* Orbital rune on the left */}
      <div className="eorzea-clock__orb" aria-hidden="true">
        <span className="eorzea-clock__orb-ring eorzea-clock__orb-ring--1"/>
        <span className="eorzea-clock__orb-ring eorzea-clock__orb-ring--2"/>
        <span className="eorzea-clock__orb-ring eorzea-clock__orb-ring--3"/>
        <span className="eorzea-clock__orb-core"/>
        <span className="eorzea-clock__orb-marker"/>
      </div>
      {/* Vertical kanji watermark */}
      <span className="eorzea-clock__kanji" aria-hidden="true">時刻</span>
      {/* Time + day info */}
      <div className="eorzea-clock__info">
        <div className="eorzea-clock__label">// EORZEA TIME</div>
        <div className="eorzea-clock__time">
          <span>{t.h}</span>
          <span className="eorzea-clock__sep">:</span>
          <span>{t.m}</span>
        </div>
        <div className="eorzea-clock__day">
          <span className="eorzea-clock__pip"/>
          {t.day} · CYCLE {t.cycle}
        </div>
      </div>
    </div>
  );
};

/* Countdown to the next bi-weekly party */
const NextNightCountdown = () => {
  const parties = (typeof getUpcomingParties === 'function') ? getUpcomingParties() : [];
  const next = parties[0];
  const event = next ? EVENT_NAMES[next.idx % EVENT_NAMES.length] : null;
  const target = next ? next.date.getTime() : null;
  const [now, setNow] = useState(Date.now());
  const [secFlick, setSecFlick] = useState(0);
  useEffect(()=>{
    const iv = setInterval(()=>{ setNow(Date.now()); setSecFlick(f=>f+1); }, 1000);
    return () => clearInterval(iv);
  },[]);
  if (!next) return null;
  const diff = target - now;
  const live = diff <= 0;
  const D = Math.max(0, Math.floor(diff/86400000));
  const H = Math.max(0, Math.floor((diff%86400000)/3600000));
  const M = Math.max(0, Math.floor((diff%3600000)/60000));
  const S = Math.max(0, Math.floor((diff%60000)/1000));
  const d = next.date;
  const ymd = `${d.getUTCFullYear()}.${String(d.getUTCMonth()+1).padStart(2,'0')}.${String(d.getUTCDate()).padStart(2,'0')}`;
  return (
    <div className="next-night">
      <div className="next-night__label">// NEXT NIGHT</div>
      {live ? (
        <div className="next-night__live">[ ★ LIVE NOW ★ ]</div>
      ) : (
        <>
          <div className="next-night__name">{event.en}</div>
          <div className="next-night__jp">{event.jp}</div>
          <div className="next-night__cd">
            <span className="cd-seg"><b>{String(D).padStart(2,'0')}</b><i>D</i></span>
            <span className="cd-sep">:</span>
            <span className="cd-seg"><b>{String(H).padStart(2,'0')}</b><i>H</i></span>
            <span className="cd-sep">:</span>
            <span className="cd-seg"><b>{String(M).padStart(2,'0')}</b><i>M</i></span>
            <span className="cd-sep">:</span>
            <span className="cd-seg cd-seg--s" key={secFlick}><b>{String(S).padStart(2,'0')}</b><i>S</i></span>
          </div>
          <div className="next-night__when">{ymd} · 16:00 ST</div>
        </>
      )}
    </div>
  );
};

/* Hero countdown — large flip-style display that counts down to the next opening.
   Uses the same getUpcomingParties() data source as NextNightCountdown. */
const HeroCountdown = () => {
  const parties = (typeof getUpcomingParties === 'function') ? getUpcomingParties() : [];
  const next = parties[0];
  const partyEvent = next ? EVENT_NAMES[next.idx % EVENT_NAMES.length] : null;
  const target = next ? next.date.getTime() : null;
  const [now, setNow] = useState(Date.now());
  useEffect(()=>{
    const iv = setInterval(()=>setNow(Date.now()), 1000);
    return () => clearInterval(iv);
  },[]);
  if (!next) return null;
  const diff = Math.max(0, target - now);
  const D = Math.floor(diff/86400000);
  const H = Math.floor((diff%86400000)/3600000);
  const M = Math.floor((diff%3600000)/60000);
  const S = Math.floor((diff%60000)/1000);
  const live = diff <= 0;
  const d = next.date;
  const ymd = `${String(d.getUTCDate()).padStart(2,'0')}.${String(d.getUTCMonth()+1).padStart(2,'0')}.${d.getUTCFullYear()}`;

  if (live) {
    return (
      <div className="hero-cd">
        <div className="hero-cd__head">
          <span className="hero-cd__label">// LIVE NOW</span>
          {partyEvent && <span className="hero-cd__event">{partyEvent.en}</span>}
        </div>
        <div className="hero-cd__live">[ ★ DOORS OPEN ★ ]</div>
      </div>
    );
  }

  return (
    <div className="hero-cd">
      <div className="hero-cd__head">
        <span className="hero-cd__label">// NEXT OPENING</span>
        {partyEvent && <span className="hero-cd__event">{partyEvent.en}</span>}
        <span className="hero-cd__date">{ymd} · 16:00 ST</span>
      </div>
      <div className="hero-cd__grid">
        <div className="hero-cd__cell">
          <span className="hero-cd__num">{String(D).padStart(2,'0')}</span>
          <span className="hero-cd__unit">DAYS</span>
        </div>
        <div className="hero-cd__cell">
          <span className="hero-cd__num">{String(H).padStart(2,'0')}</span>
          <span className="hero-cd__unit">HOURS</span>
        </div>
        <div className="hero-cd__cell">
          <span className="hero-cd__num">{String(M).padStart(2,'0')}</span>
          <span className="hero-cd__unit">MINUTES</span>
        </div>
        <div className="hero-cd__cell hero-cd__cell--seconds" key={S}>
          <span className="hero-cd__num">{String(S).padStart(2,'0')}</span>
          <span className="hero-cd__unit">SECONDS</span>
        </div>
      </div>
    </div>
  );
};

const WoahMark = ({ size = 28 }) => (
  <span className="woah-mark" style={{fontSize:size}}>
    <span className="woah-mark__text">WOAH</span><span className="woah-mark__bang">!</span>
  </span>
);

/* Real WOAH! logo — used in nav, footer, music player album art */
const WoahLogo = ({ height = 52, alt = 'WOAH! Night Club' }) => (
  <img src={v('logos/woah.png')} alt={alt}
    style={{ height, width:'auto', display:'block', objectFit:'contain' }}/>
);

const Pill = ({ children }) => <span className="pill">{children}</span>;

const AmbientParticles = () => (
  <div className="amb-particles" aria-hidden="true">
    {Array.from({length:14}).map((_,i)=>{
      const colors = ['var(--pink)','var(--cyan)','var(--violet)'];
      return <span key={i} style={{
        left:`${(i*7.3)%100}%`,
        top:`${100+ (i*11)%30}%`,
        color:colors[i%3],
        animationDuration:`${20+i*1.7}s`,
        animationDelay:`${-i*1.4}s`
      }}/>;
    })}
  </div>
);

const Nav = ({ active, navigate, onOpenMobile, soundOn, onToggleSound, pageIdx, totalPages }) => {
  const [scrolled, setScrolled] = useState(false);
  useEffect(()=>{
    const onScroll = () => setScrolled(window.scrollY > 80);
    onScroll();
    window.addEventListener('scroll', onScroll, {passive:true});
    return () => window.removeEventListener('scroll', onScroll);
  },[]);
  const go = (id) => (e) => { e.preventDefault(); navigate(id); };
  return (
    <nav className={`nav ${scrolled?'nav--scrolled':''}`}>
      <div className="nav__inner">
        <a href="#home" onClick={go('home')} className="nav__logo" aria-label="WOAH! Home">
          <div className="nav__home-pill">
            <span className="nav__home-pill-bracket nav__home-pill-bracket--l">[</span>
            <span className="nav__home-pill-stack">
              <span className="nav__home-pill-row">
                <span className="nav__home-pill-ico">★</span>
                <span className="nav__home-pill-text" data-text="HOME">HOME</span>
              </span>
              <span className="nav__home-pill-sub">CLICK TO RETURN</span>
            </span>
            <span className="nav__home-pill-bracket nav__home-pill-bracket--r">]</span>
          </div>
        </a>
        <ul className="nav__links">
          {NAV_LINKS.map(([label,id],i)=>{
            const num = String(i+1).padStart(2,'0');
            return (
            <li key={id}>
              <a href={`#${id}`} onClick={go(id)} className={active===id?'is-active':''}
                data-num={`${num}.`} data-num-active={`[${num}]`}>
                <span className="nav__triangle">▸</span>{label}
              </a>
            </li>
          );})}
          <li className="nav__sys-tag">// SYS_v2.4</li>
        </ul>
        <div className="nav__actions">
          {typeof MusicPlayer !== 'undefined' && <MusicPlayer/>}
          <a href="https://www.instagram.com/woah.nightclub/" target="_blank" rel="noopener noreferrer"
             className="nav__icon nav__icon--ig" data-tip="@woah.nightclub" aria-label="Instagram">
            <Icon name="instagram" size={18}/>
          </a>
          <a href="https://discord.gg/2UCftk2ZEB" target="_blank" rel="noopener noreferrer"
             className="nav__icon nav__icon--dc" data-tip="JOIN OUR DISCORD" aria-label="Discord">
            <Icon name="discord" size={20}/>
          </a>
          <a href="https://djgaiaworld.com/bingo/" target="_blank" rel="noopener noreferrer"
             className="nav__pill nav__pill--bingo" data-tip="[ JOIN THE BINGO NIGHT ]">
            <span className="nav__pill-sweep" aria-hidden="true"/>
            <span className="nav__pill-icon">
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round">
                <path d="M12 3 L13.8 9.5 L20.5 10.4 L15.5 14.8 L17 21 L12 17.4 L7 21 L8.5 14.8 L3.5 10.4 L10.2 9.5 Z"/>
                <circle cx="12" cy="12.5" r="1.4" fill="currentColor" stroke="none"/>
              </svg>
            </span>
            <span className="nav__pill-text">BINGO</span>
            <span className="nav__pill-live"><i/>LIVE</span>
          </a>
          <a href="https://djgaiaworld.com/Woah!-Manager/" target="_blank" rel="noopener noreferrer"
             className="nav__pill nav__pill--staff" data-tip="[ AUTHORIZED PERSONNEL ONLY ]">
            <span className="nav__pill-sweep" aria-hidden="true"/>
            <span className="nav__pill-icon">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round" strokeLinecap="round">
                <path d="M12 3 L20 6 V12 C20 16.4 16.6 19.8 12 21 C7.4 19.8 4 16.4 4 12 V6 Z"/>
                <path d="M9 12 L11 14 L15 10"/>
              </svg>
            </span>
            <span className="nav__pill-text">STAFF ACCESS</span>
            <span className="nav__pill-dot"/>
          </a>
          <button className="nav__hamburger" onClick={onOpenMobile} aria-label="Open menu">
            <Icon name="menu" size={20}/>
          </button>
        </div>
      </div>
    </nav>
  );
};

const MobileMenu = ({ open, onClose, navigate }) => {
  const go = (id) => (e) => { e.preventDefault(); onClose(); setTimeout(()=>navigate(id), 220); };
  return (
  <div className={`mobile-menu ${open?'is-open':''}`} aria-hidden={!open}>
    <div className="mobile-menu__bg" onClick={onClose}/>
    <div className="mobile-menu__content">
      <button className="modal__close" onClick={onClose} aria-label="Close menu" style={{top:'1.4rem'}}>
        <Icon name="x" size={18}/>
      </button>
      <ul>
        {NAV_LINKS.map(([label,id],i)=>(
          <li key={id} style={{animationDelay:`${i*40}ms`}}>
            <a href={`#${id}`} onClick={go(id)}>{label}</a>
          </li>
        ))}
      </ul>
      <div className="mobile-menu__socials">
        <a href="https://www.instagram.com/woah.nightclub/" target="_blank" rel="noopener noreferrer"
           className="nav__icon nav__icon--ig" aria-label="Instagram"><Icon name="instagram" size={22}/></a>
        <a href="https://discord.gg/2UCftk2ZEB" target="_blank" rel="noopener noreferrer"
           className="nav__icon nav__icon--dc" aria-label="Discord"><Icon name="discord" size={24}/></a>
      </div>
    </div>
  </div>
  );
};

const Hero = () => {
  const logoRef = useRef(null);
  const onLogoMove = (e) => {
    if (window.matchMedia('(pointer:coarse)').matches) return;
    const el = logoRef.current; if(!el) return;
    const r = el.getBoundingClientRect();
    const x = (e.clientX - r.left)/r.width - .5;
    const y = (e.clientY - r.top)/r.height - .5;
    el.style.setProperty('--lry', `${x*14}deg`);
    el.style.setProperty('--lrx', `${-y*14}deg`);
  };
  const onLogoLeave = () => {
    const el = logoRef.current; if(!el) return;
    el.style.setProperty('--lry','0deg');
    el.style.setProperty('--lrx','0deg');
  };
  const particles = Array.from({length:18}).map((_,i)=>{
    const colors = ['#FF2E97','#00F0FF','#8B5CF6'];
    return { left:`${(i*5.7)%100}%`, color:colors[i%3], dur:14+(i%5)*2, delay:-(i*0.9) };
  });

  // Get next party for the editorial countdown card
  const upcoming = (typeof getUpcomingParties === 'function') ? getUpcomingParties() : [];
  const nextParty = upcoming[0];
  const nextEvent = nextParty ? EVENT_NAMES[nextParty.idx % EVENT_NAMES.length] : null;
  const nextDateStr = nextParty ? `${String(nextParty.date.getUTCDate()).padStart(2,'0')}.${String(nextParty.date.getUTCMonth()+1).padStart(2,'0')}.${nextParty.date.getUTCFullYear()}` : '';

  return (
    <section className="hero" id="hero" data-screen-label="Hero">
      {/* Atmospheric layered background */}
      <div className="hero__bg" aria-hidden="true">
        <div className="hero__bg-layer hero__bg--1"/>
        <div className="hero__silhouette"/>
        <div className="hero__particles">
          {particles.map((p,i)=>(<span key={i} className="particle" style={{
            left:p.left, bottom:'-10px', color:p.color,
            animationDuration:`${p.dur}s`, animationDelay:`${p.delay}s`
          }}/>))}
        </div>
        <div className="hero__bg-layer hero__bg--scan"/>
        <div className="hero__bg-layer hero__bg--vignette"/>
      </div>

      {/* Floating WOAH! logo — sits next to the title with chrome/holo glitch effect */}
      <div className="hero__floating-logo" ref={logoRef}
           onMouseMove={onLogoMove} onMouseLeave={onLogoLeave}>
        <span className="hero__logo-halo"/>
        <img src={v('logos/woah.png')} alt="" className="hero__logo-img hero__logo-img--new"/>
        <img src={v('logos/woah-original.png')} alt="" className="hero__logo-img hero__logo-img--old"/>
        <span className="hero__logo-glitch" aria-hidden="true"/>
      </div>

      {/* Editorial side rail — left vertical strip */}
      <aside className="hero__rail" aria-hidden="true">
        <div className="hero__rail-top">
          <span className="hero__rail-num">N° 01</span>
          <span className="hero__rail-line"/>
          <span className="hero__rail-jp">夜の物語</span>
        </div>
        <div className="hero__rail-bottom">
          <span className="hero__rail-vol">VOL.III · 2026</span>
        </div>
      </aside>

      {/* Top-right HUD with time */}
      <div className="hero__hud-tr">
        <div className="hud-stack">
          <EorzeaTime/>
        </div>
      </div>

      {/* Main hero — clean 2-column layout */}
      <div className="hero__content">
        {/* LEFT column: all the textual content */}
        <div className="hero__left">
          <div className="hero__meta">
            <span className="hero__live">
              <span className="live-dot"/>
              <span className="hero__meta-loc">LIGHT · ALPHA · MIST W20 P35</span>
            </span>
          </div>

          <div className="hero__eyebrow">
            <span>THE ANIME MADE DJ</span>
            <span className="hero__eyebrow-dot"/>
            <span>SINCE 2023</span>
          </div>

          <h1 className="hero__title">
            <span className="hero__woah">WOAH<span className="hero__woah-bang">!</span></span>
            <span className="hero__sub">
              <span className="hero__sub-mark">[</span>
              <span>NIGHT CLUB</span>
              <span className="hero__sub-mark">]</span>
            </span>
          </h1>
          <div className="hero__jp">ウォア・ナイトクラブ</div>

          <p className="hero__dek">
            DJ Gaia's underground sanctuary inside Final Fantasy XIV. Bi-weekly resident sets,
            live producer drops, kawaii cyberpunk visuals and a roster of <strong>28 operatives</strong>
            across seven divisions. Built for friends, designed for the night.
          </p>

          <div className="hero__action-row">
            <a href="#vip" className="btn btn--pink">
              Book VIP Table <Icon name="arrow" size={14}/>
            </a>
            <a href="https://discord.gg/2UCftk2ZEB" target="_blank" rel="noopener noreferrer"
               className="btn btn--ghost-cyan">
              Join Discord <Icon name="discord" size={14}/>
            </a>
          </div>

          <HeroCountdown/>
        </div>

        {/* RIGHT column: video player */}
        <div className="hero__right">
          <div className="hero__video-wrap">
            {typeof VideoPlayer !== 'undefined' && <VideoPlayer/>}
          </div>
        </div>
      </div>

      {/* Scroll indicator */}
      <div className="hero__hud-br">
        <div className="scroll-ind">
          <span>SCROLL</span>
          <div className="scroll-ind__line"/>
          <span style={{fontFamily:'Noto Sans JP, sans-serif'}}>下へ</span>
        </div>
      </div>
    </section>
  );
};

const SectionHead = ({ id, title, jp, lead, leadEl }) => (
  <header className="section__head section__head--side">
    {/* Vertical floating title — sits on the left edge */}
    <aside className="section__head-aside" aria-hidden="true">
      <span className="section__head-num">{id}</span>
      <span className="section__head-line"/>
      <span className="section__head-title-vert">{title}</span>
      <span className="section__head-jp-vert">{jp}</span>
    </aside>

    {/* Compact horizontal bar with meta + lead */}
    <div className="section__head-bar">
      <span className="hud-label">// SECTION {id}</span>
      <h2 className="section__title gradient-title section__head-title-h">{title}</h2>
      <span className="section__jp section__head-jp-h">{jp}</span>
      {leadEl ? (
        <div className="section__head-lead-wrap">{leadEl}</div>
      ) : (lead && <p className="section__lead section__head-lead-wrap">{lead}</p>)}
    </div>
  </header>
);

const Events = () => {
  const parties = getUpcomingParties();
  const [now, setNow] = useState(Date.now());
  useEffect(()=>{ const t=setInterval(()=>setNow(Date.now()),1000); return ()=>clearInterval(t); },[]);
  const cards = parties.map((p,i)=>{
    const nm = EVENT_NAMES[p.idx % EVENT_NAMES.length];
    const d = p.date;
    const ymd = `${d.getUTCFullYear()}.${String(d.getUTCMonth()+1).padStart(2,'0')}.${String(d.getUTCDate()).padStart(2,'0')}`;
    return { ...nm, date: d, ymd, idx:i, hue:[320,275,200][i] };
  });
  const next = cards[0]?.date;
  const fmtCD = (d) => {
    if (!d) return '';
    const ms = d.getTime() - now; if (ms<=0) return 'D-00 · 00:00:00';
    const days = Math.floor(ms/86400000);
    const h = Math.floor((ms%86400000)/3600000);
    const m = Math.floor((ms%3600000)/60000);
    const s = Math.floor((ms%60000)/1000);
    return `D-${String(days).padStart(2,'0')} · ${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
  };
  return (
    <section className="section section--events" id="events" data-screen-label="Events">
      <SectionHead id="03" title="Upcoming Nights" jp="イベント"
        leadEl={<div>
          <span className="bracket">[ NEXT 3 NIGHTS · BI-WEEKLY · MONDAYS ]</span>
          <p className="section__lead" style={{marginTop:'.8rem'}}>Each banner is a different chapter. Bring your crew. Bring your fit.</p>
        </div>}
      />
      <div className="events-grid">
        {cards.map((e,i)=>(
          <article key={i} className={`event-card event-card--bg ${i===0?'event-card--next':''}`} style={{'--hue':e.hue}}>
            <div className="event-card__bg"/>
            <div className="event-card__overlay"/>
            <div className="event-card__bignum">{String(i+1).padStart(2,'0')}</div>
            {i===0 && <div className="event-card__nextbadge">★ NEXT NIGHT</div>}
            {i===0 && <div className="event-card__cd">{fmtCD(next)}</div>}
            <div className="event-card__body">
              <div className="event-card__tag">OFFICIAL NIGHT // 0{i+1}</div>
              <div className="event-card__name">{e.en}</div>
              <div className="event-card__jp">{e.jp}</div>
              <div className="event-card__date">
                <Icon name="calendar" size={13}/> <span>{e.ymd} · 16:00 ST</span>
              </div>
              <div className="event-card__genres">
                <Icon name="music" size={12}/>
                <span>EDM</span><i>·</i><span>TECHNO</span><i>·</i><span>ROCK/METAL</span><i>·</i><span>ANIME</span>
              </div>
              <a href="#vip" className="event-card__cta">RESERVE <Icon name="arrow" size={12}/></a>
            </div>
          </article>
        ))}
      </div>

    </section>
  );
};

/* Staff card socials — gray placeholder unless STAFF_SOCIALS has a link */
const StaffSocials = ({ name }) => {
  const s = STAFF_SOCIALS[name] || { instagram:null, twitter:null };
  const Btn = ({ kind, url }) => {
    const enabled = !!url;
    const tip = enabled ? (kind==='instagram'?(url.match(/instagram\.com\/([^\/?#]+)/)?.[1] && '@'+url.match(/instagram\.com\/([^\/?#]+)/)[1]) : (url.match(/(?:twitter|x)\.com\/([^\/?#]+)/)?.[1] && '@'+url.match(/(?:twitter|x)\.com\/([^\/?#]+)/)[1])) : null;
    return enabled ? (
      <a className={`staff-soc staff-soc--${kind}`} href={url} target="_blank" rel="noopener noreferrer"
         data-tip={tip||''} aria-label={kind} onClick={e=>e.stopPropagation()}>
        <Icon name={kind} size={12}/>
      </a>
    ) : (
      <span className={`staff-soc staff-soc--${kind} is-disabled`} aria-hidden="true">
        <Icon name={kind} size={12}/>
      </span>
    );
  };
  return (
    <div className="staff-card__socials">
      <Btn kind="instagram" url={s.instagram}/>
      <Btn kind="twitter" url={s.twitter}/>
    </div>
  );
};

/* DJ photo path convention: assets/dj-{id}.png. If file missing, the onError handler
   hides the img and the placeholder layer (logo + gradient + scanlines) shows. */
const djPhotoPath = (id) => v(`assets/dj-${id}.png`);

/* DJ card — gacha-banner style. Photo cover, identity logo small, info at bottom. */
const DjCard = ({ dj, featured, onOpen }) => {
  const ref = useRef(null);
  const [photoOk, setPhotoOk] = useState(true);
  const onMove = e => {
    if (window.matchMedia('(pointer:coarse)').matches) return;
    const r = ref.current.getBoundingClientRect();
    const x = (e.clientX - r.left)/r.width - .5;
    const y = (e.clientY - r.top)/r.height - .5;
    ref.current.style.setProperty('--ry', `${x*5}deg`);
    ref.current.style.setProperty('--rx', `${-y*5}deg`);
  };
  const onLeave = () => {
    ref.current.style.setProperty('--ry','0deg');
    ref.current.style.setProperty('--rx','0deg');
  };
  return (
    <article ref={ref}
      className={`dj-card ${featured?'dj-card--featured':''} ${photoOk?'dj-card--has-photo':'dj-card--placeholder'}`}
      style={{'--g':dj.g}}
      onMouseMove={onMove} onMouseLeave={onLeave}
      onClick={()=>onOpen(dj)}
      tabIndex={0}
      onKeyDown={e=>{ if(e.key==='Enter') onOpen(dj); }}>
      {/* Photo (bottom layer) — covers whole card. onError → switch to placeholder mode */}
      <img
        className="dj-card__photo"
        src={djPhotoPath(dj.id)}
        alt={dj.name}
        loading="lazy"
        onError={()=>setPhotoOk(false)}
      />
      {/* Placeholder layer — visible only when there's no photo: gradient bg + big logo */}
      <div className="dj-card__placeholder-bg" aria-hidden="true"/>
      <div className="dj-card__placeholder-logo" aria-hidden="true">
        <DjLogo id={dj.id} size={130}/>
      </div>
      {/* Atmospheric overlays */}
      <div className="dj-card__halo"/>
      <div className="dj-card__scan-overlay"/>
      <div className="dj-card__vignette"/>
      <div className="dj-card__shimmer"/>
      {/* HUD corner brackets — same color as DJ */}
      <div className="dj-card__corner dj-card__corner--tl"/>
      <div className="dj-card__corner dj-card__corner--tr"/>
      <div className="dj-card__corner dj-card__corner--bl"/>
      <div className="dj-card__corner dj-card__corner--br"/>
      {/* Top-left small logo identity badge (always visible — distinct from placeholder big logo) */}
      <div className="dj-card__id-logo" aria-hidden="true">
        <DjLogo id={dj.id} size={34}/>
      </div>
      {/* Status pill top-right */}
      <div className="dj-card__status-pill">
        <span className="dj-card__status-dot"/>
        <span>RESIDENT</span>
      </div>
      {/* Bottom info block — sits over photo with gradient */}
      <div className="dj-card__info">
        <div className="dj-card__pills">{dj.pills.map(p=><Pill key={p}>{p}</Pill>)}</div>
        <h3 className="dj-card__name">{dj.name.toUpperCase()}</h3>
        <div className="dj-card__jp">{dj.jp}</div>
        <div className="dj-card__tag">{featured ? `"${dj.tag}"` : dj.tag}</div>
        {featured && dj.origin && <div className="dj-card__badge">{dj.origin}</div>}
        {featured && dj.badge && <div className="dj-card__badge dj-card__badge--gold">{dj.badge}</div>}
      </div>
      {/* Hover CTA */}
      <div className="dj-card__cta">
        <span>VIEW PROFILE</span>
        <Icon name="arrow" size={11}/>
      </div>
    </article>
  );
};

const DjModal = ({ dj, onClose }) => {
  const [photoFail, setPhotoFail] = useState(false);
  useEffect(()=>{ setPhotoFail(false); },[dj?.id]);
  useEffect(()=>{
    if (dj) playCardOpen();
  },[dj?.id]);
  useEffect(()=>{
    if(!dj) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = e => { if(e.key==='Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => { document.body.style.overflow = prev; window.removeEventListener('keydown', onKey); };
  },[dj, onClose]);
  if(!dj) return null;
  return ReactDOM.createPortal(
    <div className="modal" onClick={onClose} role="dialog" aria-modal="true">
      <div className="modal__panel" style={{'--g':dj.g}} onClick={e=>e.stopPropagation()}>
        {/* Photo background — blurred, with cinematic slow zoom */}
        {!photoFail && (
          <img
            className="modal__bg-photo"
            src={djPhotoPath(dj.id)}
            alt=""
            aria-hidden="true"
            onError={()=>setPhotoFail(true)}
          />
        )}
        {/* Color vignette using DJ's color */}
        <div className="modal__bg-vignette" aria-hidden="true"/>
        {/* Atmospheric scanlines */}
        <div className="modal__bg-scan" aria-hidden="true"/>
        {/* Content (sits on top of photo layers) */}
        <div className="modal__content">
          <button className="modal__close" onClick={onClose} aria-label="Close"><Icon name="x" size={16}/></button>
          <div className="modal__logo"><DjLogo id={dj.id} size={180}/></div>
          <div className="modal__status">{dj.status}</div>
          <h3 className="modal__name">{dj.name.toUpperCase()}</h3>
          <div className="modal__jp">{dj.jp}</div>
          <div className="modal__tag">"{dj.tag}"</div>
          {dj.origin && <span className="modal__badge modal__badge--origin">{dj.origin}</span>}
          {dj.badge && <span className="modal__badge">{dj.badge}</span>}
          <div className="modal__pills">{dj.pills.map(p=><Pill key={p}>{p}</Pill>)}</div>
          <p className="modal__bio">{dj.bio}</p>
          {dj.note && <div className="modal__note">{dj.note}</div>}
          <div>
            <a href={dj.twitch} target="_blank" rel="noopener noreferrer" className="btn btn--twitch btn--xl">
              <Icon name="twitch" size={16}/> Watch on Twitch <Icon name="arrow" size={14}/>
            </a>
          </div>
          <div className="modal__handle">// {dj.handle} · TWITCH</div>
        </div>
      </div>
    </div>,
    document.body
  );
};

const DJs = () => {
  const [open, setOpen] = useState(null);
  return (
    <section className="section" id="djs" data-screen-label="Residents">
      <SectionHead id="02" title="Residents" jp="レジデント DJ"
        leadEl={<div>
          <span className="bracket">[ THE WOAH! COLLECTIVE — {DJS.length} ARTISTS ]</span>
          <p className="section__lead" style={{marginTop:'.8rem'}}>The voices behind every drop. The hands that move the night.</p>
        </div>}
      />
      <div className="djs-grid">
        {DJS.map(dj=><DjCard key={dj.id} dj={dj} onOpen={setOpen}/>)}
      </div>
      <DjModal dj={open} onClose={()=>setOpen(null)}/>
    </section>
  );
};

const StaffModal = ({ member, division, onClose }) => {
  useEffect(()=>{
    if (member) playCardOpen();
  },[member?.n]);
  useEffect(()=>{
    if(!member) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = e => { if(e.key==='Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => { document.body.style.overflow = prev; window.removeEventListener('keydown', onKey); };
  },[member, onClose]);
  if(!member) return null;
  const role = (member.role || division.name.split(' / ')[0]).toUpperCase();
  const bio = member.bio || 'Bio coming soon — this operative is being briefed for the WOAH! files.';
  return ReactDOM.createPortal(
    <div className="modal modal--staff" onClick={onClose} role="dialog" aria-modal="true">
      <div className="staff-modal-b" style={{'--hue':division.hue}} onClick={e=>e.stopPropagation()}>
        {/* Close button */}
        <button className="modal__close staff-modal-b__close" onClick={onClose} aria-label="Close"><Icon name="x" size={16}/></button>
        {/* LEFT SIDE — info */}
        <div className="staff-modal-b__info">
          <div className="staff-modal-b__division">
            <Icon name={division.icon} size={11}/>
            <span>{division.name}</span>
          </div>
          {member.head && <div className="staff-modal-b__headbadge">★ HEAD OF DIVISION</div>}
          <div className="staff-modal-b__role">{role}</div>
          <h3 className="staff-modal-b__name">{member.n.toUpperCase()}</h3>
          <div className="staff-modal-b__divider"/>
          <div className="staff-modal-b__about-label">// ABOUT ME</div>
          <p className="staff-modal-b__about">{bio}</p>
        </div>
        {/* RIGHT SIDE — photo frame */}
        <div className="staff-modal-b__photo-frame">
          {member.photo && (
            <img className="staff-modal-b__photo" src={member.photo} alt={member.n}/>
          )}
          {!member.photo && (
            <div className="staff-modal-b__photo-placeholder" aria-hidden="true">
              <span>[ NO PHOTO ]</span>
            </div>
          )}
          {/* CRT scanlines overlay */}
          <div className="staff-modal-b__photo-scan" aria-hidden="true"/>
          {/* HUD corners on the photo frame */}
          <div className="staff-modal-b__photo-corner staff-modal-b__photo-corner--tl"/>
          <div className="staff-modal-b__photo-corner staff-modal-b__photo-corner--tr"/>
          <div className="staff-modal-b__photo-corner staff-modal-b__photo-corner--bl"/>
          <div className="staff-modal-b__photo-corner staff-modal-b__photo-corner--br"/>
        </div>
      </div>
    </div>,
    document.body
  );
};

const Staff = () => {
  const [open, setOpen] = useState(null);
  const totalOps = STAFF_DIVISIONS.reduce((s,d)=>s+d.members.length,0);
  let runningIdx = 0;
  return (
  <section className="section section--staff" id="staff" data-screen-label="Staff">
    <div className="staff__bg" aria-hidden="true">
      <svg className="blueprint" viewBox="0 0 1600 1000" preserveAspectRatio="none">
        <defs>
          <pattern id="staffbp" width="60" height="60" patternUnits="userSpaceOnUse">
            <path d="M0 0H60V60" fill="none" stroke="rgba(0,240,255,.06)" strokeWidth=".5"/>
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#staffbp)"/>
        <circle cx="200" cy="500" r="160" fill="none" stroke="rgba(0,240,255,.08)" strokeDasharray="4 6"/>
        <circle cx="1400" cy="700" r="220" fill="none" stroke="rgba(255,46,151,.06)" strokeDasharray="2 8"/>
      </svg>
    </div>
    <SectionHead id="01" title="The Crew" jp="スタッフ"
      leadEl={<div>
        <span className="bracket">[ ROSTER 2026 · {STAFF_DIVISIONS.length} DIVISIONS · {String(totalOps).padStart(2,'0')} OPERATIVES ]</span>
        <p className="section__lead" style={{marginTop:'.8rem'}}>The team that brings every WOAH! night to life.</p>
      </div>}
    />
    <div className="staff-mosaic">
      {STAFF_DIVISIONS.map((div,di)=>(
        <React.Fragment key={div.num}>
          {/* Divider de división — ocupa toda la fila del grid */}
          <div className="staff-divider" style={{'--hue':div.hue, animationDelay:`${di*120}ms`}}>
            <div className="staff-divider__num">{div.num}</div>
            <div className="staff-divider__rule"/>
            <div className="staff-divider__title">
              <span className="staff-divider__icon"><Icon name={div.icon} size={18}/></span>
              <span className="staff-divider__name">{div.name}</span>
              <span className="staff-divider__jp">{div.jp}</span>
            </div>
            <div className="staff-divider__count">[ {String(div.members.length).padStart(2,'0')} ]</div>
          </div>
          {/* Cards de la división */}
          {div.members.map((m,mi)=>{
            const idx = runningIdx++;
            return (
              <article key={`${div.num}-${mi}`}
                className={`staff-card ${m.head?'staff-card--head':''} ${m.photo?'staff-card--has-photo':''}`}
                style={{'--hue':div.hue, animationDelay:`${idx*30}ms`}}
                tabIndex={0}
                role="button"
                aria-label={`Open profile of ${m.n}`}
                onClick={()=>setOpen({member:m, division:div})}
                onKeyDown={e=>{ if(e.key==='Enter') setOpen({member:m, division:div}); }}>
                {m.head && <div className="staff-card__headbadge">★ HEAD</div>}
                <div className="staff-card__roletag" aria-hidden="true">
                  <Icon name={div.icon} size={11}/>
                  <span>{div.name.split(' / ')[0]}</span>
                </div>
                {m.photo && <img src={m.photo} alt={m.n} className="staff-card__photo" loading="lazy"/>}
                {m.photo && <span className="staff-card__scan-overlay"/>}
                {!m.photo && (
                  <div className="staff-card__portrait">
                    <span className="staff-card__scan"/>
                    <span className="staff-card__nophoto">[ NO PHOTO ]</span>
                  </div>
                )}
                <div className="staff-card__name">{m.n}</div>
                <div className="staff-card__role">{div.name.split(' / ')[0]}</div>
                <StaffSocials name={m.n}/>
                <span className="staff-card__edge"/>
              </article>
            );
          })}
        </React.Fragment>
      ))}
    </div>
    <StaffModal member={open?.member} division={open?.division} onClose={()=>setOpen(null)}/>
  </section>
  );
};

const Cocktails = () => (
  <section className="section section--cocktails" id="cocktails" data-screen-label="Cocktails">
    <div className="section--cocktails__bg" aria-hidden="true">
      <svg className="blueprint" viewBox="0 0 1600 1000" preserveAspectRatio="none">
        <defs>
          <pattern id="bp" width="60" height="60" patternUnits="userSpaceOnUse">
            <path d="M0 0H60V60" fill="none" stroke="rgba(0,240,255,.06)" strokeWidth=".5"/>
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill="url(#bp)"/>
        <text x="1300" y="120" fontFamily="Noto Sans JP" fontSize="80" fill="rgba(0,240,255,.04)">カクテル</text>
        <circle cx="200" cy="500" r="120" fill="none" stroke="rgba(0,240,255,.1)" strokeDasharray="4 6"/>
        <circle cx="200" cy="500" r="180" fill="none" stroke="rgba(255,46,151,.07)" strokeDasharray="2 8"/>
        <line x1="0" y1="500" x2="400" y2="500" stroke="rgba(0,240,255,.1)" strokeDasharray="2 4"/>
        <line x1="200" y1="280" x2="200" y2="720" stroke="rgba(0,240,255,.1)" strokeDasharray="2 4"/>
      </svg>
    </div>
    <SectionHead id="04" title="Drink the Future" jp="ウォア・カクテル"
      lead="Twelve signature formulas. Four categories. Each glass is a different runtime."
    />

    {COCKTAIL_CATS.map(cat=>(
      <div className="cocktail-cat" key={cat.code}>
        <div className="cocktail-cat__head">
          <span className="bracket">[ {cat.name} ]</span>
          <span className="hud-label" style={{color:'var(--text2)'}}>// {cat.code} · {cat.sub}</span>
          <div className="cocktail-cat__line"/>
        </div>
        <div className="cocktail-grid">
          {cat.drinks.map(d=>(
            <article key={d.code} className={`cocktail-card ${d.photo?'cocktail-card--has-photo':''}`} style={{'--c1':d.c1,'--c2':d.c2}}>
              <div className="cocktail-card__visual">
                <div className="cocktail-card__corner cocktail-card__corner--tl"/>
                <div className="cocktail-card__corner cocktail-card__corner--br"/>
                <div className="cocktail-card__code">// {d.code}</div>
                {d.featured && <div className="cocktail-card__ribbon">★ HOUSE SIGNATURE</div>}
                {d.photo
                  ? <img src={d.photo} alt={d.name} className="cocktail-card__photo" loading="lazy"/>
                  : <Cocktail kind={d.code} c1={d.c1} c2={d.c2} glass={d.glass} extras={d.extras}/>
                }
                <div className="cocktail-card__shimmer"/>
              </div>
              <div className="cocktail-card__body">
                <h4 className="cocktail-card__name">{d.name}</h4>
                <div className="cocktail-card__jp">{d.jp}</div>
                <div className="cocktail-card__ing">{d.ing}</div>
                <div className="cocktail-card__note">"{d.note}"</div>
                <div className="cocktail-card__foot">
                  <span className={`cocktail-card__abv ${d.abv==='0%'?'is-zero':''}`}>{d.abv}</span>
                  <span className="cocktail-card__price">{d.price}</span>
                </div>
              </div>
            </article>
          ))}
        </div>
      </div>
    ))}
  </section>
);

const Merch = () => (
  <section className="section section--merch-soon" id="merch" data-screen-label="Merch">
    <SectionHead id="06" title="Wear the Vibe" jp="グッズ"
      lead="Limited drops. Cut-and-sewn in Eorzea. Coming soon to the WOAH! storefront."
    />
    <div className="merch-soon">
      <div className="merch-soon__hud" aria-hidden="true">
        <span className="merch-soon__hud-dot"/>
        <span className="merch-soon__hud-line"/>
        <span>// STATUS · IN PRODUCTION</span>
      </div>
      <div className="merch-soon__icon" aria-hidden="true">
        <Icon name="package" size={72}/>
      </div>
      <h3 className="merch-soon__title">COMING SOON</h3>
      <div className="merch-soon__jp">準備中</div>
      <p className="merch-soon__copy">
        Our merch line is being crafted as we speak. T-shirts, hoodies, caps and exclusive WOAH! drops
        are on their way. Stay tuned — the first collection drops on a full-moon night.
      </p>
      <div className="merch-soon__stats">
        <div className="merch-soon__stat">
          <div className="merch-soon__stat-label">// COLLECTION</div>
          <div className="merch-soon__stat-value">DROP 01</div>
        </div>
        <div className="merch-soon__stat">
          <div className="merch-soon__stat-label">// STATUS</div>
          <div className="merch-soon__stat-value">PRODUCING</div>
        </div>
        <div className="merch-soon__stat">
          <div className="merch-soon__stat-label">// LAUNCH</div>
          <div className="merch-soon__stat-value">TBA</div>
        </div>
      </div>
      <a href="https://discord.gg/2UCftk2ZEB" target="_blank" rel="noopener noreferrer"
         className="btn btn--ghost-cyan btn--xl merch-soon__cta">
        <Icon name="bell" size={14}/> NOTIFY ME ON DISCORD <Icon name="arrow" size={13}/>
      </a>
    </div>
  </section>
);

const VIP_TIERS = [
  { id:'1night',  name:'1 NIGHT',     sub:'Single Evening Pass',  jp:'夜',
    price:'1,000,000', priceUnit:'GIL', cadence:'one night',
    color:'#00F0FF', kanjiColor:'rgba(0,240,255,.85)',
    bg:v('assets/vip-bg-1night.jpg'),
    tag:'Best for trying it out.',
    perksExtra:[] },
  { id:'1month', name:'1 MONTH',     sub:'30 Days of Premium',  jp:'月',
    price:'1,500,000', priceUnit:'GIL', cadence:'per month',
    color:'#FF2E97', kanjiColor:'rgba(255,46,151,.85)',
    bg:v('assets/vip-bg-1month.jpg'),
    tag:'Most chosen by regulars.',
    popular:true,
    perksExtra:[] },
  { id:'3months', name:'3 MONTHS',    sub:'Quarterly Excellence',  jp:'三',
    price:'2,000,000', priceUnit:'GIL', cadence:'per quarter',
    color:'#8B5CF6', kanjiColor:'rgba(139,92,246,.85)',
    bg:v('assets/vip-bg-3months.jpg'),
    tag:'Best value per night.',
    perksExtra:['Priority booking on themed nights'] },
  { id:'lifetime', name:'LIFETIME',   sub:'Forever Elite',  jp:'永',
    price:'5,000,000', priceUnit:'GIL', cadence:'one-time payment',
    color:'#FFD700', kanjiColor:'rgba(255,215,0,.9)',
    bg:v('assets/vip-bg-lifetime.jpg'),
    tag:'The ultimate flex.',
    lifetime:true,
    perksExtra:['Custom Discord title','Name on Hall of Fame','Lifetime priority access'] },
];

const VIP_PERKS_BASE = [
  ['zap','SKIP THE LINE','No more waiting. VIPs get priority entry to the club.'],
  ['wine','2 FREE DRINKS','Each night, grab two complimentary drinks at the bar.'],
  ['sparkles','EXCLUSIVE DISCORD ROLE','Get recognized as a VIP in our community.'],
  ['heart','SUPPORT THE CLUB','Your membership helps keep the party alive and the clubs running.'],
];

const VIP = () => {
  const [tierIdx, setTierIdx] = useState(1); // start on "1 MONTH" (popular)
  const tier = VIP_TIERS[tierIdx];
  // Generate a fake member number stable per tier so it doesn't jitter on rerender
  const memberNum = React.useMemo(()=>{
    const seed = (tier.id+'WOAH').split('').reduce((a,c)=>a+c.charCodeAt(0),0);
    const seg = (n)=>((seed*n)%9000+1000).toString();
    return `${seg(7)} ${seg(13)} ${seg(19)} ${seg(23)}`;
  },[tier.id]);

  return (
  <section className="section section--vip" id="vip" data-screen-label="VIP Membership">
    <div className="section--vip__bg" aria-hidden="true">
      <div className="section--vip__radial" style={{background:`radial-gradient(ellipse at 50% 30%, ${tier.color}22 0%, transparent 60%)`}}/>
      <div className="section--vip__watermark">VIP</div>
      <div className="section--vip__particles">
        {Array.from({length:18}).map((_,i)=>(
          <span key={i} style={{
            left:`${(i*5.5)%100}%`,
            animationDuration:`${22+(i%6)*2}s`,
            animationDelay:`${-i*1.3}s`
          }}/>
        ))}
      </div>
    </div>

    <header className="section__head section__head--side section__head--gold">
      <aside className="section__head-aside" aria-hidden="true">
        <span className="section__head-num">05</span>
        <span className="section__head-line"/>
        <span className="section__head-title-vert">VIP Membership</span>
        <span className="section__head-jp-vert">VIP メンバーシップ</span>
      </aside>
      <div className="section__head-bar">
        <span className="hud-label" style={{color:'var(--gold)', borderColor:'rgba(255,215,0,.3)', background:'rgba(255,215,0,.06)'}}>// SECTION 05</span>
        <h2 className="section__title gold-title section__head-title-h">VIP Membership</h2>
        <span className="section__jp section__head-jp-h" style={{color:'var(--gold)'}}>VIP メンバーシップ</span>
        <p className="section__lead section__head-lead-wrap">Elevate your club experience with premium perks. Choose your tier and ascend.</p>
      </div>
    </header>

    {/* THE CARD — interactive tier switcher */}
    <div className="vip-stage">
      <div className="vip-stage__top-tag">
        <span className="bracket" style={{color:'var(--gold)',borderColor:'rgba(255,215,0,.3)'}}>[ SELECT YOUR TIER ]</span>
        <span className="vip-stage__tag-sub">// 4 membership levels available</span>
      </div>

      {/* Tier tabs */}
      <div className="vip-tabs" role="tablist" aria-label="VIP tier selector">
        {VIP_TIERS.map((t,i)=>(
          <button key={t.id}
            role="tab"
            aria-selected={i===tierIdx}
            className={`vip-tab ${i===tierIdx?'is-active':''} ${t.lifetime?'vip-tab--life':''}`}
            style={{'--c':t.color}}
            onClick={()=>setTierIdx(i)}>
            {t.popular && <span className="vip-tab__ribbon">★ POPULAR</span>}
            {t.lifetime && <span className="vip-tab__ribbon vip-tab__ribbon--life">♛ ELITE</span>}
            <span className="vip-tab__name">{t.name}</span>
            <span className="vip-tab__price">{t.price} <em>{t.priceUnit}</em></span>
          </button>
        ))}
      </div>

      {/* The MEMBERSHIP CARD */}
      <div className="vip-card-wrap">
        <article key={tier.id} className={`vip-card vip-card--${tier.id}`} style={{'--c':tier.color}}>
          {/* Background image */}
          <img className="vip-card__bg" src={tier.bg} alt="" aria-hidden="true"/>
          {/* Rotating orbital ring — gives the feel of the focal element spinning */}
          <div className="vip-card__orbit" aria-hidden="true">
            <span className="vip-card__orbit-ring vip-card__orbit-ring--1"/>
            <span className="vip-card__orbit-ring vip-card__orbit-ring--2"/>
            <span className="vip-card__orbit-ring vip-card__orbit-ring--3"/>
          </div>
          {/* Floating particles in tier color */}
          <div className="vip-card__particles" aria-hidden="true">
            {Array.from({length:14}).map((_,i)=>(
              <span key={i} style={{
                left:`${(i*7.3)%100}%`,
                animationDuration:`${10+(i%5)*1.4}s`,
                animationDelay:`${-i*0.7}s`,
                width:`${2+(i%3)}px`,
                height:`${2+(i%3)}px`,
              }}/>
            ))}
          </div>
          {/* Drifting sakura petals */}
          <div className="vip-card__petals" aria-hidden="true">
            {Array.from({length:6}).map((_,i)=>(
              <span key={i} style={{
                left:`${(i*17+5)%90}%`,
                animationDuration:`${14+(i%4)*2}s`,
                animationDelay:`${-i*2.1}s`,
              }}>❀</span>
            ))}
          </div>
          {/* Diagonal light streak */}
          <div className="vip-card__streak" aria-hidden="true"/>
          <div className="vip-card__overlay" aria-hidden="true"/>
          {/* HUD corner brackets */}
          <div className="vip-card__corner vip-card__corner--tl"/>
          <div className="vip-card__corner vip-card__corner--tr"/>
          <div className="vip-card__corner vip-card__corner--bl"/>
          <div className="vip-card__corner vip-card__corner--br"/>
          {/* Content overlay — left side */}
          <div className="vip-card__content">
            <div className="vip-card__header">
              <div className="vip-card__chip">
                <div className="vip-card__chip-inner"/>
              </div>
              <div className="vip-card__brand">
                <div className="vip-card__brand-name">WOAH!</div>
                <div className="vip-card__brand-sub">NIGHT CLUB · VIP</div>
              </div>
            </div>
            <div className="vip-card__center">
              <div className="vip-card__tier-label">// TIER</div>
              <h3 className="vip-card__tier-name">{tier.name}</h3>
              <div className="vip-card__tier-sub">{tier.sub}</div>
            </div>
            <div className="vip-card__footer">
              <div className="vip-card__num">{memberNum}</div>
              <div className="vip-card__cadence">VALID · {tier.cadence.toUpperCase()}</div>
            </div>
          </div>
          {/* Lifetime gets extra sparkles */}
          {tier.lifetime && (
            <div className="vip-card__sparkles" aria-hidden="true">
              {Array.from({length:8}).map((_,i)=>(
                <span key={i} style={{
                  top:`${10+(i*11)%80}%`,
                  left:`${15+(i*17)%75}%`,
                  animationDelay:`${-i*0.4}s`
                }}>✦</span>
              ))}
            </div>
          )}
        </article>
      </div>

      {/* Perks panel — sits beside / below card */}
      <div className="vip-perks-panel">
        <div className="vip-perks-panel__head">
          <span className="bracket" style={{color:tier.color, borderColor:`${tier.color}55`}}>[ {tier.name} INCLUDES ]</span>
          <span className="vip-perks-panel__tag" style={{color:tier.color}}>{tier.tag}</span>
        </div>
        <ul className="vip-perks-list">
          {VIP_PERKS_BASE.map(([icon,title,desc])=>(
            <li key={title} className="vip-perk-row">
              <div className="vip-perk-row__icon" style={{color:tier.color}}><Icon name={icon} size={20}/></div>
              <div className="vip-perk-row__body">
                <div className="vip-perk-row__title">{title}</div>
                <div className="vip-perk-row__desc">{desc}</div>
              </div>
              <Icon name="check" size={16} color={tier.color}/>
            </li>
          ))}
          {tier.perksExtra.map((extra,i)=>(
            <li key={`x${i}`} className="vip-perk-row vip-perk-row--extra">
              <div className="vip-perk-row__icon" style={{color:tier.color}}><Icon name="star" size={20}/></div>
              <div className="vip-perk-row__body">
                <div className="vip-perk-row__title">{extra}</div>
                <div className="vip-perk-row__desc">Bonus included with {tier.name}.</div>
              </div>
              <Icon name="check" size={16} color={tier.color}/>
            </li>
          ))}
        </ul>
        <div className="vip-perks-panel__cta">
          <a href="https://discord.gg/2UCftk2ZEB" target="_blank" rel="noopener noreferrer"
             className="btn btn--xl vip-cta" style={{'--c':tier.color}}>
            <Icon name="crown" size={14}/>
            Claim {tier.name}
            <Icon name="arrow" size={14}/>
          </a>
          <div className="vip-perks-panel__contact">// CONTACT THE MANAGEMENT</div>
        </div>
      </div>
    </div>
  </section>
  );
};

const Memories = () => {
  return (
    <section className="section" id="memories" data-screen-label="Memories">
      <header className="section__head section__head--side">
        <aside className="section__head-aside" aria-hidden="true">
          <span className="section__head-num">07</span>
          <span className="section__head-line"/>
          <span className="section__head-title-vert">Memories</span>
          <span className="section__head-jp-vert">思い出</span>
        </aside>
        <div className="section__head-bar">
          <span className="hud-label">// SECTION 07</span>
          <h2 className="section__title gradient-title section__head-title-h">Memories</h2>
          <span className="section__jp section__head-jp-h">思い出</span>
        </div>
      </header>
      {typeof MemoriesGalleryV2 !== 'undefined' && <MemoriesGalleryV2/>}
    </section>
  );
};

const Footer = () => {
  const year = new Date().getFullYear();
  return (
    <footer className="footer footer--pro">
      {/* Top edge accent — animated gradient hairline */}
      <div className="footer__top-line" aria-hidden="true"/>

      <div className="footer__inner">
        {/* LEFT — Brand block */}
        <div className="footer__brand-block">
          <a href="#home" className="footer__logo-mark" aria-label="WOAH! Home">
            <span className="footer__logo-text">WOAH<span className="footer__logo-bang">!</span></span>
            <span className="footer__logo-jp">ウォア</span>
          </a>
          <span className="footer__tagline">DJ Gaia&rsquo;s underground sanctuary inside Final Fantasy XIV.</span>
        </div>

        {/* CENTER — Copyright credit */}
        <div className="footer__facts">
          <span className="footer__copyright">
            © 2026 <strong>WOAH!</strong><sup>™</sup> NIGHT CLUB · SITE DESIGN AND CODE BY <span className="footer__credit-flower">🌸</span> <strong>GAIA LOVE</strong>
          </span>
        </div>

        {/* RIGHT — Social icons */}
        <div className="footer__actions">
          <div className="footer__social">
            <a href="https://discord.gg/2UCftk2ZEB" target="_blank" rel="noopener noreferrer"
               className="footer__social-btn" aria-label="Discord" data-tip="Discord">
              <Icon name="discord" size={16}/>
            </a>
            <a href="https://www.instagram.com/woah.nightclub/" target="_blank" rel="noopener noreferrer"
               className="footer__social-btn" aria-label="Instagram" data-tip="Instagram">
              <Icon name="instagram" size={16}/>
            </a>
            <a href="https://djgaiaworld.com" target="_blank" rel="noopener noreferrer"
               className="footer__social-btn" aria-label="DJ Gaia website" data-tip="DJ Gaia">
              <Icon name="globe" size={16}/>
            </a>
            <a href="https://djgaiaworld.com/bingo/" target="_blank" rel="noopener noreferrer"
               className="footer__social-btn footer__social-btn--accent" aria-label="Bingo Tool" data-tip="Bingo · LIVE">
              <Icon name="dices" size={16}/>
            </a>
          </div>
        </div>
      </div>
    </footer>
  );
};

/* ---------- Top Marquee — sits below the nav, measures its height in real time ---------- */
const TopMarquee = () => {
  const [navH, setNavH] = useState(80);
  useEffect(() => {
    const measure = () => {
      const nav = document.querySelector('.nav');
      if (nav) setNavH(nav.getBoundingClientRect().height);
    };
    measure();
    window.addEventListener('resize', measure);
    // Also re-measure shortly after mount, in case fonts/images load late
    const t = setTimeout(measure, 300);
    const t2 = setTimeout(measure, 1200);
    return () => {
      window.removeEventListener('resize', measure);
      clearTimeout(t);
      clearTimeout(t2);
    };
  }, []);

  // One set of items, rendered twice for seamless infinite loop
  const items = [
    'WOAH!', '狼の夜', 'NIGHT CLUB', 'DJ GAIA RESIDENT',
    'BI-WEEKLY MONDAYS', 'KAWAII CYBERPUNK',
    'WOAH!', '狼煙', 'LIGHT · ALPHA · MIST W20 P35',
  ];
  const renderSet = (keyPrefix) => items.map((text, i) => (
    <React.Fragment key={`${keyPrefix}-${i}`}>
      <span>{text}</span>
      <span className="top-marquee__dot">◆</span>
    </React.Fragment>
  ));
  return (
    <div className="top-marquee" aria-hidden="true" style={{ top: `${navH}px` }}>
      <div className="top-marquee__track">
        {renderSet('a')}
        {renderSet('b')}
      </div>
    </div>
  );
};

/* ---------- Routing + Transitions ---------- */

const PAGES = [
  { id:'home',      label:'HOME',      theme:'home',      Comp:Hero,      sid:'7F3A' },
  { id:'staff',     label:'STAFF',     theme:'staff',     Comp:Staff,     sid:'4A2E' },
  { id:'djs',       label:'RESIDENTS', theme:'djs',       Comp:DJs,       sid:'9D44' },
  { id:'events',    label:'EVENTS',    theme:'events',    Comp:Events,    sid:'B12C' },
  { id:'cocktails', label:'MENU',      theme:'cocktails', Comp:Cocktails, sid:'C7F1' },
  { id:'vip',       label:'VIP',       theme:'vip',       Comp:VIP,       sid:'GOLD' },
  { id:'merch',     label:'MERCH',     theme:'merch',     Comp:Merch,     sid:'08AB' },
  { id:'memories',  label:'MEMORIES',  theme:'memories',  Comp:Memories,  sid:'F0F0' },
];

const getPage = (id) => PAGES.find(p=>p.id===id) || PAGES[0];

/* Cyberpunk procedural transition sound — WebAudio
   Layers: hi-tech click → metallic zap sweep → whoosh noise → sub-bass thump → digital glitch tail.
   Designed to fit the cyberpunk anime UI vibe.
*/
let _audioCtx;
const _ensureCtx = () => {
  if (!_audioCtx) _audioCtx = new (window.AudioContext||window.webkitAudioContext)();
  // Browsers suspend the context until first user gesture; resume to be safe
  if (_audioCtx.state === 'suspended') { try { _audioCtx.resume(); } catch(e){} }
  return _audioCtx;
};

const playWhoosh = () => {
  try {
    const ctx = _ensureCtx();
    const t = ctx.currentTime;
    const master = ctx.createGain();
    master.gain.value = 0.85;
    master.connect(ctx.destination);

    /* === LAYER 1: Hi-tech click (sharp transient) === */
    const click = ctx.createOscillator();
    click.type = 'square';
    click.frequency.setValueAtTime(2400, t);
    click.frequency.exponentialRampToValueAtTime(800, t + 0.04);
    const clickGain = ctx.createGain();
    clickGain.gain.setValueAtTime(0, t);
    clickGain.gain.linearRampToValueAtTime(0.06, t + 0.005);
    clickGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.06);
    click.connect(clickGain).connect(master);
    click.start(t); click.stop(t + 0.07);

    /* === LAYER 2: Metallic zap sweep (the signature cyberpunk swoosh) === */
    const zap = ctx.createOscillator();
    zap.type = 'sawtooth';
    zap.frequency.setValueAtTime(180, t + 0.02);
    zap.frequency.exponentialRampToValueAtTime(2200, t + 0.28);
    const zapFilter = ctx.createBiquadFilter();
    zapFilter.type = 'bandpass';
    zapFilter.Q.value = 6;
    zapFilter.frequency.setValueAtTime(600, t + 0.02);
    zapFilter.frequency.exponentialRampToValueAtTime(3200, t + 0.28);
    const zapGain = ctx.createGain();
    zapGain.gain.setValueAtTime(0, t + 0.02);
    zapGain.gain.linearRampToValueAtTime(0.07, t + 0.06);
    zapGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.34);
    zap.connect(zapFilter).connect(zapGain).connect(master);
    zap.start(t + 0.02); zap.stop(t + 0.36);

    /* === LAYER 3: Noise whoosh (air movement / digital sweep) === */
    const buf = ctx.createBuffer(1, ctx.sampleRate * 0.45, ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < d.length; i++) d[i] = (Math.random()*2-1) * (1 - i/d.length);
    const noise = ctx.createBufferSource(); noise.buffer = buf;
    const noiseFilter = ctx.createBiquadFilter();
    noiseFilter.type = 'bandpass';
    noiseFilter.Q.value = 10;
    noiseFilter.frequency.setValueAtTime(500, t);
    noiseFilter.frequency.exponentialRampToValueAtTime(5000, t + 0.32);
    const noiseGain = ctx.createGain();
    noiseGain.gain.setValueAtTime(0.12, t);
    noiseGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.42);
    noise.connect(noiseFilter).connect(noiseGain).connect(master);
    noise.start(t); noise.stop(t + 0.45);

    /* === LAYER 4: Sub-bass thump (gives it "weight") === */
    const sub = ctx.createOscillator();
    sub.type = 'sine';
    sub.frequency.setValueAtTime(80, t + 0.04);
    sub.frequency.exponentialRampToValueAtTime(38, t + 0.22);
    const subGain = ctx.createGain();
    subGain.gain.setValueAtTime(0, t + 0.04);
    subGain.gain.linearRampToValueAtTime(0.18, t + 0.07);
    subGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.28);
    sub.connect(subGain).connect(master);
    sub.start(t + 0.04); sub.stop(t + 0.3);

    /* === LAYER 5: Digital glitch tail (8-bit blip at the end) === */
    const blip = ctx.createOscillator();
    blip.type = 'square';
    blip.frequency.setValueAtTime(1600, t + 0.22);
    blip.frequency.setValueAtTime(900, t + 0.27);
    blip.frequency.setValueAtTime(2200, t + 0.31);
    blip.frequency.exponentialRampToValueAtTime(220, t + 0.4);
    const blipGain = ctx.createGain();
    blipGain.gain.setValueAtTime(0.05, t + 0.22);
    blipGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.42);
    blip.connect(blipGain).connect(master);
    blip.start(t + 0.22); blip.stop(t + 0.42);
  } catch(e) { /* silent fail */ }
};

/* Premium card-open SFX — minimalist, ~350ms total.
   Layers: hi-tech tick (very brief), bright sparkle bloom, soft sub-pulse.
   Designed to feel "wow, premium" without being intrusive on repeated opens. */
const playCardOpen = () => {
  if (typeof window !== 'undefined' && window.__woahSoundOn === false) return;
  try {
    const ctx = _ensureCtx();
    const t = ctx.currentTime;
    const master = ctx.createGain();
    master.gain.value = 0.55;
    master.connect(ctx.destination);

    /* === LAYER 1: micro tick (a precise high transient — like a tactile button) === */
    const tick = ctx.createOscillator();
    tick.type = 'triangle';
    tick.frequency.setValueAtTime(3200, t);
    tick.frequency.exponentialRampToValueAtTime(1800, t + 0.025);
    const tickGain = ctx.createGain();
    tickGain.gain.setValueAtTime(0, t);
    tickGain.gain.linearRampToValueAtTime(0.07, t + 0.003);
    tickGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.04);
    tick.connect(tickGain).connect(master);
    tick.start(t); tick.stop(t + 0.05);

    /* === LAYER 2: rising sparkle bloom (the "wow" — bright harmonic bell) === */
    /* Two slightly detuned sines play a perfect-fifth-ish chord (E5 + B5) for a luxe shimmer */
    const note1 = ctx.createOscillator();
    note1.type = 'sine';
    note1.frequency.setValueAtTime(659.25, t + 0.02); // E5
    const note2 = ctx.createOscillator();
    note2.type = 'sine';
    note2.frequency.setValueAtTime(987.77, t + 0.04); // B5
    const note3 = ctx.createOscillator();
    note3.type = 'sine';
    note3.frequency.setValueAtTime(1318.51, t + 0.06); // E6
    const bellGain = ctx.createGain();
    bellGain.gain.setValueAtTime(0, t);
    bellGain.gain.linearRampToValueAtTime(0.10, t + 0.05);
    bellGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.42);
    /* Soft bandpass to give it that crystalline character */
    const bandpass = ctx.createBiquadFilter();
    bandpass.type = 'bandpass';
    bandpass.frequency.setValueAtTime(900, t);
    bandpass.frequency.exponentialRampToValueAtTime(1800, t + 0.25);
    bandpass.Q.value = 1.4;
    note1.connect(bellGain);
    note2.connect(bellGain);
    note3.connect(bellGain);
    bellGain.connect(bandpass).connect(master);
    note1.start(t + 0.02); note1.stop(t + 0.42);
    note2.start(t + 0.04); note2.stop(t + 0.42);
    note3.start(t + 0.06); note3.stop(t + 0.42);

    /* === LAYER 3: gentle sub-bass thump (gives weight without booming) === */
    const sub = ctx.createOscillator();
    sub.type = 'sine';
    sub.frequency.setValueAtTime(110, t);
    sub.frequency.exponentialRampToValueAtTime(55, t + 0.18);
    const subGain = ctx.createGain();
    subGain.gain.setValueAtTime(0, t);
    subGain.gain.linearRampToValueAtTime(0.06, t + 0.02);
    subGain.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
    sub.connect(subGain).connect(master);
    sub.start(t); sub.stop(t + 0.24);
  } catch(e) { /* silent fail */ }
};

const TransitionOverlay = ({ phase, page }) => (
  <div className={`tx tx--${phase}`} aria-hidden="true">
    {/* Hexagonal grid backdrop */}
    <div className="tx__hex"/>

    {/* Vertical glitch slabs that crash into the center */}
    <div className="tx__slabs">
      <span className="tx__slab tx__slab--l"/>
      <span className="tx__slab tx__slab--r"/>
    </div>

    {/* Static + RGB chromatic glitch noise */}
    <div className="tx__static"/>
    <div className="tx__chroma tx__chroma--r"/>
    <div className="tx__chroma tx__chroma--g"/>
    <div className="tx__chroma tx__chroma--b"/>

    {/* Horizontal cyan bands that streak across */}
    <div className="tx__bands">
      {Array.from({length:9}).map((_,i)=>(
        <span key={i} style={{top:`${i*11}%`, animationDelay:`${i*22}ms`}}/>
      ))}
    </div>

    {/* Energy wave — pink burst from center */}
    <div className="tx__wave"/>

    {/* Cross-hair laser scanlines */}
    <div className="tx__scan tx__scan--out"/>
    <div className="tx__scan tx__scan--in"/>
    <div className="tx__scan-v tx__scan-v--out"/>
    <div className="tx__scan-v tx__scan-v--in"/>

    {/* Theme color flash on entry */}
    <div className="tx__flash" style={{background:`var(--theme-flash, transparent)`}}/>

    {/* Big section name in the center */}
    <div className="tx__title">
      <span className="tx__title-prefix">// SECTION_{String(PAGES.indexOf(page)+1).padStart(2,'0')}</span>
      <span className="tx__title-text" data-text={page.label}>{page.label}</span>
      <span className="tx__title-bar"/>
    </div>

    {/* Loader */}
    <div className="tx__loader">
      <div className="tx__loader-mark"><WoahMark size={36}/></div>
      <div className="tx__loader-text">&gt; LOADING [ {page.label} ] _</div>
    </div>

    {/* Corner brackets */}
    <span className="tx__corner tx__corner--tl"/>
    <span className="tx__corner tx__corner--tr"/>
    <span className="tx__corner tx__corner--bl"/>
    <span className="tx__corner tx__corner--br"/>
  </div>
);

const SessionId = ({ page }) => (
  <div className="session-id">SESSION: WOAH-{page.label}-{page.sid}</div>
);

const CursorFlash = ({ trigger }) => {
  const [pos, setPos] = useState({x:-100,y:-100});
  const [on, setOn] = useState(false);
  useEffect(()=>{
    const onMove = e => setPos({x:e.clientX,y:e.clientY});
    window.addEventListener('mousemove', onMove);
    return () => window.removeEventListener('mousemove', onMove);
  },[]);
  useEffect(()=>{
    if (!trigger) return;
    setOn(true);
    const t = setTimeout(()=>setOn(false), 380);
    return () => clearTimeout(t);
  },[trigger]);
  if (!on) return null;
  return <div className="cursor-flash" style={{left:pos.x,top:pos.y}}/>;
};

/* ---------- App ---------- */

/* Hash routing helpers — keep page state in URL so F5 / back / forward work */
const getPageFromHash = () => {
  const hash = (window.location.hash || '').replace(/^#/, '').toLowerCase();
  // Only accept it if it matches a known page id
  if (PAGES && PAGES.some(p => p.id === hash)) return hash;
  return 'home';
};

const App = () => {
  const [mobileOpen, setMobileOpen] = useState(false);
  // Initialize from URL hash so refresh keeps you on the same page
  const [pageId, setPageId] = useState(getPageFromHash);
  const [phase, setPhase] = useState('idle'); // idle | out | mid | in
  const [pendingId, setPendingId] = useState(null);
  const [soundOn, setSoundOn] = useState(true);
  const [cursorTick, setCursorTick] = useState(0);

  // Expose soundOn globally so non-React helpers (playCardOpen) can respect it
  useEffect(()=>{
    if (typeof window !== 'undefined') window.__woahSoundOn = soundOn;
  }, [soundOn]);

  const reduced = typeof window !== 'undefined' &&
    window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  const navigate = useCallback((id) => {
    if (id === pageId || phase !== 'idle') return;
    if (soundOn) playWhoosh();
    setCursorTick(t=>t+1);

    // Update URL hash so refresh / share-link / browser back-forward work
    // Use replaceState for 'home' to avoid littering history with empty hash
    if (id === 'home') {
      history.replaceState(null, '', window.location.pathname + window.location.search);
    } else if (window.location.hash !== `#${id}`) {
      history.pushState(null, '', `#${id}`);
    }

    if (reduced) {
      setPhase('out');
      setTimeout(()=>{ setPageId(id); setPhase('in'); }, 100);
      setTimeout(()=>setPhase('idle'), 300);
      window.scrollTo(0,0);
      return;
    }
    setPendingId(id);
    const fast = window.matchMedia('(max-width: 640px)').matches;
    const T = fast ? { out:200, mid:300, total:900 } : { out:300, mid:400, total:1200 };
    setPhase('out');
    setTimeout(()=>{ setPhase('mid'); }, T.out);
    setTimeout(()=>{ setPageId(id); setPhase('in'); window.scrollTo(0,0); }, T.out + T.mid);
    setTimeout(()=>{ setPhase('idle'); setPendingId(null); }, T.total);
  },[pageId, phase, soundOn, reduced]);

  // Expose navigate globally so child components in other files can trigger it
  useEffect(()=>{ window.navigateTo = navigate; },[navigate]);

  // Listen for browser back/forward so URL stays in sync with state
  useEffect(()=>{
    const onPop = () => {
      const target = getPageFromHash();
      if (target !== pageId && phase === 'idle') {
        // Re-trigger navigate but without pushing another history entry
        // (the browser already handled the URL change)
        if (soundOn) playWhoosh();
        if (reduced) {
          setPhase('out');
          setTimeout(()=>{ setPageId(target); setPhase('in'); }, 100);
          setTimeout(()=>setPhase('idle'), 300);
          window.scrollTo(0,0);
          return;
        }
        setPendingId(target);
        const fast = window.matchMedia('(max-width: 640px)').matches;
        const T = fast ? { out:200, mid:300, total:900 } : { out:300, mid:400, total:1200 };
        setPhase('out');
        setTimeout(()=>{ setPhase('mid'); }, T.out);
        setTimeout(()=>{ setPageId(target); setPhase('in'); window.scrollTo(0,0); }, T.out + T.mid);
        setTimeout(()=>{ setPhase('idle'); setPendingId(null); }, T.total);
      }
    };
    window.addEventListener('popstate', onPop);
    window.addEventListener('hashchange', onPop);
    return () => {
      window.removeEventListener('popstate', onPop);
      window.removeEventListener('hashchange', onPop);
    };
  },[pageId, phase, soundOn, reduced]);

  // expose globally so existing in-page links can route
  useEffect(()=>{ window.__woahNavigate = navigate; },[navigate]);

  // intercept all href="#xxx" clicks where xxx matches a page id
  useEffect(()=>{
    const onClick = (e) => {
      const a = e.target.closest && e.target.closest('a[href^="#"]');
      if (!a) return;
      const href = a.getAttribute('href') || '';
      const id = href.slice(1);
      if (PAGES.some(p=>p.id===id)) {
        e.preventDefault();
        navigate(id);
      }
    };
    document.addEventListener('click', onClick);
    return () => document.removeEventListener('click', onClick);
  },[navigate]);

  const page = getPage(pageId);
  const pendingPage = pendingId ? getPage(pendingId) : page;
  const pageIdx = PAGES.findIndex(p=>p.id===pageId)+1;
  const Comp = page.Comp;

  return (
    <div className={`app theme--${page.theme} phase--${phase}`} data-theme={page.theme}>
      <AmbientParticles/>
      <CustomCursor/>
      <Nav active={pageId} navigate={navigate} onOpenMobile={()=>setMobileOpen(true)}
           soundOn={soundOn} onToggleSound={()=>setSoundOn(s=>!s)}
           pageIdx={pageIdx} totalPages={PAGES.length}/>
      <TopMarquee/>
      <MobileMenu open={mobileOpen} onClose={()=>setMobileOpen(false)} navigate={navigate}/>
      <main className="page-wrap">
        <div className="page" key={pageId}>
          <Comp/>
        </div>
      </main>
      <Footer/>
      {phase!=='idle' && <TransitionOverlay phase={phase} page={pendingPage}/>}
      <CursorFlash trigger={cursorTick}/>
    </div>
  );
};

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