/* app.jsx — Mini Scholars: one continuous scroll performance.
   Preloader typewriter → parallax hero w/ rising centerpiece → (chapters.jsx
   statement+stats, gallery) → cream content chapters. */
const { useState, useEffect, useRef } = React;

const clamp = (v, mn, mx) => Math.max(mn, Math.min(mx, v));
const smooth = (t) => t * t * (3 - 2 * t);

/* rAF-driven smooth scroll — behavior:'smooth' is a no-op in embedded previews */
let glideRaf = null;
function glideTo(top) {
  if (glideRaf) cancelAnimationFrame(glideRaf);
  const start = window.scrollY, dist = top - start;
  if (Math.abs(dist) < 2) return;
  const dur = clamp(Math.abs(dist) * 0.35, 350, 1100);
  const t0 = performance.now();
  const ease = (t) => 1 - Math.pow(1 - t, 3);
  const step = (now) => {
    const t = Math.min(1, (now - t0) / dur);
    window.scrollTo(0, start + dist * ease(t));
    glideRaf = t < 1 ? requestAnimationFrame(step) : null;
  };
  glideRaf = requestAnimationFrame(step);
  /* safety: if rAF stalls, land at the target */
  setTimeout(() => { if (glideRaf) { cancelAnimationFrame(glideRaf); glideRaf = null; window.scrollTo(0, top); } }, dur + 400);
}
/* user input interrupts a glide */
["wheel", "touchstart"].forEach((ev) =>
  window.addEventListener(ev, () => { if (glideRaf) { cancelAnimationFrame(glideRaf); glideRaf = null; } }, { passive: true })
);

/* ============================ PRELOADER ============================ */
const PRE_WORD = "Mini Scholars Learning Center";
function Preloader({ refEl, typed, cursorHidden }) {
  return (
    <div className="preloader" ref={refEl}>
      <img className="pre-logo" src="assets/logo-emblem.png" alt="Mini Scholars emblem" />
      <div className="pre-word">
        {PRE_WORD.split("").map((ch, i) => (
          <span key={i} className={"pre-letter" + (i < typed ? " show" : "")}>
            {ch === " " ? "\u00A0" : ch}
          </span>
        ))}
        <span className={"pre-cursor" + (cursorHidden ? " hide" : "")}></span>
      </div>
      <div className="pre-sub">Where First Steps Lead to Scholarship</div>
    </div>
  );
}

/* ============================ NAV ============================ */
function Nav({ onJump, onMenu, navElRef, progRef }) {
  return (
    <nav className="imm-nav" ref={navElRef}>
      <a className="imm-brand" onClick={() => glideTo(0)}>
        <span className="brand-mark" aria-hidden="true"></span>
        Mini Scholars
      </a>
      <div className="nav-links">
        <a className="nav-link" onClick={() => onJump("ch-programs")}>Programs</a>
        <a className="nav-link" onClick={() => onJump("ch-story")}>Story</a>
        <a className="nav-link" onClick={() => onJump("ch-gallery")}>Gallery</a>
        <a className="nav-link" onClick={() => onJump("ch-contact")}>Contact</a>
        <button className="nav-cta" onClick={() => { window.location.href = "Waitlist.html"; }}>Join Waitlist</button>
      </div>
      <button className="menu-toggle" aria-label="Menu" onClick={onMenu}>
        <span></span><span></span>
      </button>
      <div className="nav-progress" aria-hidden="true"><i ref={progRef}></i></div>
    </nav>
  );
}

/* ============================ HERO ============================ */
function Hero({ heroRef, textRef, centerRef, onJump }) {
  return (
    <section className="hero" ref={heroRef}>
      <div className="hero-blobs"><div className="blob b1"></div><div className="blob b2"></div></div>

      {/* rising centerpiece photo card */}
      <div className="centerpiece" ref={centerRef}>
        <div className="cp-frame">
          <img className="cp-img" src="assets/building-cutout-final.png" alt="Mini Scholars Learning Center — 9000 Old Santa Fe Rd" />
          <div className="cp-tag"><span className="dot"></span>9000 Old Santa Fe Rd · Kansas City</div>
        </div>
      </div>

      <div className="hero-text" ref={textRef}>
        <p className="hero-eyebrow">Kansas City Early Learning</p>
        <div className="hero-row">
          <h1 className="hero-display">Where First Steps<br />Lead to <span className="ital">Scholarship</span></h1>
          <p className="hero-aside">An early-learning center built with vision, warmth, and a tech-forward heart — for ages six weeks through five years.</p>
        </div>
        <p className="hero-sub-m">An early-learning center built with vision, warmth, and a tech-forward heart.</p>
        <div className="hero-cta-row">
          <button className="btn-solid" onClick={() => { window.location.href = "Waitlist.html"; }}>Join the Waitlist</button>
          <button className="btn-line" onClick={() => onJump("ch-programs")}>Explore Programs</button>
        </div>
      </div>

      <div className="scroll-hint"><span>Scroll</span><span className="bar"></span></div>
    </section>
  );
}

/* ============================ SERVICES (editorial chapter) ============================ */
const SERVICES = [
  { idx: "01", name: "High-Quality Early Education", id: "svc-edu", ph: "Wooden blocks photo", img: "assets/program-education.jpg",
    body: <>Curriculum aligned with <b>Missouri DESE standards</b>, enhanced by <b>AI-powered tools</b> and immersive sensory learning for infants through pre-K.</> },
  { idx: "02", name: "Organic Meal Program", id: "svc-meal", ph: "Fresh produce photo", img: "assets/program-meals.jpg",
    body: <>Locally sourced, <b>USDA-approved organic meals</b> designed by local <b>nutritionists</b> — wholesome, nutrient-rich, and made with care.</> },
  { idx: "03", name: "Parent Portal App", id: "svc-app", ph: "Parent on phone photo", img: "assets/program-app.jpg",
    body: <>Real-time updates on your child's <b>daily activities, meals and milestones</b> through our <b>secure parent portal and mobile app</b>.</> },
];
function ServicesChapter() {
  return (
    <section className="chapter" id="ch-programs">
      <div className="chapter-head reveal">
        <p className="chapter-eyebrow">What we offer</p>
        <h2 className="chapter-title">Programs &amp; <span className="ital">services</span></h2>
      </div>
      <div className="svc-list">
        {SERVICES.map((s) => (
          <div className="svc-row reveal" key={s.id}>
            <div className="svc-idx">{s.idx}</div>
            <div className="svc-name">{s.name}</div>
            <div className="svc-desc">{s.body}</div>
            <div className="svc-media"><image-slot id={s.id} shape="rounded" radius="14" fit="cover" src={s.img} placeholder={s.ph}></image-slot></div>
          </div>
        ))}
      </div>
    </section>
  );
}

/* ============================ APP ============================ */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "motion": "balanced",
  "accent": "#D4967D"
}/*EDITMODE-END*/;

const MOTION = { calm: 0.55, balanced: 1, cinematic: 1.5 };

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [typed, setTyped] = useState(0);
  const [cursorHidden, setCursorHidden] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);

  const preRef = useRef(), heroRef = useRef(), textRef = useRef(), centerRef = useRef();
  const storyRef = useRef(), galleryRef = useRef();
  const cpAnim = useRef({ ty: 0, sc: 1, op: 1 });
  const gTrack = useRef(null);
  const navElRef = useRef(), progRef = useRef();
  const st = useRef({ started: false });
  const magRef = useRef(MOTION.balanced);
  useEffect(() => { magRef.current = MOTION[t.motion] || 1; }, [t.motion]);

  /* accent */
  useEffect(() => {
    const r = document.documentElement.style;
    r.setProperty("--terra", t.accent);
    r.setProperty("--accent", t.accent);
  }, [t.accent]);

  /* preloader choreography */
  useEffect(() => {
    const CHAR = 58, START = 420;
    const timers = [];
    for (let i = 1; i <= PRE_WORD.length; i++)
      timers.push(setTimeout(() => setTyped(i), START + i * CHAR));
    const LIFT = START + PRE_WORD.length * CHAR + 650;
    timers.push(setTimeout(() => setCursorHidden(true), LIFT - 180));
    timers.push(setTimeout(() => {
      preRef.current && preRef.current.classList.add("lift");
      centerRef.current && centerRef.current.classList.add("rise");
    }, LIFT));
    timers.push(setTimeout(() => { textRef.current && textRef.current.classList.add("in"); }, LIFT + 1200));
    timers.push(setTimeout(() => {
      preRef.current && preRef.current.classList.add("done");
      centerRef.current && centerRef.current.classList.add("scrollctl");
      st.current.started = true;
      render();
    }, LIFT + 2050));
    return () => timers.forEach(clearTimeout);
  }, []);

  /* core scroll render */
  const render = () => {
    const mag = magRef.current;
    const y = window.scrollY;
    const vh = window.innerHeight;

    /* nav color: white while a dark chapter sits at the viewport top */
    let onDark = false;
    [storyRef, galleryRef].forEach((r) => {
      const el = r.current; if (!el) return;
      const rc = el.getBoundingClientRect();
      if (rc.top <= 60 && rc.bottom > 80) onDark = true;
    });
    document.documentElement.style.setProperty("--nav-color", onDark ? "#F4EFE6" : "#566661");

    /* nav presence + reading-progress indicator */
    if (navElRef.current) {
      navElRef.current.classList.toggle("scrolled", y > vh * 0.6);
      navElRef.current.classList.toggle("on-dark", onDark);
    }
    if (progRef.current) {
      const docMax = document.documentElement.scrollHeight - vh;
      progRef.current.style.width = (clamp(y / (docMax || 1), 0, 1) * 100).toFixed(2) + "%";
    }

    /* hero parallax: blobs drift, centerpiece rises + scales then fades */
    const hero = heroRef.current;
    if (hero) {
      const hp = clamp(y / (hero.offsetHeight || vh), 0, 1);
      const e = smooth(hp);
      if (st.current.started && centerRef.current) {
        /* heavier, slower response: an eased fade with inertia — the building
           dissolves before the next chapter reaches it, instead of being
           swallowed by it. Lerped state gives it physical weight. */
        const f = smooth(clamp((hp - 0.16) / 0.42, 0, 1));   /* idle until ~16%, fully gone by ~58% */
        const t = cpAnim.current;
        const tgt = { ty: -f * 7 * mag, sc: 1 + f * 0.08 * mag, op: 1 - f };
        t.ty += (tgt.ty - t.ty) * 0.1;
        t.sc += (tgt.sc - t.sc) * 0.1;
        t.op += (tgt.op - t.op) * 0.1;
        centerRef.current.style.transform = `translateX(-50%) translateY(${t.ty}vh) scale(${t.sc})`;
        centerRef.current.style.opacity = clamp(t.op, 0, 1);
      }
      if (textRef.current && st.current.started) {
        textRef.current.style.transform = `translateY(${-e * 8 * mag}vh)`;
        textRef.current.style.opacity = clamp(1 - hp / 0.55, 0, 1);
      }
    }

    /* gallery: pinned horizontal scrub — cards slide sideways as you scroll
       through the 320vh wrap; each photo counter-drifts for parallax depth */
    const gal = galleryRef.current;
    if (gal) {
      if (!gTrack.current) gTrack.current = gal.querySelector(".g-track");
      const track = gTrack.current;
      if (track && track.children.length) {
        const total = gal.offsetHeight - vh;
        const p = clamp(-gal.getBoundingClientRect().top / (total || 1), 0, 1);
        const cards = track.children;
        const cw = cards[0].offsetWidth;
        const gap = parseFloat(getComputedStyle(track).columnGap || getComputedStyle(track).gap) || 28;
        const dist = (cards.length - 1) * (cw + gap);
        track.style.transform = `translate3d(${-p * dist}px, 0, 0)`;
        for (let i = 0; i < cards.length; i++) {
          const slot = cards[i].querySelector("image-slot");
          if (slot) {
            const off = clamp((p * dist - i * (cw + gap)) * 0.07 * mag, -46, 46);
            slot.style.transform = `translateX(${off}px) scale(1.12)`;
          }
        }
      }
    }

    /* statement pin choreography: lines brighten in, stats cascade, photo drifts */
    const sw = storyRef.current;
    if (sw) {
      const total = sw.offsetHeight - vh;
      const p = clamp(-sw.getBoundingClientRect().top / (total || 1), 0, 1);
      sw.querySelectorAll(".st-line").forEach((el, i) => {
        const tt = clamp((p - (0.04 + i * 0.13)) / 0.12, 0, 1);
        el.style.opacity = 0.38 + 0.62 * tt;
        el.style.transform = `translateY(${(1 - tt) * 14}px)`;
      });
      sw.querySelectorAll(".st-stat").forEach((el, i) => {
        const tt = clamp((p - (0.42 + i * 0.07)) / 0.14, 0, 1);
        el.style.opacity = 0.32 + 0.68 * tt;
        el.style.transform = `translateY(${(1 - tt) * 22}px)`;
      });
      const ph = sw.querySelector(".st-photo");
      if (ph && getComputedStyle(ph).display !== "none") {
        /* final act: text lifts away, the photo's box grows (object-fit
           re-crops live, revealing the full aerial), stopping shy of the
           edges — then fades away as the gallery arrives */
        const p2 = clamp((p - 0.7) / 0.26, 0, 1);
        const slot = ph.querySelector("image-slot");
        if (slot) slot.style.transform = "";
        const stMain = sw.querySelector(".st-main");
        const ensureFlow = () => {
          if (st.current.phHolder) { st.current.phHolder.remove(); st.current.phHolder = null; }
          ph.style.position = ""; ph.style.left = ""; ph.style.top = "";
          ph.style.width = ""; ph.style.height = ""; ph.style.zIndex = ""; ph.style.margin = "";
        };
        /* fade as the gallery slides in — same vanishing act as the hero photo */
        const galEl = galleryRef.current;
        const fade = galEl
          ? clamp((galEl.getBoundingClientRect().top - vh * 0.25) / (vh * 0.55), 0, 1)
          : 1;
        ph.style.opacity = fade;
        ph.style.visibility = fade <= 0.01 ? "hidden" : "";
        if (p2 <= 0) {
          ensureFlow();
          ph.style.transform = `translateY(${(0.5 - p) * 5 * mag}vh)`;
          ph.style.borderRadius = "";
          if (stMain) { stMain.style.transform = ""; stMain.style.opacity = ""; }
        } else {
          const e3 = smooth(p2);
          /* text slides up and out — the photo takes the stage beneath it */
          if (stMain) {
            stMain.style.transform = `translateY(${-e3 * 60}vh)`;
            stMain.style.opacity = clamp(1 - e3 * 1.6, 0, 1);
          }
          let base = st.current.phBase;
          if (!base || st.current.phBaseW !== window.innerWidth || st.current.phBaseH !== vh) {
            ensureFlow();
            const ptf = ph.style.transform;
            ph.style.transform = "none";
            const b = ph.getBoundingClientRect();
            base = st.current.phBase = { left: b.left, top: b.top, w: b.width, h: b.height };
            st.current.phBaseW = window.innerWidth; st.current.phBaseH = vh;
            ph.style.transform = ptf;
          }
          /* swap into fixed positioning ONCE (a placeholder keeps the grid
             steady) — no per-frame layout feedback, no shake */
          if (!st.current.phHolder) {
            const holder = document.createElement("div");
            holder.style.height = base.h + "px";
            ph.parentNode.insertBefore(holder, ph);
            st.current.phHolder = holder;
            ph.style.position = "fixed";
            ph.style.zIndex = "4";
            ph.style.margin = "0";
            ph.style.transform = "none";
          }
          const R = st.current.phImgR || 16 / 9;
          /* box morphs from portrait frame to a near-full-width rounded card —
             stopping ~half an inch off the viewport edges */
          const M = 48; /* edge margin */
          const targetW = window.innerWidth - M * 2;
          const W = base.w + (targetW - base.w) * e3;
          const targetH = Math.min(targetW / R, vh - M * 2);
          const Hh = base.h + (targetH - base.h) * e3;
          const cx = (base.left + base.w / 2) + (window.innerWidth / 2 - (base.left + base.w / 2)) * e3;
          const cy = (base.top + base.h / 2) + (vh / 2 - (base.top + base.h / 2)) * e3;
          ph.style.width = W + "px";
          ph.style.height = Hh + "px";
          ph.style.left = (cx - W / 2) + "px";
          ph.style.top = (cy - Hh / 2) + "px";
          /* corners stay soft — rounding slightly up as it grows */
          ph.style.borderRadius = (18 + 10 * e3) + "px";
        }
      }
    }
  };

  /* size the centerpiece so it crests exactly below the hero text — never overlapping */
  useEffect(() => {
    const sizeCard = () => {
      const txt = textRef.current, cp = centerRef.current;
      if (!txt || !cp) return;
      const bottom = txt.offsetTop + txt.offsetHeight;
      const h = Math.max(150, window.innerHeight - bottom - 28);
      cp.style.height = Math.min(h, window.innerHeight * 0.62) + "px";
    };
    sizeCard();
    const t1 = setTimeout(sizeCard, 600), t2 = setTimeout(sizeCard, 2500);
    window.addEventListener("resize", sizeCard);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(sizeCard);
    return () => { clearTimeout(t1); clearTimeout(t2); window.removeEventListener("resize", sizeCard); };
  }, []);

  /* natural aspect ratio of the statement aerial (for the expansion finale) */
  useEffect(() => {
    const im = new Image();
    im.onload = () => { st.current.phImgR = im.naturalWidth / im.naturalHeight; };
    im.src = "assets/story-aerial.jpg";
  }, []);

  /* scroll + raf loop (scroll is source of truth; raf smooths) */
  useEffect(() => {
    const onScroll = () => render();
    const onResize = () => render();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onResize);
    let raf;
    const loop = () => { render(); raf = requestAnimationFrame(loop); };
    raf = requestAnimationFrame(loop);
    return () => { cancelAnimationFrame(raf); window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onResize); };
  }, []);

  /* reveal-on-scroll */
  useEffect(() => {
    const io = new IntersectionObserver((ents) => {
      ents.forEach((en) => { if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); } });
    }, { threshold: 0.14 });
    const q = () => document.querySelectorAll(".reveal:not(.in)").forEach((el) => io.observe(el));
    q(); const tt = setTimeout(q, 400);
    return () => { clearTimeout(tt); io.disconnect(); };
  }, []);

  const jump = (id) => {
    setMenuOpen(false);
    const el = document.getElementById(id);
    if (el) glideTo(el.getBoundingClientRect().top + window.scrollY - 70);
  };

  return (
    <>
      <Preloader refEl={preRef} typed={typed} cursorHidden={cursorHidden} />
      <Nav onJump={jump} onMenu={() => setMenuOpen((v) => !v)} navElRef={navElRef} progRef={progRef} />
      {menuOpen && (
        <div className="mobile-menu">
          <a onClick={() => jump("ch-programs")}>Programs</a>
          <a onClick={() => jump("ch-story")}>Story</a>
          <a onClick={() => jump("ch-gallery")}>Gallery</a>
          <a onClick={() => jump("ch-contact")}>Contact</a>
          <button className="btn-solid" onClick={() => { window.location.href = "Waitlist.html"; }}>Join the Waitlist</button>
        </div>
      )}

      <Hero heroRef={heroRef} textRef={textRef} centerRef={centerRef} onJump={jump} />

      <div id="ch-story"><StatementStats refEl={storyRef} /></div>
      <div id="ch-gallery"><HoverGallery refEl={galleryRef} /></div>

      <main className="content-wrap">
        <ServicesChapter />
        <div id="ch-waitlist" style={{ position: "relative" }}><Blobs /><WaitlistPolicies onJoin={() => { window.location.href = "Waitlist.html"; }} /></div>
        <div id="ch-contact"><ContactForm panel /></div>
        <SiteFooter />
      </main>

      <TweaksPanel>
        <TweakSection label="Motion" />
        <TweakRadio label="Parallax" value={t.motion} options={["calm", "balanced", "cinematic"]}
          onChange={(v) => setTweak("motion", v)} />
        <TweakSection label="Brand" />
        <TweakColor label="Accent" value={t.accent}
          options={["#D4967D", "#C5805F", "#B5746A", "#A88A6B"]}
          onChange={(v) => setTweak("accent", v)} />
      </TweaksPanel>
    </>
  );
}

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