/* @jsx React.createElement */
/* ============================================================
   INDIE Music School & Studio — single-file site
   React 18 UMD + Babel standalone. No build step.
   All components + DEFAULT_CONTENT + Supabase CMS loader.
   ============================================================ */

const { useState, useEffect, useRef, useCallback } = React;

/* ─────────────────────────────────────────────
   Config
   ───────────────────────────────────────────── */
const SUPABASE_URL = 'https://indie.supabase.co';
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Indsc3pqdnZveXltd2dwbXRnd2V4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ1ODg5OTcsImV4cCI6MjA5MDE2NDk5N30.5emUfc2jlLxutLhtGZFD0Rq3MOUl4JAcU3Fd5RydjRU';
const CONTENT_TABLE = 'ims_website_content';
const FETCH_TIMEOUT = 3000;

const WA_NUMBER = '628998267999';
const WA_DISPLAY = '+62 899-8267-999';
function waLink(text) {
  const base = 'https://wa.me/' + WA_NUMBER;
  return text ? base + '?text=' + encodeURIComponent(text) : base;
}

/* ─────────────────────────────────────────────
   DEFAULT_CONTENT — the site renders from this instantly.
   CMS rows deep-merge over these values.
   ───────────────────────────────────────────── */
const DEFAULT_CONTENT = {
  hero: {
    eyebrow: 'INDIE Music · School & Studio',
    title: 'Happiness starts here.',
    subtitle: 'Sekolah musik anti-mainstream di Gading Serpong. Studio yang estetik, guru yang asik, dan cara belajar yang interaktif — bikin musik nggak lagi kayak beban, tapi jadi happiness yang dipancarkan tiap minggu.',
    cta_primary: { label: 'Coba kelas gratis', url: waLink('Halo IMS! Aku mau coba kelas gratis dong.') },
    cta_secondary: { label: 'Lihat metodenya', url: '#method' },
    video_url: '',
    slides: [
      {
        image: 'assets/photos/hero-band.jpg',
        eyebrow: 'INDIE Music · School & Studio',
        title: 'Happiness starts here.',
        subtitle: 'Sekolah musik anti-mainstream di Gading Serpong. Studio yang estetik, guru yang asik, dan cara belajar yang interaktif — bikin musik nggak lagi kayak beban.',
        typing: true,
      },
      {
        image: 'assets/photos/hero-drum-kid.jpg',
        eyebrow: 'Kurikulum anti-mainstream',
        title: 'Main musik, bukan dihafalin teori.',
        subtitle: 'Dari minggu pertama kamu udah main lagu beneran — teori nyusul sambil jalan, bukan jadi gerbang.',
        typing: true,
      },
      {
        image: 'assets/photos/hero-piano-hands.jpg',
        eyebrow: 'Studio estetik · Gading Serpong',
        title: 'Belajar di tempat yang bikin betah.',
        subtitle: 'Ruang kelas & studio recording standar pro. Datang buat les, pulang bawa vibe.',
        typing: false,
      },
    ],
    images: [
      'assets/photos/hero-band.jpg',
      'assets/photos/hero-drum-kid.jpg',
      'assets/photos/hero-piano-hands.jpg',
    ],
  },

  marquee: {
    items: [
      'Guitar', 'Piano', 'Drum', 'Bass', 'Vocal',
      'Anak & Dewasa', 'Private · Group · In-Home',
      'Pre-Beginner → Performance 3', '6 Skill Dimensions',
      '59 Stamps', 'Parent Dashboard', 'Studio Recording & Rehearsal',
    ],
  },

  stats: {
    items: [
      { value: '5', suffix: '', label: 'Instrumen' },
      { value: '6', suffix: '', label: 'Level journey' },
      { value: '59', suffix: '', label: 'Stamp dikoleksi' },
      { value: '92', suffix: '%', label: 'Lanjut setelah bulan 3' },
    ],
  },

  instruments: {
    title: 'Lima instrumen. Satu tempat buat jatuh cinta sama musik.',
    intro: 'Mau mulai dari mana? Pilih instrumen kamu — atau anak kamu. Semua diajar dengan lagu beneran sejak minggu pertama.',
    items: [
      { key: 'guitar', name: 'Guitar', desc: 'Dari open chord pertama sampai fingerstyle solo. Akustik, elektrik, klasik — pilih gitar kamu.', image: 'assets/photos/lesson-guitar.jpg', wa_text: 'Halo IMS! Aku mau coba kelas gitar.' },
      { key: 'piano', name: 'Piano', desc: 'Koordinasi dua tangan, pengenalan chord & scale, teori dasar. Pop dan lainnya, sesuai karakter kamu.', image: 'assets/photos/lesson-piano.jpg', wa_text: 'Halo IMS! Aku mau coba kelas piano.' },
      { key: 'drum', name: 'Drum', desc: 'Koordinasi tangan dan kaki, groove basic 4/4, drum fill pertama kamu. Rock dan Pop.', image: 'assets/photos/hero-drum-kid.jpg', wa_text: 'Halo IMS! Aku mau coba kelas drum.' },
      { key: 'bass', name: 'Bass', desc: 'Feel, groove, dan fondasi dalam sebuah lagu. Petik dua jari yang stabil, lock sama drum.', image: 'assets/photos/lesson-bass.jpg', wa_text: 'Halo IMS! Aku mau coba kelas bass.' },
      { key: 'vocal', name: 'Vocal', desc: 'Cari suara asli kamu. Napas, pitch, tone — plus pede pakainya. Dari matching pitch sampai lagu utuh.', image: 'assets/photos/lesson-vocal.jpg', wa_text: 'Halo IMS! Aku mau coba kelas vokal.' },
    ],
  },

  method: {
    title: 'Progress yang beneran keliatan — tanpa bikin kayak ujian.',
    intro: 'Sekolah lain biasanya cuma bilang "good job" terus selesai. Kita bikin perjalanan kamu kelihatan, dari chord pertama sampai naik panggung.',
    steps: [
      { title: 'Coba dulu, gratis', desc: 'Session pertama gratis buat murid baru. Nggak perlu kartu kredit, nggak perlu komitmen. Dateng, main, rasain vibe-nya.' },
      { title: 'Ketemu guru & level kamu', desc: 'Kita mulai dari posisi kamu sekarang — Pre-Beginner sampai Performance. Nggak ada yang buru-buru, nggak ada yang ketinggalan.' },
      { title: 'Level + Radar + Badges', desc: 'Tiga lensa jujur: Level Journey (6 milestone), Skill Radar (6 dimensi di-score tiap session), dan Milestone Badges yang di-unlock kalau kamu beneran udah bisa.' },
      { title: 'Naik panggung & masuk studio', desc: 'Di Performance 1, kamu dapet studio recording session beneran — bukan add-on berbayar. Take-nya kamu bawa pulang.' },
    ],
  },

  teachers: {
    title: 'Belajar bareng orangnya langsung.',
    intro: 'Di IMS, kamu nggak diajar sama "instruktur generasi ke-tiga" yang baru kerja minggu lalu. Kamu diajar sama orang yang bikin sekolahnya — kurikulum, filosofi, dan vibe-nya.',
    items: [
      {
        name: 'Andre Putra',
        role: 'Co-Founder & Main Teacher',
        instrument: 'Guitar',
        bio: 'Andre udah main gitar sejak kelas 6 SD, mulai ngajar sejak SMA, dan akhirnya mendirikan INDIE Music School karena capek lihat banyak anak belajar musik tapi malah nggak enjoy. Filosofinya simple: musik seharusnya jadi tempat buat bahagia, berekspresi, dan berkembang — bukan bikin stress.',
        photo_url: '',
      },
    ],
  },

  gallery: {
    title: 'Intip dulu tempatnya.',
    images: [
      { src: 'assets/photos/group-kids.jpg', caption: 'Kelas grup anak — seru, nggak nyeremin.' },
      { src: 'assets/photos/recital-stage.jpg', caption: 'Recital & panggung — momen naik level.' },
      { src: 'assets/photos/studio-gear.jpg', caption: 'Studio gear — dipakai murid & musisi luar.' },
      { src: 'assets/photos/lesson-piano.jpg', caption: 'Ruang kelas piano yang hangat.' },
      { src: 'assets/photos/hero-band.jpg', caption: 'Live room — rekam single yang siap rilis.' },
      { src: 'assets/photos/lesson-guitar.jpg', caption: 'One-on-one, langsung sama founder-nya.' },
    ],
  },

  testimonials: {
    title: 'Kata mereka yang udah main bareng kita.',
    items: [
      { quote: 'Anak gue dulu suka minta skip les piano. Sekarang malah minta berangkat lebih cepet biar bisa ngobrol dulu sama gurunya sebelum kelas mulai. Itu udah review-nya.', name: 'Rina P.', meta: 'Orang tua Kayla, 9 thn · Piano · 8 bulan di IMS' },
      { quote: 'Umur 34 baru mulai belajar gitar dari nol total. Ternyata nggak seseram itu — Pre-Beginner emang didesain buat orang kayak aku. Sekarang udah bisa ngiringin lagu sendiri.', name: 'Danang S.', meta: 'Murid dewasa · Guitar · Private' },
      { quote: 'Yang bikin beda itu progress-nya keliatan. Tiap minggu ada catatan, ada stamp, radar-nya nambah. Jadi tahu uang les nggak ke mana-mana.', name: 'Melisa T.', meta: 'Orang tua Raka, 11 thn · Drum' },
    ],
  },

  pricing: {
    title: 'Satu kurikulum, tiga cara belajar.',
    intro: 'Kurikulum sama, ritme beda. Semua harga per bulan (4 session). Sepanjang tahun kamu boleh kombinasi format.',
    plans: [
      { name: 'Group', price: 'Rp 700rb', unit: '/ bulan · 4 session', features: ['Maks 4 murid per kelas', 'Lebih hemat, lebih seru', 'Skill Radar + Stamp card', 'Parent dashboard'], highlight: false },
      { name: 'Private', price: 'Rp 1,1jt', unit: '/ bulan · 4 session', features: ['1-on-1 langsung sama guru', 'Jadwal fleksibel', 'Skill Radar + Milestone Badges', 'Studio session di Performance 1', 'Parent dashboard'], highlight: true },
      { name: 'In-Home Private', price: 'Rp 1,5jt', unit: '/ bulan · 4 session', features: ['Guru datang ke rumah kamu', 'Greater Jakarta only', 'Semua benefit Private', 'Nggak perlu jalan macet'], highlight: false },
    ],
  },

  faq: {
    title: 'Pertanyaan yang sering masuk ke WA kita.',
    items: [
      { q: 'Berapa lama sampai aku bisa main satu lagu utuh?', a: 'Biasanya minggu ke-3 udah bisa. Lagu beneran, yang orang kenal, yang "bisa direkam pakai HP". Belum Chopin sih, tapi udah musik.' },
      { q: 'Kalau aku/anakku mau ganti instrumen, bisa?', a: 'Bebas ganti kapan aja. Stamp card sama Skill Radar yang umum (rhythm, ear, theory) tetep nyangkut. Yang spesifik per instrumen reset.' },
      { q: 'Ada prep ujian musik (ABRSM, RSL) nggak?', a: 'Ada. Kasih tau pas daftar — kita slot-in ke kurikulum yang sesuai. Andre handle prep gitar; piano dikejar bareng kurikulum standar.' },
      { q: 'Orang dewasa beneran bisa mulai dari nol?', a: 'Setengah murid dewasa kita mulai dari nol total. Pre-Beginner emang didesain buat itu. Nggak ada yang buru-buru.' },
      { q: 'Policy cancel / reschedule gimana?', a: 'Reschedule maksimal 6 jam sebelum kelas — gratis. Lebih dari itu, kehitung. Semua status ke-track di dashboard.' },
      { q: 'Studio bisa di-rent buat non-murid IMS?', a: 'Bisa. Booking by hour, dengan atau tanpa engineer. WA aja buat cek slot kosong. Murid IMS dapet prioritas + rate khusus.' },
    ],
  },

  cta: {
    title: 'Mampir dulu. Pulang udah senyum.',
    subtitle: 'Booking trial 90 detik. Kita match-in kamu ke guru, hold slot kamu, dan siapin instrumen yang udah ditune dari tadi.',
    button: { label: 'Coba kelas gratis via WhatsApp', url: waLink('Halo IMS! Aku mau booking kelas trial gratis.') },
  },

  contact: {
    address: 'Ruko Neo Arcade Blok A33, Gading Serpong, Tangerang, Banten 15334',
    phone: '+628998267999',
    wa: waLink('Halo IMS!'),
    instagram: 'https://www.instagram.com/indiemusic.id',
    maps_url: 'https://maps.google.com/?q=-6.243819059545993,106.62397551047178',
    hours: 'Buka tiap hari · 10.00 – 21.00',
  },
};

/* ─────────────────────────────────────────────
   Deep merge — CMS data over defaults.
   Objects merge recursively; arrays & scalars replace.
   ───────────────────────────────────────────── */
function isPlainObject(v) {
  return v && typeof v === 'object' && !Array.isArray(v);
}
function deepMerge(base, over) {
  if (!isPlainObject(base) || !isPlainObject(over)) {
    return over === undefined ? base : over;
  }
  const out = Object.assign({}, base);
  for (const k of Object.keys(over)) {
    if (isPlainObject(base[k]) && isPlainObject(over[k])) {
      out[k] = deepMerge(base[k], over[k]);
    } else if (over[k] !== undefined) {
      out[k] = over[k];
    }
  }
  return out;
}

/* ─────────────────────────────────────────────
   Content hook — render defaults immediately,
   re-render when Supabase resolves. Any failure → defaults.
   ───────────────────────────────────────────── */
function useContent() {
  const [content, setContent] = useState(DEFAULT_CONTENT);

  useEffect(() => {
    let alive = true;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
    const url = SUPABASE_URL + '/rest/v1/' + CONTENT_TABLE + '?select=section,data';

    fetch(url, {
      headers: {
        apikey: SUPABASE_ANON_KEY,
        Authorization: 'Bearer ' + SUPABASE_ANON_KEY,
      },
      signal: controller.signal,
    })
      .then(r => (r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status))))
      .then(rows => {
        if (!alive || !Array.isArray(rows)) return;
        const merged = Object.assign({}, DEFAULT_CONTENT);
        for (const row of rows) {
          if (!row || !row.section) continue;
          const base = DEFAULT_CONTENT[row.section] || {};
          merged[row.section] = deepMerge(base, row.data || {});
        }
        setContent(merged);
      })
      .catch(() => { /* silent → defaults stay */ })
      .finally(() => clearTimeout(timer));

    return () => { alive = false; clearTimeout(timer); controller.abort(); };
  }, []);

  return content;
}

/* ─────────────────────────────────────────────
   Reveal-on-scroll — IntersectionObserver, reduced-motion safe.
   Returns a ref; adds .is-in when element enters viewport.
   ───────────────────────────────────────────── */
const FORCE_MOTION = typeof window !== 'undefined'
  && /[?&]motion=1/.test(window.location.search);
const PREFERS_REDUCED = !FORCE_MOTION
  && typeof window !== 'undefined'
  && window.matchMedia
  && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// ?motion=1 juga menonaktifkan blok CSS reduced-motion
if (FORCE_MOTION && typeof document !== 'undefined') {
  document.documentElement.classList.add('force-motion');
}

function useReveal(options) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (PREFERS_REDUCED || !('IntersectionObserver' in window)) {
      el.classList.add('is-in');
      return;
    }
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          el.classList.add('is-in');
          io.unobserve(el);
        }
      });
    }, Object.assign({ threshold: 0.15, rootMargin: '0px 0px -8% 0px' }, options));
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return ref;
}

function Reveal({ children, delay = 0, className = '', tag = 'div', style = {}, ...rest }) {
  const ref = useReveal();
  const Tag = tag;
  return (
    <Tag
      ref={ref}
      className={'reveal ' + className}
      style={Object.assign({ transitionDelay: delay + 'ms' }, style)}
      {...rest}
    >
      {children}
    </Tag>
  );
}

/* ─────────────────────────────────────────────
   Count-up — animates when scrolled into view.
   ───────────────────────────────────────────── */
function useInView(threshold = 0.4) {
  const ref = useRef(null);
  const [inView, setInView] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (!('IntersectionObserver' in window)) { setInView(true); return; }
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => { if (e.isIntersecting) { setInView(true); io.unobserve(el); } });
    }, { threshold });
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return [ref, inView];
}

function useTypewriter(text, active, speed = 52) {
  const full = String(text || '');
  const [n, setN] = useState(active && !PREFERS_REDUCED ? 0 : full.length);
  useEffect(() => {
    if (!active || PREFERS_REDUCED) { setN(full.length); return; }
    setN(0);
    const t0 = Date.now();
    const t = setInterval(() => {
      const n = Math.min(full.length, Math.floor((Date.now() - t0) / speed));
      setN(n);
      if (n >= full.length) clearInterval(t);
    }, 40);
    return () => clearInterval(t);
  }, [full, active]);
  return [full.slice(0, n), n < full.length];
}

function CountUp({ value, suffix = '' }) {
  const target = parseFloat(String(value).replace(/[^0-9.]/g, ''));
  const decimals = String(value).includes('.') ? 1 : 0;
  const [ref, inView] = useInView(0.5);
  const [display, setDisplay] = useState(isNaN(target) ? value : (PREFERS_REDUCED ? value : '0'));

  useEffect(() => {
    if (!inView || isNaN(target)) { if (isNaN(target)) setDisplay(value); return; }
    if (PREFERS_REDUCED) { setDisplay(target.toFixed(decimals)); return; }
    let raf;
    const dur = 1400;
    let start;
    const tick = (ts) => {
      if (start === undefined) start = ts;
      const p = Math.min((ts - start) / dur, 1);
      const eased = 1 - Math.pow(1 - p, 3);
      setDisplay((target * eased).toFixed(decimals));
      if (p < 1) raf = requestAnimationFrame(tick);
      else setDisplay(target.toFixed(decimals));
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [inView, target]);

  return <span ref={ref}>{display}{suffix}</span>;
}

/* ─────────────────────────────────────────────
   NavBar — transparent over hero → solid on scroll.
   ───────────────────────────────────────────── */
const NAV_LINKS = [
  { href: '#programs', label: 'Program' },
  { href: '#method', label: 'Metode' },
  { href: '#teachers', label: 'Guru' },
  { href: '#studio', label: 'Studio' },
  { href: '#pricing', label: 'Harga' },
  { href: '#faq', label: 'FAQ' },
];

function NavBar({ content }) {
  const [scrolled, setScrolled] = useState(false);
  const [open, setOpen] = useState(false);

  useEffect(() => {
    const onScroll = () => setScrolled(window.scrollY > 24);
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, []);

  useEffect(() => {
    document.body.style.overflow = open ? 'hidden' : '';
    return () => { document.body.style.overflow = ''; };
  }, [open]);

  const cta = content.hero.cta_primary || {};

  return (
    <header className={'nav' + (scrolled ? ' nav--solid' : '') + (open ? ' nav--open' : '')}>
      <div className="nav__inner">
        <a href="#top" className="nav__brand" onClick={() => setOpen(false)}>
          <img src="assets/logos/ims.png" alt="INDIE Music" width="40" height="40" />
          <span className="nav__brand-text">
            <span className="nav__brand-name">INDIE Music</span>
            <span className="nav__brand-sub">School &amp; Studio</span>
          </span>
        </a>

        <nav className="nav__links" aria-label="Utama">
          {NAV_LINKS.map(l => (
            <a key={l.href} href={l.href} className="nav__link">{l.label}</a>
          ))}
        </nav>

        <div className="nav__actions">
          <a className="nav__signin" href="https://member.indiespace.id" target="_blank" rel="noopener">
            Member sign in <span aria-hidden>↗</span>
          </a>
          <a className="btn btn--primary nav__cta" href={cta.url || waLink()}>{cta.label || 'Coba kelas gratis'}</a>
          <button
            className="nav__toggle"
            aria-label={open ? 'Tutup menu' : 'Buka menu'}
            aria-expanded={open}
            onClick={() => setOpen(o => !o)}
          >
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
              {open
                ? (<g><path d="M6 6 L18 18" /><path d="M18 6 L6 18" /></g>)
                : (<g><path d="M4 7 L20 7" /><path d="M4 12 L20 12" /><path d="M4 17 L20 17" /></g>)}
            </svg>
          </button>
        </div>
      </div>

      <div className={'nav__overlay' + (open ? ' is-open' : '')}>
        <nav aria-label="Mobile">
          {NAV_LINKS.map(l => (
            <a key={l.href} href={l.href} onClick={() => setOpen(false)}>{l.label}</a>
          ))}
        </nav>
        <a className="btn btn--primary nav__overlay-cta" href={cta.url || waLink()} onClick={() => setOpen(false)}>
          {cta.label || 'Coba kelas gratis'}
        </a>
        <a className="nav__overlay-sign" href="https://member.indiespace.id" target="_blank" rel="noopener">Member sign in ↗</a>
      </div>
    </header>
  );
}

/* ─────────────────────────────────────────────
   Hero — Ken Burns slideshow OR video, dark overlay.
   ───────────────────────────────────────────── */
function Hero({ data }) {
  const legacyImages = (data.images && data.images.length) ? data.images : DEFAULT_CONTENT.hero.images;
  const slides = (data.slides && data.slides.length)
    ? data.slides
    : legacyImages.map((img, i) => ({
        image: img, eyebrow: data.eyebrow, title: data.title,
        subtitle: data.subtitle, typing: i === 0,
      }));
  const [idx, setIdx] = useState(0);
  const hasVideo = !!(data.video_url && data.video_url.trim());
  const cur = slides[idx % slides.length] || slides[0];

  useEffect(() => {
    if (hasVideo || slides.length <= 1) return;
    const t = setInterval(() => setIdx(i => (i + 1) % slides.length), 9000);
    return () => clearInterval(t);
  }, [hasVideo, slides.length]);

  const p = data.cta_primary || {};
  const sec = data.cta_secondary || {};
  const [typed, isTyping] = useTypewriter(cur.title, !!cur.typing);

  return (
    <section id="top" className="hero">
      <div className="hero__media" aria-hidden>
        {hasVideo ? (
          <video
            className="hero__video"
            autoPlay muted loop playsInline
            poster={slides[0].image}
          >
            <source src={data.video_url} type="video/mp4" />
          </video>
        ) : (
          slides.map((sl, i) => (
            <div
              key={(sl.image || '') + i}
              className={'hero__slide' + (i === idx ? ' is-active' : '')}
              style={{ backgroundImage: 'url(' + sl.image + ')' }}
            />
          ))
        )}
        <div className="hero__scrim" />
      </div>

      <div className="hero__content" key={idx}>
        <div className="hero__eyebrow hero__text-in">{cur.eyebrow}</div>
        <h1 className="hero__title hero__text-in" style={{ animationDelay: '110ms' }}>
          {cur.typing && !PREFERS_REDUCED ? (
            <>
              <span className="hero__title-ghost" aria-hidden>{cur.title}</span>
              <span className="hero__title-typed">
                {typed}
                <span className={'hero__caret' + (isTyping ? '' : ' hero__caret--done')} aria-hidden />
              </span>
            </>
          ) : cur.title}
        </h1>
        <p className="hero__subtitle hero__text-in" style={{ animationDelay: '230ms' }}>{cur.subtitle}</p>
        <div className="hero__ctas hero__text-in" style={{ animationDelay: '350ms' }}>
          {p.label && <a className="btn btn--primary btn--lg" href={p.url || waLink()}>{p.label}</a>}
          {sec.label && <a className="btn btn--glass btn--lg" href={sec.url || '#method'}>{sec.label}</a>}
        </div>
      </div>

      {slides.length > 1 && !hasVideo && (
        <div className="hero__dots" role="tablist" aria-label="Slideshow">
          {slides.map((_, i) => (
            <button
              key={i}
              className={'hero__dot' + (i === idx ? ' is-active' : '')}
              aria-label={'Slide ' + (i + 1)}
              onClick={() => setIdx(i)}
            />
          ))}
        </div>
      )}

      <a href="#programs" className="hero__scroll" aria-label="Scroll ke bawah">
        <span>Scroll</span>
        <svg width="18" height="26" viewBox="0 0 18 26" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden>
          <rect x="1" y="1" width="16" height="24" rx="8" />
          <path className="hero__scroll-dot" d="M9 6 L9 11" />
        </svg>
      </a>
    </section>
  );
}

/* ─────────────────────────────────────────────
   Marquee — infinite CSS scroll.
   ───────────────────────────────────────────── */
function Marquee({ data }) {
  const items = data.items || [];
  const run = items.concat(items);
  return (
    <div className="marquee" aria-hidden>
      <div className="marquee__track">
        {run.map((t, i) => (
          <span key={i} className="marquee__item">
            {t}
            <span className="marquee__sep">✳</span>
          </span>
        ))}
      </div>
    </div>
  );
}

/* ─────────────────────────────────────────────
   Stats band — count-up on scroll.
   ───────────────────────────────────────────── */
function Stats({ data }) {
  const items = data.items || [];
  return (
    <section className="stats">
      <div className="stats__grid wrap">
        {items.map((s, i) => (
          <Reveal key={i} className="stats__item" delay={i * 90}>
            <div className="stats__value">
              <CountUp value={s.value} suffix={s.suffix} />
            </div>
            <div className="stats__label">{s.label}</div>
          </Reveal>
        ))}
      </div>
    </section>
  );
}

/* ─────────────────────────────────────────────
   Instruments — editorial photo cards.
   ───────────────────────────────────────────── */
function Instruments({ data }) {
  const items = data.items || [];
  return (
    <section id="programs" className="section instruments">
      <div className="wrap">
        <Reveal className="section__head">
          <div className="eyebrow-line">Program · Pilih instrumen kamu</div>
          <h2 className="section__title">{data.title}</h2>
          <p className="section__intro">{data.intro}</p>
        </Reveal>

        <div className="instruments__grid">
          {items.map((it, i) => (
            <Reveal
              key={it.key || i}
              tag="a"
              className={'inst-card inst-card--' + ((i % 5 === 0 || i % 5 === 3) ? 'wide' : 'tall')}
              href={waLink(it.wa_text)}
              delay={(i % 3) * 80}
            >
              <div className="inst-card__media">
                <img src={it.image} alt={it.name} loading="lazy" />
              </div>
              <div className="inst-card__overlay" />
              <div className="inst-card__body">
                <span className="inst-card__index">0{i + 1}</span>
                <h3 className="inst-card__name">{it.name}</h3>
                <p className="inst-card__desc">{it.desc}</p>
                <span className="inst-card__cta">Coba kelas {it.name.toLowerCase()} <span aria-hidden>→</span></span>
              </div>
            </Reveal>
          ))}
        </div>
      </div>
    </section>
  );
}

/* ─────────────────────────────────────────────
   Method — numbered alternating steps.
   ───────────────────────────────────────────── */
function Method({ data }) {
  const steps = data.steps || [];
  return (
    <section id="method" className="section section--surface method">
      <div className="wrap">
        <Reveal className="section__head">
          <div className="eyebrow-line">Metode IMS · A + B + C</div>
          <h2 className="section__title">{data.title}</h2>
          <p className="section__intro">{data.intro}</p>
        </Reveal>

        <div className="method__steps">
          {steps.map((st, i) => (
            <Reveal key={i} className={'method__step' + (i % 2 ? ' method__step--alt reveal--right' : ' reveal--left')} delay={40}>
              <div className="method__num" aria-hidden>{String(i + 1).padStart(2, '0')}</div>
              <div className="method__step-body">
                <h3 className="method__step-title">{st.title}</h3>
                <p className="method__step-desc">{st.desc}</p>
              </div>
              <div className="method__connector" aria-hidden />
            </Reveal>
          ))}
        </div>
      </div>
    </section>
  );
}

/* ─────────────────────────────────────────────
   Teachers — photo OR monogram cards.
   ───────────────────────────────────────────── */
function initialsOf(name) {
  return (name || '')
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2)
    .map(w => w[0])
    .join('')
    .toUpperCase();
}

function Teachers({ data }) {
  const items = data.items || [];
  return (
    <section id="teachers" className="section teachers">
      <div className="wrap">
        <Reveal className="section__head">
          <div className="eyebrow-line">Guru · Diajar langsung sama Co-Founder</div>
          <h2 className="section__title">{data.title}</h2>
          <p className="section__intro">{data.intro}</p>
        </Reveal>

        <div className={'teachers__grid' + (items.length === 1 ? ' teachers__grid--single' : '')}>
          {items.map((t, i) => (
            <Reveal key={i} className="teacher reveal--zoom" delay={(i % 3) * 90}>
              <div className="teacher__photo">
                {t.photo_url
                  ? <img src={t.photo_url} alt={t.name} loading="lazy" />
                  : <span className="teacher__monogram" aria-hidden>{initialsOf(t.name)}</span>}
                {t.instrument && <span className="teacher__badge">{t.instrument}</span>}
              </div>
              <div className="teacher__body">
                <h3 className="teacher__name">{t.name}</h3>
                <div className="teacher__role">{t.role}</div>
                <p className="teacher__bio">{t.bio}</p>
              </div>
            </Reveal>
          ))}
        </div>
      </div>
    </section>
  );
}

/* ─────────────────────────────────────────────
   Gallery — horizontal scroll-snap strip.
   ───────────────────────────────────────────── */
function Gallery({ data }) {
  const imgs = data.images || [];
  return (
    <section id="studio" className="section section--dark gallery">
      <div className="wrap">
        <Reveal className="section__head section__head--dark">
          <div className="eyebrow-line eyebrow-line--dark">Studio · Recording &amp; Rehearsal</div>
          <h2 className="section__title">{data.title}</h2>
        </Reveal>
      </div>
      <div className="gallery__strip no-scrollbar">
        {imgs.map((im, i) => (
          <figure className="gallery__item" key={i}>
            <img src={im.src} alt={im.caption || ''} loading="lazy" />
            {im.caption && <figcaption className="gallery__cap">{im.caption}</figcaption>}
          </figure>
        ))}
      </div>
    </section>
  );
}

/* ─────────────────────────────────────────────
   Testimonials — auto-advancing carousel + swipe.
   ───────────────────────────────────────────── */
function Testimonials({ data }) {
  const items = data.items || [];
  const [idx, setIdx] = useState(0);
  const touchX = useRef(null);
  const n = items.length;

  const go = useCallback((d) => setIdx(i => (i + d + n) % n), [n]);

  useEffect(() => {
    if (n <= 1 || PREFERS_REDUCED) return;
    const t = setInterval(() => setIdx(i => (i + 1) % n), 6000);
    return () => clearInterval(t);
  }, [n]);

  if (!n) return null;

  const onTouchStart = e => { touchX.current = e.touches[0].clientX; };
  const onTouchEnd = e => {
    if (touchX.current === null) return;
    const dx = e.changedTouches[0].clientX - touchX.current;
    if (Math.abs(dx) > 50) go(dx < 0 ? 1 : -1);
    touchX.current = null;
  };

  return (
    <section className="section section--surface testimonials">
      <div className="wrap">
        <Reveal className="section__head section__head--center">
          <div className="eyebrow-line">Testimoni · Jawaban jujur</div>
          <h2 className="section__title">{data.title}</h2>
        </Reveal>

        <div className="testi" onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
          <div className="testi__viewport">
            <div className="testi__track" style={{ transform: 'translateX(-' + (idx * 100) + '%)' }}>
              {items.map((t, i) => (
                <figure className="testi__slide" key={i} aria-hidden={i !== idx}>
                  <blockquote className="testi__quote">“{t.quote}”</blockquote>
                  <figcaption className="testi__cite">
                    <span className="testi__avatar" aria-hidden>{initialsOf(t.name)}</span>
                    <span>
                      <span className="testi__name">{t.name}</span>
                      <span className="testi__meta">{t.meta}</span>
                    </span>
                  </figcaption>
                </figure>
              ))}
            </div>
          </div>

          {n > 1 && (
            <div className="testi__dots">
              {items.map((_, i) => (
                <button
                  key={i}
                  className={'testi__dot' + (i === idx ? ' is-active' : '')}
                  aria-label={'Testimoni ' + (i + 1)}
                  onClick={() => setIdx(i)}
                />
              ))}
            </div>
          )}
        </div>
      </div>
    </section>
  );
}

/* ─────────────────────────────────────────────
   Pricing — clean cards, one highlighted.
   ───────────────────────────────────────────── */
function Pricing({ data }) {
  const plans = data.plans || [];
  return (
    <section id="pricing" className="section pricing">
      <div className="wrap">
        <Reveal className="section__head section__head--center">
          <div className="eyebrow-line">Harga · Transparan</div>
          <h2 className="section__title">{data.title}</h2>
          <p className="section__intro">{data.intro}</p>
        </Reveal>

        <div className="pricing__grid">
          {plans.map((pl, i) => (
            <Reveal key={i} className={'plan' + (pl.highlight ? ' plan--hl' : '')} delay={i * 90}>
              {pl.highlight && <div className="plan__ribbon">Paling populer</div>}
              <div className="plan__name">{pl.name}</div>
              <div className="plan__price">{pl.price}</div>
              <div className="plan__unit">{pl.unit}</div>
              <ul className="plan__features">
                {(pl.features || []).map((f, j) => (
                  <li key={j}>
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden><polyline points="5 12 10 17 19 7" /></svg>
                    {f}
                  </li>
                ))}
              </ul>
              <a className={'btn btn--lg ' + (pl.highlight ? 'btn--primary' : 'btn--outline')} href={waLink('Halo IMS! Aku tertarik paket ' + pl.name + '.')}>
                Pilih {pl.name}
              </a>
            </Reveal>
          ))}
        </div>
        <p className="pricing__note">Session pertama gratis buat murid baru. Bayar setelah trial, lewat Xendit.</p>
      </div>
    </section>
  );
}

/* ─────────────────────────────────────────────
   FAQ — accessible accordion.
   ───────────────────────────────────────────── */
function FAQ({ data }) {
  const items = data.items || [];
  const [open, setOpen] = useState(0);
  return (
    <section id="faq" className="section section--surface faq">
      <div className="wrap faq__wrap">
        <Reveal className="faq__intro">
          <div className="eyebrow-line">FAQ · Jawaban jujur</div>
          <h2 className="section__title">{data.title}</h2>
          <p className="section__intro">
            Selain ini, WA aja langsung. <strong>{WA_DISPLAY}</strong>.
          </p>
          <a className="btn btn--primary" href={waLink('Halo IMS! Aku ada pertanyaan.')}>Tanya via WhatsApp</a>
        </Reveal>

        <div className="faq__list">
          {items.map((f, i) => {
            const isOpen = open === i;
            return (
              <div key={i} className={'faq__item' + (isOpen ? ' is-open' : '')}>
                <button
                  className="faq__q"
                  aria-expanded={isOpen}
                  onClick={() => setOpen(isOpen ? -1 : i)}
                >
                  <span className="faq__q-num" aria-hidden>0{i + 1}</span>
                  <span className="faq__q-text">{f.q}</span>
                  <span className="faq__q-icon" aria-hidden>{isOpen ? '−' : '+'}</span>
                </button>
                <div className="faq__a-wrap">
                  <div className="faq__a">{f.a}</div>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

/* ─────────────────────────────────────────────
   Final CTA — full-width dark.
   ───────────────────────────────────────────── */
function FinalCTA({ data }) {
  const btn = data.button || {};
  return (
    <section className="finalcta">
      <div className="finalcta__word" aria-hidden>MAIN.</div>
      <Reveal className="finalcta__inner">
        <div className="eyebrow-line eyebrow-line--dark">「 Kelas trial · Gratis buat murid baru 」</div>
        <h2 className="finalcta__title">{data.title}</h2>
        <p className="finalcta__sub">{data.subtitle}</p>
        <div className="finalcta__ctas">
          <a className="btn btn--white btn--lg" href={btn.url || waLink()}>{btn.label || 'Coba kelas gratis'}</a>
        </div>
      </Reveal>
    </section>
  );
}

/* ─────────────────────────────────────────────
   Footer + contact.
   ───────────────────────────────────────────── */
const SISTER_LOGOS = ['indiebrand', 'indiespace', 'indiekost', 'indiecafe'];

function Footer({ contact }) {
  const c = contact || {};
  const phoneDigits = (c.phone || '').replace(/[^0-9]/g, '');
  return (
    <footer className="footer">
      <div className="wrap">
        <div className="footer__top">
          <div className="footer__brand">
            <div className="footer__brand-row">
              <img src="assets/logos/ims.png" alt="INDIE Music" width="48" height="48" />
              <div>
                <div className="footer__brand-name">INDIE Music</div>
                <div className="footer__brand-sub">School &amp; Studio</div>
              </div>
            </div>
            <p className="footer__blurb">
              Sekolah musik anti-mainstream di Gading Serpong. Belajar musik yang rasanya kayak senang,
              bukan kewajiban. Guitar, piano, drum, bass, vocal — buat anak dan dewasa.
            </p>
            <div className="footer__sisters">
              {SISTER_LOGOS.map(b => (
                <img key={b} src={'assets/logos/' + b + '.png'} alt={b} width="30" height="30" loading="lazy" />
              ))}
              <span>Part of INDIE Group</span>
            </div>
          </div>

          <div className="footer__cols">
            <FooterCol title="Belajar" items={['Guitar', 'Piano', 'Drum', 'Bass', 'Vocal']} />
            <FooterCol title="Program" items={['Anak · Private', 'Anak · Group', 'Dewasa · Private', 'In-Home Private', 'Kelas trial']} />
            <div className="footer__col">
              <div className="footer__col-title">Kontak</div>
              <ul>
                {c.address && <li>{c.address}</li>}
                {c.phone && <li><a href={waLink('Halo IMS!')}>{c.phone} (WA)</a></li>}
                <li><a href="mailto:hello@indiemusic.id">hello@indiemusic.id</a></li>
                {c.hours && <li>{c.hours}</li>}
                {c.maps_url && <li><a href={c.maps_url} target="_blank" rel="noopener">Lihat di Maps ↗</a></li>}
                {c.instagram && <li><a href={c.instagram} target="_blank" rel="noopener">Instagram ↗</a></li>}
              </ul>
            </div>
          </div>
        </div>

        <div className="footer__bottom">
          <span>© 2026 INDIE Music School &amp; Studio</span>
          <span className="footer__dot">·</span>
          <span>Bagian dari <strong>INDIE Group</strong></span>
          <span className="footer__spacer" />
          <span className="jp footer__jp">音楽は、しあわせの始まり。</span>
        </div>
      </div>
    </footer>
  );
}

function FooterCol({ title, items }) {
  return (
    <div className="footer__col">
      <div className="footer__col-title">{title}</div>
      <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>
    </div>
  );
}

/* Floating WhatsApp button */
function WAFloat() {
  return (
    <a className="wa-float" href={waLink('Halo IMS! Aku mau tanya soal les musik.')} aria-label="Chat WhatsApp" target="_blank" rel="noopener">
      <svg width="26" height="26" viewBox="0 0 32 32" fill="currentColor" aria-hidden>
        <path d="M16 3C9 3 3.5 8.5 3.5 15.5c0 2.4.7 4.7 1.9 6.7L3 29l7-1.8c1.9 1 4 1.6 6 1.6 7 0 12.5-5.5 12.5-12.5S23 3 16 3zm0 22.7c-1.8 0-3.6-.5-5.1-1.4l-.4-.2-4.2 1.1 1.1-4.1-.2-.4a10 10 0 01-1.6-5.5C5.4 9.7 10.2 5 16 5s10.6 4.7 10.6 10.5S21.8 25.7 16 25.7zm5.8-7.9c-.3-.2-1.9-.9-2.2-1s-.5-.2-.7.2-.8 1-1 1.2-.4.3-.7.1a8.7 8.7 0 01-2.6-1.6 9.6 9.6 0 01-1.8-2.2c-.2-.3 0-.5.1-.7l.5-.6c.2-.2.2-.4.3-.6s0-.4 0-.6l-1-2.4c-.3-.6-.5-.5-.7-.5h-.6c-.2 0-.6.1-.9.4a3.6 3.6 0 00-1.1 2.7c0 1.6 1.2 3.2 1.3 3.4s2.3 3.6 5.6 5c.8.3 1.4.5 1.9.7.8.3 1.5.2 2 .1.6-.1 1.9-.8 2.2-1.5s.3-1.4.2-1.5-.3-.2-.6-.4z"/>
      </svg>
    </a>
  );
}

/* ─────────────────────────────────────────────
   App
   ───────────────────────────────────────────── */
function App() {
  const c = useContent();
  return (
    <React.Fragment>
      <NavBar content={c} />
      <main>
        <Hero data={c.hero} />
        <Marquee data={c.marquee} />
        <Instruments data={c.instruments} />
        <Stats data={c.stats} />
        <Method data={c.method} />
        <Teachers data={c.teachers} />
        <Gallery data={c.gallery} />
        <Testimonials data={c.testimonials} />
        <Pricing data={c.pricing} />
        <FAQ data={c.faq} />
        <FinalCTA data={c.cta} />
      </main>
      <Footer contact={c.contact} />
      <WAFloat />
    </React.Fragment>
  );
}

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