/* ==========================================================================
   Tokora Studio — behaviours, one per block, in the order of the page.

     1.  Scroll reveal
     2.  Scroll-driven sections     (hero, flow, connect, steps)
     3.  Hero: scale and hover cards
     4.  Top bar: mega menu and drawer
     5.  Rail: current section and theme
     6.  Story: the generated component tiles
     7.  Library: word flip and marquees
     8.  Install: tabs and copy
     9.  Steps: video on click
     10. FAQ: replay the open animation

   Nothing here is required for the page to be readable. Every block starts
   by looking for its own markup and returns quietly if it is not there, so
   deleting a section from index.html never breaks the rest.
   ========================================================================== */

(() => {
  "use strict";

  const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
  const clamp = (v, min, max) => Math.min(max, Math.max(min, v));

  /* ------------------------------------------------------------------------
     1. Scroll reveal
     ------------------------------------------------------------------------
     Trigger at 15% from the bottom of the *viewport*, not of the element: a
     threshold expressed as a share of the element would leave anything
     taller than the screen hidden for good. */

  const revealables = document.querySelectorAll(
    "[data-reveal], .story-visual, .library-row"
  );

  if (revealables.length) {
    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (!entry.isIntersecting) continue;
          entry.target.classList.add("is-in");
          observer.unobserve(entry.target);
        }
      },
      { rootMargin: "0px 0px -15% 0px", threshold: 0 }
    );

    for (const el of revealables) {
      const delay = el.getAttribute("data-reveal-delay");
      if (delay) el.style.setProperty("--reveal-delay", delay);
      observer.observe(el);
    }

    // Safety net: whatever is still hidden when the page bottom is reached
    // gets revealed. Without it a block sitting in the last 15% of the page
    // could never cross the threshold.
    addEventListener(
      "scroll",
      () => {
        const reste = document.body.scrollHeight - scrollY - innerHeight;
        if (reste > 4) return;
        for (const el of revealables) el.classList.add("is-in");
      },
      { passive: true }
    );
  }

  /* ------------------------------------------------------------------------
     2. Scroll-driven sections
     ------------------------------------------------------------------------
     Four sections are taller than the screen and keep their content pinned
     while you scroll past them. Each one gets a --progress from 0 to 1,
     written once per frame, and the stylesheet does the rest. */

  const piste = [];

  const suivre = (element, appliquer) => {
    if (element) piste.push({ element, appliquer });
  };

  // How far we are through a section, 0 before it, 1 once its last pixel of
  // scroll budget is spent. The budget is the section height minus one
  // viewport, which is exactly what the sticky child covers.
  const avancement = (element) => {
    const boite = element.getBoundingClientRect();
    const budget = boite.height - innerHeight;
    if (budget <= 0) return boite.top <= 0 ? 1 : 0;
    return clamp(-boite.top / budget, 0, 1);
  };

  const hero = document.querySelector(".hero");
  suivre(hero, (p) => hero.style.setProperty("--progress", p.toFixed(4)));

  const connect = document.querySelector(".connect");
  suivre(connect, (p) => connect.style.setProperty("--progress", p.toFixed(4)));

  // The flow: six steps, one panel and one toolbar button per step.
  const flow = document.querySelector(".flow");
  const flowBoutons = [...document.querySelectorAll(".flow-bar-btn")];
  const flowPanneaux = [...document.querySelectorAll(".flow-panel")];
  if (flow && flowBoutons.length) {
    let courant = -1;
    suivre(flow, (p) => {
      const etape = clamp(Math.floor(p * flowBoutons.length), 0, flowBoutons.length - 1);
      if (etape === courant) return;
      courant = etape;
      flowBoutons.forEach((b, i) => b.classList.toggle("is-current", i === etape));
      flowPanneaux.forEach((panneau) =>
        panneau.classList.toggle("is-current", Number(panneau.dataset.step) === etape)
      );
    });
  }

  // The steps: the vertical scroll drags the track sideways. Below 745 px the
  // track is a plain carousel again, so the transform is left alone.
  const steps = document.querySelector(".steps");
  const track = document.querySelector(".steps-track");
  const statut = document.querySelector(".steps-status");
  if (steps && track) {
    const cartes = [...track.children];
    let annonce = -1;
    suivre(steps, (p) => {
      if (!matchMedia("(width >= 745px)").matches) {
        track.style.removeProperty("--x");
        return;
      }
      // The track is wider than its frame and overflows it, so its own
      // clientWidth is the full 6 600 px, not the visible slice. The
      // distance to cover is measured against the frame.
      const course = Math.max(0, track.scrollWidth - track.parentElement.clientWidth);
      track.style.setProperty("--x", `${-(course * p).toFixed(1)}px`);

      const index = clamp(Math.round(p * (cartes.length - 1)), 0, cartes.length - 1);
      if (statut && index !== annonce) {
        annonce = index;
        const titre = cartes[index].querySelector("h3");
        statut.textContent = `Step ${index + 1} of ${cartes.length} — ${titre ? titre.textContent : ""}`;
      }
    });

    // Same announcement when the carousel is swiped instead of scrolled.
    track.addEventListener(
      "scroll",
      () => {
        if (matchMedia("(width >= 745px)").matches && document.documentElement.classList.contains("js")) return;
        const index = clamp(
          Math.round((track.scrollLeft / (track.scrollWidth - track.clientWidth || 1)) * (cartes.length - 1)),
          0,
          cartes.length - 1
        );
        if (index === annonce || !statut) return;
        annonce = index;
        const titre = cartes[index].querySelector("h3");
        statut.textContent = `Step ${index + 1} of ${cartes.length} — ${titre ? titre.textContent : ""}`;
      },
      { passive: true }
    );
  }

  let programme = false;
  const jouer = () => {
    programme = false;
    for (const { element, appliquer } of piste) appliquer(avancement(element));
  };
  const planifier = () => {
    if (programme) return;
    programme = true;
    requestAnimationFrame(jouer);
  };

  if (piste.length) {
    addEventListener("scroll", planifier, { passive: true });
    addEventListener("resize", planifier);
    jouer();
  }

  /* ------------------------------------------------------------------------
     3. Hero: scale and hover cards
     ------------------------------------------------------------------------
     The hero is one scaled block: rings, badges and type all shrink by the
     same ratio so the composition never reflows. The stylesheet ships a
     stepped fallback per breakpoint; this refines it to the exact fit. */

  const heroInner = document.querySelector(".hero-inner");
  if (hero && heroInner) {
    const LARGEUR = 1440;  // the width the composition was drawn at
    const HAUTEUR = 760;   // the height it needs at that width

    const echelle = () => {
      if (!matchMedia("(width >= 745px)").matches) {
        hero.style.removeProperty("--hero-scale");
        return;
      }
      const dispo = heroInner.clientHeight;
      hero.style.setProperty(
        "--hero-scale",
        Math.min(innerWidth / LARGEUR, dispo / HAUTEUR).toFixed(4)
      );
    };
    echelle();
    addEventListener("resize", echelle);
  }

  // The badges introduce themselves one at a time while the hero is on
  // screen, the way a visitor would discover them by hovering.
  const badges = [...document.querySelectorAll(".orbit-badge")];
  if (badges.length && !reduced) {
    let index = 0;
    let minuteur = 0;

    const tourner = () => {
      badges.forEach((b, i) => b.classList.toggle("is-tip", i === index));
      index = (index + 1) % badges.length;
      minuteur = setTimeout(tourner, 2600);
    };

    const observer = new IntersectionObserver(
      ([entree]) => {
        if (entree.isIntersecting && !minuteur) {
          minuteur = setTimeout(tourner, 1200);
        } else if (!entree.isIntersecting) {
          clearTimeout(minuteur);
          minuteur = 0;
          badges.forEach((b) => b.classList.remove("is-tip"));
        }
      },
      { threshold: 0.2 }
    );
    if (hero) observer.observe(hero);

    // A real hover always wins over the tour.
    for (const badge of badges) {
      badge.addEventListener("pointerenter", () => {
        clearTimeout(minuteur);
        minuteur = 0;
        badges.forEach((b) => b.classList.remove("is-tip"));
      });
    }
  }

  /* ------------------------------------------------------------------------
     4. Top bar: mega menu and drawer
     ------------------------------------------------------------------------
     A hover menu needs two delays: a short one before opening, and a longer
     one before closing, so the pointer can cross the gap between the trigger
     and the panel without the panel vanishing under it. */

  const declencheur = document.querySelector(".menu-trigger");
  const panneau = document.querySelector(".menu-panel");

  if (declencheur && panneau) {
    const menu = declencheur.closest(".menu");
    let ouvrir = 0;
    let fermer = 0;

    const etat = (ouvert) => declencheur.setAttribute("aria-expanded", String(ouvert));

    menu.addEventListener("pointerenter", () => {
      clearTimeout(fermer);
      ouvrir = setTimeout(() => etat(true), 90);
    });
    menu.addEventListener("pointerleave", () => {
      clearTimeout(ouvrir);
      fermer = setTimeout(() => etat(false), 280);
    });
    declencheur.addEventListener("click", () => {
      etat(declencheur.getAttribute("aria-expanded") !== "true");
    });
    menu.addEventListener("focusout", (event) => {
      if (!menu.contains(event.relatedTarget)) etat(false);
    });
    addEventListener("keydown", (event) => {
      if (event.key === "Escape") etat(false);
    });

    // The four products swap the promo panel on the right.
    const entrees = [...panneau.querySelectorAll(".menu-item")];
    for (const entree of entrees) {
      const montrer = () => {
        for (const autre of entrees) {
          const promo = document.getElementById(autre.getAttribute("aria-controls"));
          const actif = autre === entree;
          autre.classList.toggle("is-current", actif);
          if (promo) promo.hidden = !actif;
        }
      };
      entree.addEventListener("pointerenter", montrer);
      entree.addEventListener("focus", montrer);
      entree.addEventListener("click", montrer);
    }
  }

  const bascule = document.querySelector(".drawer-toggle");
  const tiroir = document.getElementById("drawer");
  if (bascule && tiroir) {
    bascule.addEventListener("click", () => {
      const ouvert = bascule.getAttribute("aria-expanded") === "true";
      bascule.setAttribute("aria-expanded", String(!ouvert));
      tiroir.hidden = ouvert;
    });
    for (const lien of tiroir.querySelectorAll("a")) {
      lien.addEventListener("click", () => {
        bascule.setAttribute("aria-expanded", "false");
        tiroir.hidden = true;
      });
    }
  }

  /* ------------------------------------------------------------------------
     5. Rail: current section and theme
     ------------------------------------------------------------------------ */

  const railLiens = [...document.querySelectorAll(".rail-item")];

  // Two entries point at the same anchor — Type and Color both lead to the
  // foundations block — so only the first match is highlighted. Marking
  // both would show two current items at once.
  const premierPour = new Map();
  for (const lien of railLiens) {
    const href = lien.getAttribute("href");
    if (!premierPour.has(href)) premierPour.set(href, lien);
  }

  const cibles = [...premierPour.keys()]
    .map((href) => document.querySelector(href))
    .filter(Boolean);

  if (cibles.length) {
    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (!entry.isIntersecting) continue;
          const gagnant = premierPour.get(`#${entry.target.id}`);
          for (const lien of railLiens) {
            const actif = lien === gagnant;
            lien.classList.toggle("is-current", actif);
            if (actif) lien.setAttribute("aria-current", "page");
            else lien.removeAttribute("aria-current");
          }
        }
      },
      { rootMargin: "-30% 0px -60% 0px", threshold: 0 }
    );
    for (const cible of cibles) observer.observe(cible);
  }

  const boutonTheme = document.querySelector(".rail-theme");
  if (boutonTheme) {
    const etiquette = boutonTheme.querySelector(".rail-theme-label");
    const sombreParDefaut = matchMedia("(prefers-color-scheme: dark)").matches;

    const appliquer = (sombre) => {
      document.documentElement.dataset.theme = sombre ? "dark" : "light";
      boutonTheme.setAttribute("aria-pressed", String(sombre));
      if (etiquette) etiquette.textContent = sombre ? "Dark" : "Light";
      try {
        localStorage.setItem("tokora-theme", sombre ? "dark" : "light");
      } catch (erreur) {
        // Private browsing refuses localStorage; the toggle still works for
        // the current page, it just does not survive a reload.
      }
    };

    let memoire = null;
    try {
      memoire = localStorage.getItem("tokora-theme");
    } catch (erreur) {
      memoire = null;
    }
    appliquer(memoire ? memoire === "dark" : sombreParDefaut);

    boutonTheme.addEventListener("click", () => {
      appliquer(document.documentElement.dataset.theme !== "dark");
    });
  }

  /* ------------------------------------------------------------------------
     6. Story: the generated component tiles
     ------------------------------------------------------------------------
     Twenty-four decorative circles, each with its own flip delay. They carry
     no meaning — their container is aria-hidden — so building them here
     keeps 24 identical <span> out of index.html. */

  const ICONES = [
    "M4 6h16M4 12h16M4 18h10",
    "M12 4 22 20H2Z",
    "M4 9h16v6H4Z",
    "M5 12h14M13 6l6 6-6 6",
    "M6 6h12v12H6Z",
    "M12 3v18M3 12h18",
    "m5 13 4 4 10-10",
    "M4 7h16M4 12h10M4 17h16",
  ];

  const NS = "http://www.w3.org/2000/svg";

  const tuile = (index, retard) => {
    const boite = document.createElement("span");
    boite.style.animationDelay = `${retard}ms`;

    const svg = document.createElementNS(NS, "svg");
    svg.setAttribute("viewBox", "0 0 24 24");
    svg.setAttribute("aria-hidden", "true");
    svg.setAttribute("focusable", "false");

    const trace = document.createElementNS(NS, "path");
    trace.setAttribute("d", ICONES[index % ICONES.length]);
    trace.setAttribute("fill", "none");
    trace.setAttribute("stroke", "currentColor");
    trace.setAttribute("stroke-width", "1.6");
    trace.setAttribute("stroke-linecap", "round");
    trace.setAttribute("transform", "translate(4.6 4.6) scale(0.62)");

    svg.append(trace);
    boite.append(svg);
    return boite;
  };

  for (const grille of document.querySelectorAll(".tiles, .flow-components")) {
    const nombre = grille.classList.contains("tiles") ? 24 : 16;
    for (let i = 0; i < nombre; i++) grille.append(tuile(i, i * 60));
  }

  /* ------------------------------------------------------------------------
     7. Library: word flip and marquees
     ------------------------------------------------------------------------ */

  const rotatif = document.querySelector("[data-rotate]");
  if (rotatif && !reduced) {
    let mots = [];
    try {
      mots = JSON.parse(rotatif.dataset.rotate);
    } catch (erreur) {
      mots = [];
    }

    if (mots.length > 1) {
      let index = 0;
      let minuteur = 0;

      const tourner = () => {
        const sortant = rotatif.querySelector(".rotate-word");
        index = (index + 1) % mots.length;

        const entrant = document.createElement("span");
        entrant.className = "rotate-word is-in";
        entrant.textContent = mots[index];
        rotatif.append(entrant);

        sortant.classList.add("is-out");
        sortant.addEventListener("animationend", () => sortant.remove(), { once: true });
      };

      const observer = new IntersectionObserver(([entree]) => {
        if (entree.isIntersecting && !minuteur) minuteur = setInterval(tourner, 2400);
        else if (!entree.isIntersecting) {
          clearInterval(minuteur);
          minuteur = 0;
        }
      });
      observer.observe(rotatif);
    }
  }

  // Each strip is duplicated so translateX(-50%) lands exactly on the copy
  // and the loop has no seam. The speed is given in pixels per second, so a
  // longer strip takes proportionally longer instead of racing.
  for (const rangee of document.querySelectorAll(".library-row")) {
    const bande = rangee.querySelector(".library-strip");
    if (!bande) continue;

    const vitesse = Number(rangee.dataset.speed) || 60;
    const largeur = bande.scrollWidth;
    bande.append(...[...bande.children].map((n) => n.cloneNode(true)));
    bande.style.setProperty("--speed", `${Math.round(largeur / vitesse)}s`);
    bande.setAttribute("aria-hidden", "false");
  }

  /* ------------------------------------------------------------------------
     8. Install: tabs and copy
     ------------------------------------------------------------------------ */

  const onglets = [...document.querySelectorAll(".install-tab")];
  if (onglets.length) {
    const choisir = (onglet) => {
      for (const autre of onglets) {
        const cible = document.getElementById(autre.getAttribute("aria-controls"));
        const actif = autre === onglet;
        autre.classList.toggle("is-current", actif);
        autre.setAttribute("aria-selected", String(actif));
        if (cible) cible.hidden = !actif;
      }
    };
    for (const onglet of onglets) onglet.addEventListener("click", () => choisir(onglet));
  }

  const copier = document.querySelector(".install-copy");
  if (copier && navigator.clipboard) {
    copier.addEventListener("click", async () => {
      const ligne = document.querySelector(".install-line:not([hidden])");
      if (!ligne) return;
      try {
        await navigator.clipboard.writeText(ligne.textContent.replace(/^\$\s*/, ""));
        copier.classList.add("is-done");
        setTimeout(() => copier.classList.remove("is-done"), 1200);
      } catch (erreur) {
        // Clipboard access can be refused; the command stays selectable.
      }
    });
  }

  /* ------------------------------------------------------------------------
     9. Steps: video on click
     ------------------------------------------------------------------------
     276 KB has no business being downloaded before anyone asked to watch it,
     so the poster is a button and the <video> is built on click. */

  for (const bouton of document.querySelectorAll(".step-play")) {
    bouton.addEventListener("click", () => {
      const source = bouton.dataset.video;
      if (!source) return;

      const video = document.createElement("video");
      video.src = source;
      video.controls = true;
      video.autoplay = true;
      video.loop = true;
      video.muted = true;
      video.playsInline = true;
      video.setAttribute("aria-label", bouton.querySelector("img")?.alt || "");
      bouton.replaceWith(video);
      video.play().catch(() => {
        // Autoplay refused: the controls are there, the visitor can start it.
      });
    });
  }

  /* ------------------------------------------------------------------------
     10. FAQ: replay the open animation
     ------------------------------------------------------------------------
     Closing a <details> does not remove its content from the render tree —
     the browser hides it with content-visibility — so the CSS animation
     never fires a second time. Restarting it by hand is the only way. */

  for (const item of document.querySelectorAll(".faq-item")) {
    item.addEventListener("toggle", () => {
      if (!item.open) return;
      const reponse = item.querySelector(".faq-answer");
      if (!reponse) return;
      reponse.style.animation = "none";
      void reponse.offsetHeight; // forces the style to be recomputed
      reponse.style.animation = "";
    });
  }
})();
