/**
 * Shirune Studio — seven behaviours, one after the other, nothing shared.
 *
 *   1. Scroll reveals        blocks rise into place as they enter the screen
 *   2. Header                veil thickens on scroll, bar hides going down
 *   3. Hero parallax         the hero content drifts down over one screen
 *   4. Word-by-word lede     the About paragraph lights up as it goes by
 *   5. Pinned project rail   vertical scroll becomes horizontal scroll
 *   6. Testimonials          the two arrows scroll the carousel one card
 *   7. Mobile menu           the panel slides in, with a focus trap
 *
 * Every one of them is an *addition*: remove this file and the page still
 * reads, still opens its FAQ, still switches its service tabs, and the two
 * carousels can still be swiped. Nothing here is load-bearing.
 */

(function () {
  "use strict";

  /* Anyone who asked their system for less motion gets the final state of
     everything, immediately. We check it once and branch on it below. */
  const calmer = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  /* Scroll handlers must not do layout work on every event. This wraps a
     function so it runs at most once per frame. */
  function parFrame(action) {
    let prevu = false;
    return function () {
      if (prevu) return;
      prevu = true;
      requestAnimationFrame(function () {
        prevu = false;
        action();
      });
    };
  }

  /* ======================================================================
     1. Scroll reveals
     ----------------------------------------------------------------------
     The trigger is measured on the *screen*, not on the element: a block
     taller than the window would never reach "30% of itself visible" and
     would stay invisible forever. `rootMargin` pulls the bottom edge up by
     15% of the viewport instead, so a block starts moving once it is a
     sixth of the way in.
     ====================================================================== */

  const aReveler = document.querySelectorAll("[data-reveal]");

  if (calmer || !("IntersectionObserver" in window)) {
    aReveler.forEach(function (bloc) { bloc.classList.add("is-in"); });
  } else {
    const guetteur = new IntersectionObserver(function (entrees) {
      entrees.forEach(function (entree) {
        if (!entree.isIntersecting) return;
        entree.target.classList.add("is-in");
        guetteur.unobserve(entree.target);
      });
    }, { rootMargin: "0px 0px -15% 0px", threshold: 0 });

    aReveler.forEach(function (bloc) { guetteur.observe(bloc); });

    /* Safety net: whatever is still hidden when the page bottom is reached
       is revealed. Without it, a block that never crosses the threshold —
       the last one on a short page, for instance — would stay invisible. */
    window.addEventListener("scroll", parFrame(function () {
      const bas = window.scrollY + window.innerHeight;
      if (bas < document.documentElement.scrollHeight - 4) return;
      aReveler.forEach(function (bloc) { bloc.classList.add("is-in"); });
    }), { passive: true });
  }

  /* ======================================================================
     2. Header
     ----------------------------------------------------------------------
     Two independent things:

     — `--veil` goes from 0 to 1 over the first 9% of the page. The
       stylesheet turns it into the bar's background and into the colour of
       the pill behind the links.
     — the bar slides out of the way while you scroll down and comes back
       as soon as you scroll up. The 8px dead zone keeps it still during
       the small jitter of a trackpad.
     ====================================================================== */

  const entete = document.querySelector("[data-header]");

  if (entete) {
    entete.classList.add("is-in");

    let dernier = window.scrollY;

    const suivreEntete = parFrame(function () {
      const y = window.scrollY;
      const hauteur = document.documentElement.scrollHeight - window.innerHeight;
      const voile = hauteur > 0 ? Math.min(1, y / (hauteur * 0.09)) : 0;
      entete.style.setProperty("--veil", voile.toFixed(3));

      if (Math.abs(y - dernier) > 8) {
        const descend = y > dernier && y > 120;
        entete.classList.toggle("is-hidden", descend);
        dernier = y;
      }
    });

    suivreEntete();
    window.addEventListener("scroll", suivreEntete, { passive: true });
  }

  /* ======================================================================
     3. Hero parallax
     ----------------------------------------------------------------------
     The hero content sinks by half its own height over the first screenful
     of scroll, which is what makes the photograph feel like it is standing
     still behind it.
     ====================================================================== */

  const heros = document.querySelector("[data-parallax]");

  if (heros && !calmer) {
    const glisserHeros = parFrame(function () {
      const avance = Math.min(1, window.scrollY / window.innerHeight);
      heros.style.transform = "translateY(" + (avance * 50).toFixed(2) + "%)";
    });

    glisserHeros();
    window.addEventListener("scroll", glisserHeros, { passive: true });
  }

  /* ======================================================================
     4. The lede, word by word
     ----------------------------------------------------------------------
     Each word is wrapped in a <span> and lit in turn while the paragraph
     travels from the bottom third of the screen to the top third. The text
     is in the HTML from the start — the script only splits it, so a reader
     without JavaScript gets the whole paragraph in plain black.
     ====================================================================== */

  const paragraphe = document.querySelector("[data-words]");

  if (paragraphe && !calmer) {
    const mots = paragraphe.textContent.trim().split(/\s+/);
    paragraphe.textContent = "";
    mots.forEach(function (mot, i) {
      const span = document.createElement("span");
      span.className = "word";
      span.textContent = mot;
      paragraphe.appendChild(span);
      if (i < mots.length - 1) paragraphe.appendChild(document.createTextNode(" "));
    });

    const spans = paragraphe.querySelectorAll(".word");

    const allumer = parFrame(function () {
      const boite = paragraphe.getBoundingClientRect();
      const haut = window.innerHeight * 0.75;   // the paragraph starts lighting here
      const bas = window.innerHeight * 0.3;     // and is fully lit here
      const avance = (haut - boite.top) / (haut - bas);
      const allumes = Math.round(Math.max(0, Math.min(1, avance)) * spans.length);

      spans.forEach(function (span, i) {
        span.classList.toggle("is-lit", i < allumes);
      });
    });

    allumer();
    window.addEventListener("scroll", allumer, { passive: true });
    window.addEventListener("resize", allumer);
  }

  /* ======================================================================
     5. The pinned project rail
     ----------------------------------------------------------------------
     `.projects__track` is five screens tall; the stage sticks to the top
     for the whole of it. We turn the progress through the track into the
     rail's `scrollLeft`.

     The distance is measured, not hard-coded: the original slides a fixed
     -1700px whatever the window is, which leaves a gap on the right of a
     wide screen. `scrollWidth - clientWidth` is exactly the distance that
     brings the last card to the right edge, at any width.
     ====================================================================== */

  const piste = document.querySelector("[data-track]");
  const scene = document.querySelector("[data-stage]");
  const rail = document.querySelector("[data-rail]");

  if (piste && scene && rail) {
    /* The pinning only exists on wide screens — below 992px the stylesheet
       makes the track auto-height and the rail is a plain carousel. */
    const large = window.matchMedia("(min-width: 992px)");

    /* Past roughly 2550px the six cards already fit side by side: there is
       nothing left to slide, so the track collapses to the height of the
       stage instead of holding the page still for five screens. */
    function ajusterPiste() {
      if (!large.matches) { piste.style.height = ""; return; }
      const aFaire = rail.scrollWidth - rail.clientWidth;
      piste.style.height = aFaire > 0 ? "" : scene.offsetHeight + "px";
    }

    const glisserRail = parFrame(function () {
      if (!large.matches) return;

      const course = piste.offsetHeight - window.innerHeight;
      if (course <= 0) return;

      const avance = Math.max(0, Math.min(1, -piste.getBoundingClientRect().top / course));
      rail.scrollLeft = avance * (rail.scrollWidth - rail.clientWidth);
    });

    ajusterPiste();
    glisserRail();
    window.addEventListener("scroll", glisserRail, { passive: true });
    window.addEventListener("resize", function () { ajusterPiste(); glisserRail(); });
  }

  /* ======================================================================
     6. Testimonials
     ----------------------------------------------------------------------
     Without this the two arrows are anchors that jump to the neighbouring
     slide, which already works. Here we upgrade them into buttons that
     scroll by exactly one card and wrap around at the ends.
     ====================================================================== */

  const carrousel = document.querySelector("[data-quotes]");
  const precedent = document.querySelector("[data-quotes-prev]");
  const suivant = document.querySelector("[data-quotes-next]");
  const etat = document.querySelector("[data-quotes-status]");

  if (carrousel && precedent && suivant) {
    const fiches = carrousel.children;

    function rangCourant() {
      const pas = fiches[1] ? fiches[1].offsetLeft - fiches[0].offsetLeft : 1;
      return Math.round(carrousel.scrollLeft / pas);
    }

    function allerA(rang) {
      const total = fiches.length;
      const cible = ((rang % total) + total) % total;   // wrap around both ways
      carrousel.scrollTo({
        left: fiches[cible].offsetLeft - fiches[0].offsetLeft,
        behavior: calmer ? "auto" : "smooth",
      });
    }

    [precedent, suivant].forEach(function (lien) {
      lien.setAttribute("role", "button");
      lien.addEventListener("click", function (evenement) {
        evenement.preventDefault();
        allerA(rangCourant() + (lien === suivant ? 1 : -1));
      });
    });

    if (etat) {
      const annoncer = parFrame(function () {
        etat.textContent = "Testimonial " + (rangCourant() + 1) + " of " + fiches.length;
      });
      annoncer();
      carrousel.addEventListener("scroll", annoncer, { passive: true });
    }
  }

  /* ======================================================================
     7. Mobile menu
     ----------------------------------------------------------------------
     The panel covers the screen, so while it is open the focus must stay
     inside it and Escape must close it.
     ====================================================================== */

  const bouton = document.querySelector("[data-burger]");
  const panneau = document.querySelector("[data-menu]");

  if (bouton && panneau) {
    const fermer = panneau.querySelector("[data-menu-close]");
    const liens = panneau.querySelectorAll("a, button");

    function basculer(ouvert) {
      panneau.classList.toggle("is-open", ouvert);
      bouton.setAttribute("aria-expanded", String(ouvert));
      document.body.style.overflow = ouvert ? "hidden" : "";
      if (ouvert) liens[0].focus();
      else bouton.focus();
    }

    bouton.addEventListener("click", function () { basculer(true); });
    if (fermer) fermer.addEventListener("click", function () { basculer(false); });

    panneau.addEventListener("click", function (evenement) {
      if (evenement.target.closest("a")) basculer(false);
    });

    document.addEventListener("keydown", function (evenement) {
      if (!panneau.classList.contains("is-open")) return;

      if (evenement.key === "Escape") { basculer(false); return; }
      if (evenement.key !== "Tab") return;

      const premier = liens[0];
      const dernierLien = liens[liens.length - 1];
      if (evenement.shiftKey && document.activeElement === premier) {
        evenement.preventDefault();
        dernierLien.focus();
      } else if (!evenement.shiftKey && document.activeElement === dernierLien) {
        evenement.preventDefault();
        premier.focus();
      }
    });
  }
})();
