/**
 * Nomadra — behaviours.
 *
 * One block per behaviour, in the order they appear on the page. Nothing here
 * is required to read the site: every hidden state lives under `html.js`, and
 * this file is what removes it. Delete this file and the page still works.
 *
 * The numbers (durations, delays, stagger amounts, easing) come from the
 * animation data of the reference, not from taste. They are listed in the
 * README next to the originals.
 */

(function () {
  "use strict";

  var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  /* =======================================================================
     1. Scroll reveals
     -----------------------------------------------------------------------
     The trigger is 15% of the *viewport* from the bottom, never a share of
     the element: a percentage of the element would leave anything taller
     than the window hidden for ever.

     The safety net at the end handles the last screen of the page, which can
     never travel 15% past the fold.
     ======================================================================= */

  var revealTargets = document.querySelectorAll("[data-reveal], [data-split], [data-curtain], [data-counter], .story__photo");

  function show(el) {
    if (el.classList.contains("is-in")) return;
    var delay = el.getAttribute("data-reveal-delay");
    if (delay && !reduced) el.style.setProperty("--reveal-delay", delay + "ms");
    el.classList.add("is-in");
  }

  if ("IntersectionObserver" in window) {
    var revealObserver = new IntersectionObserver(
      function (entries) {
        entries.forEach(function (entry) {
          if (!entry.isIntersecting) return;
          show(entry.target);
          revealObserver.unobserve(entry.target);
        });
      },
      { rootMargin: "0px 0px -15% 0px", threshold: 0 }
    );
    revealTargets.forEach(function (el) {
      revealObserver.observe(el);
    });

    // Safety net: at the bottom of the page, reveal whatever is left.
    window.addEventListener(
      "scroll",
      function () {
        if (window.scrollY + window.innerHeight < document.body.scrollHeight - 4) return;
        revealTargets.forEach(show);
      },
      { passive: true }
    );
  } else {
    revealTargets.forEach(show);
  }

  /* =======================================================================
     2. Split text
     -----------------------------------------------------------------------
     Headings are cut into characters, paragraphs into lines. The original
     staggered by a total amount rather than a per-item step: 0.5s spread
     over every character of a title, 0.2s over the lines of a paragraph.

     The cut version is hidden from assistive tech and the readable string is
     put back as an aria-label — a heading spelled letter by letter is worse
     than no animation at all.
     ======================================================================= */

  function wrapChars(el, totalStagger) {
    var text = el.textContent.replace(/\s+/g, " ").trim();
    el.setAttribute("aria-label", text);
    el.textContent = "";

    var holder = document.createElement("span");
    holder.setAttribute("aria-hidden", "true");

    var chars = [];
    text.split(" ").forEach(function (word, wordIndex, words) {
      // One wrapper per word so a line break never falls inside a word.
      var wordEl = document.createElement("span");
      wordEl.className = "split__word";
      wordEl.style.display = "inline-block";
      wordEl.style.whiteSpace = "nowrap";

      word.split("").forEach(function (char) {
        var span = document.createElement("span");
        span.className = "split__char";
        span.textContent = char;
        wordEl.appendChild(span);
        chars.push(span);
      });

      holder.appendChild(wordEl);
      if (wordIndex < words.length - 1) holder.appendChild(document.createTextNode(" "));
    });

    el.appendChild(holder);

    var last = chars.length - 1 || 1;
    chars.forEach(function (span, i) {
      span.style.setProperty("--stagger", Math.round((i / last) * totalStagger) + "ms");
    });
    return chars;
  }

  function wrapLines(el, totalStagger, masked) {
    var text = el.textContent.replace(/\s+/g, " ").trim();
    el.setAttribute("aria-label", text);
    el.textContent = "";

    var holder = document.createElement("span");
    holder.setAttribute("aria-hidden", "true");
    holder.style.display = "block";

    // First pass: one span per word, so we can read where the browser broke.
    var words = text.split(" ").map(function (word, i, all) {
      var span = document.createElement("span");
      span.style.display = "inline-block";
      span.textContent = word + (i < all.length - 1 ? " " : "");
      holder.appendChild(span);
      return span;
    });
    el.appendChild(holder);

    // Second pass: group the words that share a top offset.
    var lines = [];
    var currentTop = null;
    words.forEach(function (span) {
      var top = span.offsetTop;
      if (currentTop === null || Math.abs(top - currentTop) > 2) {
        lines.push([]);
        currentTop = top;
      }
      lines[lines.length - 1].push(span.textContent);
    });

    holder.textContent = "";
    var lineEls = lines.map(function (parts) {
      var line = document.createElement("span");
      line.className = "split__line";
      line.textContent = parts.join("");
      if (masked) {
        var mask = document.createElement("span");
        mask.className = "split__mask";
        mask.appendChild(line);
        holder.appendChild(mask);
      } else {
        holder.appendChild(line);
      }
      return line;
    });

    var last = lineEls.length - 1 || 1;
    lineEls.forEach(function (line, i) {
      line.style.setProperty("--stagger", Math.round((i / last) * totalStagger) + "ms");
    });
  }

  var scrubbed = [];

  document.querySelectorAll("[data-split]").forEach(function (el) {
    var kind = el.getAttribute("data-split");
    if (kind === "lines") {
      wrapLines(el, 200, true);
    } else if (el.hasAttribute("data-split-scrub")) {
      // The story paragraph lights up letter by letter as it crosses the
      // screen, so it needs its characters but no entrance stagger.
      scrubbed.push({ el: el, chars: wrapChars(el, 0) });
    } else {
      wrapChars(el, el.hasAttribute("data-split-mask") ? 500 : 500);
    }
  });

  /* =======================================================================
     3. The paragraph tied to the scroll position
     -----------------------------------------------------------------------
     Reference: start "top bottom", end "70% 50%", scrub 0.8, characters from
     20% to full opacity. Read on scroll, written once per frame.
     ======================================================================= */

  if (scrubbed.length && !reduced) {
    var scrubTicking = false;

    function paintScrub() {
      scrubTicking = false;
      scrubbed.forEach(function (item) {
        var box = item.el.getBoundingClientRect();
        var height = window.innerHeight;
        // 0 when the block enters from the bottom, 1 when its 70% mark
        // reaches the middle of the screen.
        var span = height + box.height * 0.7 - height * 0.5;
        var progress = (height - box.top) / span;
        progress = Math.max(0, Math.min(1, progress));

        var lit = Math.round(progress * item.chars.length);
        item.chars.forEach(function (char, i) {
          char.classList.toggle("is-lit", i < lit);
        });
      });
    }

    window.addEventListener(
      "scroll",
      function () {
        if (scrubTicking) return;
        scrubTicking = true;
        requestAnimationFrame(paintScrub);
      },
      { passive: true }
    );
    paintScrub();
  }

  /* =======================================================================
     4. Counters
     -----------------------------------------------------------------------
     "189K+" becomes one reel per character. A digit reel holds 0-9 followed
     by the digit itself, so it makes a full turn before landing; anything
     else is a one-item reel that never moves.
     ======================================================================= */

  document.querySelectorAll("[data-counter]").forEach(function (el) {
    var value = el.textContent.trim();
    el.setAttribute("aria-label", value);
    el.textContent = "";

    var holder = document.createElement("span");
    holder.setAttribute("aria-hidden", "true");
    holder.style.display = "flex";

    value.split("").forEach(function (char, i) {
      var reel = document.createElement("span");
      reel.className = "counter__reel";

      var strip = document.createElement("span");
      strip.className = "counter__digits";

      var faces = /[0-9]/.test(char) ? "0123456789".split("").concat(char) : [char];
      faces.forEach(function (face) {
        var cell = document.createElement("span");
        cell.className = "counter__digit";
        cell.textContent = face;
        strip.appendChild(cell);
      });

      if (faces.length > 1) {
        strip.style.setProperty("--stop", "-" + (faces.length - 1) + "em");
        strip.style.setProperty("--stagger", i * 80 + "ms");
      }

      reel.appendChild(strip);
      holder.appendChild(reel);
    });

    el.appendChild(holder);
  });

  /* =======================================================================
     5. Menu
     ======================================================================= */

  var navToggle = document.querySelector("[data-nav-toggle]");
  var nav = document.querySelector("[data-nav]");

  if (navToggle && nav) {
    navToggle.addEventListener("click", function () {
      var open = nav.classList.toggle("is-open");
      navToggle.setAttribute("aria-expanded", String(open));
      navToggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
    });

    nav.addEventListener("click", function (event) {
      if (event.target.closest("a")) {
        nav.classList.remove("is-open");
        navToggle.setAttribute("aria-expanded", "false");
        navToggle.setAttribute("aria-label", "Open menu");
      }
    });
  }

  /* =======================================================================
     6. Destinations
     -----------------------------------------------------------------------
     While the block is pinned, one progress value between 0 and 1 drives
     everything: the list slides up by 60% of its own height and the index
     entry matching the progress lights up. Above 991px only — below that the
     stylesheet takes over with a plain CSS loop.
     ======================================================================= */

  var places = document.querySelector("[data-places]");

  if (places) {
    var entries = places.querySelectorAll(".places__entry");
    var pinned = window.matchMedia("(min-width: 992px)");
    var placesTicking = false;

    function paintPlaces() {
      placesTicking = false;
      if (!pinned.matches) {
        places.style.removeProperty("--progress");
        return;
      }

      var box = places.getBoundingClientRect();
      var travel = box.height - window.innerHeight;
      var progress = travel > 0 ? -box.top / travel : 0;
      progress = Math.max(0, Math.min(1, progress));
      places.style.setProperty("--progress", progress.toFixed(4));

      var active = Math.min(entries.length - 1, Math.floor(progress * entries.length));
      entries.forEach(function (entry, i) {
        entry.classList.toggle("is-active", i === active);
      });
    }

    window.addEventListener(
      "scroll",
      function () {
        if (placesTicking) return;
        placesTicking = true;
        requestAnimationFrame(paintPlaces);
      },
      { passive: true }
    );
    window.addEventListener("resize", paintPlaces);
    paintPlaces();
  }

  /* =======================================================================
     7. Gallery — the coverflow
     -----------------------------------------------------------------------
     Reference settings: 2.8s between slides, 0.85s of travel on a
     power3.inOut curve (the CSS calls it --ease-in-out-quart), the middle
     card at 120% height, its neighbours at 92%/108%, the ones beyond at
     84%/96%, and everything past the third one faded out.

     The markup is a scroll-snap strip. We only switch to absolute placement
     once we know the script is running, so a reader without JS keeps a strip
     it can swipe.
     ======================================================================= */

  var gallery = document.querySelector("[data-gallery]");

  if (gallery) {
    var track = gallery.querySelector("[data-gallery-track]");
    var shots = Array.prototype.slice.call(track.querySelectorAll(".shot"));
    var dotsBox = gallery.querySelector("[data-gallery-dots]");
    var status = gallery.querySelector("[data-gallery-status]");
    var active = Math.floor(shots.length / 2);
    var timer = null;

    // Base size of a card, read from the stylesheet before we take the cards
    // out of the flow.
    var base = shots[0].getBoundingClientRect();
    var baseW = base.width;
    var baseH = base.height;

    function settings() {
      var narrow = window.innerWidth <= 767;
      var mid = window.innerWidth <= 991;
      return {
        gap: narrow ? 28 : mid ? 30 : 32,
        activeH: narrow ? 1.1 : mid ? 1.14 : 1.2,
        sideW: narrow ? 0.9 : 0.92,
        sideH: narrow ? 1 : mid ? 1.04 : 1.08,
        farW: narrow ? 0.82 : 0.84,
        farH: narrow ? 0.88 : mid ? 0.92 : 0.96,
        activeY: narrow ? -8 : -16,
        sideY: narrow ? -4 : -10,
        farY: narrow ? 0 : -4,
      };
    }

    function sizeAt(distance, cfg) {
      if (distance === 0) return { w: baseW, h: baseH * cfg.activeH, y: cfg.activeY };
      if (distance === 1) return { w: baseW * cfg.sideW, h: baseH * cfg.sideH, y: cfg.sideY };
      return { w: baseW * cfg.farW, h: baseH * cfg.farH, y: distance === 2 ? cfg.farY : 0 };
    }

    // Distance on a ring: card 0 sits next to card n-1.
    function ringDistance(index) {
      var d = index - active;
      var half = shots.length / 2;
      if (d > half) d -= shots.length;
      if (d < -half) d += shots.length;
      return d;
    }

    function offsetX(distance, cfg) {
      if (distance === 0) return 0;
      var sign = distance < 0 ? -1 : 1;
      var steps = Math.abs(distance);
      var x = 0;
      for (var step = 1; step <= steps; step++) {
        x += sizeAt(step - 1, cfg).w / 2 + sizeAt(step, cfg).w / 2 + cfg.gap;
      }
      return sign * x;
    }

    function paintGallery() {
      var cfg = settings();
      shots.forEach(function (shot, i) {
        var distance = ringDistance(i);
        var far = Math.abs(distance);
        var size = sizeAt(far, cfg);
        shot.style.setProperty("--x", Math.round(offsetX(distance, cfg)) + "px");
        shot.style.setProperty("--y", size.y + "px");
        shot.style.setProperty("--w", Math.round(size.w) + "px");
        shot.style.setProperty("--h", Math.round(size.h) + "px");
        shot.style.setProperty("--o", far > 3 ? 0 : far === 3 ? 0.35 : 1);
        shot.style.setProperty("--z", far === 0 ? 30 : 20 - far);
        shot.classList.toggle("is-active", far === 0);
      });

      dotsBox.querySelectorAll(".gallery__dot").forEach(function (dot, i) {
        dot.setAttribute("aria-selected", String(i === active));
        dot.setAttribute("tabindex", i === active ? "0" : "-1");
      });

      if (status) {
        status.textContent = "Photo " + (active + 1) + " of " + shots.length + ": " +
          shots[active].querySelector(".shot__tag").textContent.trim();
      }
    }

    function goTo(index) {
      active = (index + shots.length) % shots.length;
      paintGallery();
    }

    // Dots double as the keyboard control.
    shots.forEach(function (shot, i) {
      var dot = document.createElement("button");
      dot.type = "button";
      dot.className = "gallery__dot";
      dot.setAttribute("role", "tab");
      dot.setAttribute("aria-selected", String(i === active));
      dot.setAttribute("aria-label", "Show photo " + (i + 1));
      dot.addEventListener("click", function () {
        goTo(i);
        restart();
      });
      dotsBox.appendChild(dot);
    });

    dotsBox.addEventListener("keydown", function (event) {
      if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
      event.preventDefault();
      goTo(active + (event.key === "ArrowRight" ? 1 : -1));
      restart();
      dotsBox.querySelectorAll(".gallery__dot")[active].focus();
    });

    function restart() {
      window.clearInterval(timer);
      if (reduced) return;
      timer = window.setInterval(function () {
        goTo(active + 1);
      }, 2800);
    }

    gallery.classList.add("is-coverflow");
    paintGallery();
    restart();

    window.addEventListener("resize", paintGallery);
    gallery.addEventListener("mouseenter", function () {
      window.clearInterval(timer);
    });
    gallery.addEventListener("mouseleave", restart);
    document.addEventListener("visibilitychange", function () {
      if (document.hidden) window.clearInterval(timer);
      else restart();
    });
  }

  /* =======================================================================
     8. Reviews — a scroll-snap strip with two arrows
     ======================================================================= */

  var carousel = document.querySelector("[data-carousel]");

  if (carousel) {
    var strip = carousel.querySelector("[data-carousel-track]");
    var slides = strip.children;
    var prev = carousel.querySelector("[data-carousel-prev]");
    var next = carousel.querySelector("[data-carousel-next]");
    var announce = carousel.querySelector("[data-carousel-status]");

    function currentSlide() {
      return Math.round(strip.scrollLeft / slides[0].getBoundingClientRect().width);
    }

    function move(step) {
      var index = Math.max(0, Math.min(slides.length - 1, currentSlide() + step));
      strip.scrollTo({ left: index * slides[0].getBoundingClientRect().width, behavior: reduced ? "auto" : "smooth" });
    }

    prev.addEventListener("click", function () {
      move(-1);
    });
    next.addEventListener("click", function () {
      move(1);
    });

    strip.addEventListener(
      "scroll",
      function () {
        var index = currentSlide();
        if (announce) announce.textContent = "Review " + (index + 1) + " of " + slides.length;
        prev.disabled = index === 0;
        next.disabled = index === slides.length - 1;
      },
      { passive: true }
    );
  }

  /* =======================================================================
     9. FAQ and services — animating <details>
     -----------------------------------------------------------------------
     A closed <details> is taken out of rendering, so a CSS transition or a
     @starting-style rule never replays after the first open. The height is
     therefore animated here, from the measured height of the panel.
     ======================================================================= */

  function animateOpen(panel) {
    if (reduced) return;
    var height = panel.scrollHeight;
    panel.animate(
      [
        { height: "0px", opacity: 0 },
        { height: height + "px", opacity: 1 },
      ],
      { duration: 400, easing: "cubic-bezier(0.77, 0, 0.175, 1)" }
    );
  }

  document.querySelectorAll(".faq__item").forEach(function (item) {
    item.addEventListener("toggle", function () {
      if (item.open) animateOpen(item.querySelector(".faq__body"));
    });
  });

  /* Services: the reference opened a row on hover only, which no keyboard can
     reach. Hover still works — with an opening and a closing delay, the
     second one long enough to cross the gap between two rows — and the row is
     also a plain <details> anyone can click or tab into. */
  var services = document.querySelectorAll("[data-service]");

  if (services.length) {
    var closeTimer = null;
    var fine = window.matchMedia("(hover: hover) and (pointer: fine)");

    function openOnly(target) {
      services.forEach(function (item) {
        if (item === target) {
          if (!item.open) {
            item.open = true;
            animateOpen(item.querySelector(".service__body"));
          }
        } else {
          item.open = false;
        }
      });
    }

    services.forEach(function (item) {
      item.addEventListener("toggle", function () {
        if (item.open) {
          openOnly(item);
        }
      });

      item.addEventListener("mouseenter", function () {
        if (!fine.matches) return;
        window.clearTimeout(closeTimer);
        closeTimer = window.setTimeout(function () {
          openOnly(item);
        }, 80);
      });

      item.addEventListener("mouseleave", function () {
        if (!fine.matches) return;
        window.clearTimeout(closeTimer);
      });
    });
  }

  /* =======================================================================
     10. Hero video — downloaded on demand
     -----------------------------------------------------------------------
     Until the visitor presses play the card shows a 13 KB poster. The 428 KB
     file is only requested on that first click.
     ======================================================================= */

  var player = document.querySelector("[data-player]");

  if (player) {
    var toggle = player.querySelector("[data-player-toggle]");
    var media = null;

    toggle.addEventListener("click", function () {
      if (!media) {
        media = document.createElement("video");
        media.src = "assets/video/hero-hiker.mp4";
        media.poster = "assets/video/hero-hiker-poster.webp";
        media.muted = true;
        media.loop = true;
        media.playsInline = true;
        media.setAttribute("aria-label", "A hiker pausing on a ridge at sunrise");
        player.insertBefore(media, toggle);
        player.querySelector(".player__poster").remove();
      }

      if (media.paused) {
        media.play();
        player.classList.add("is-playing");
        toggle.setAttribute("aria-label", "Pause video");
      } else {
        media.pause();
        player.classList.remove("is-playing");
        toggle.setAttribute("aria-label", "Play video");
      }
    });
  }

  /* =======================================================================
     11. Forms
     -----------------------------------------------------------------------
     There is no back end in a template. The submit is stopped here and
     answered in place; point the `action` attribute at your own endpoint and
     delete this block.
     ======================================================================= */

  document.querySelectorAll("[data-form]").forEach(function (form) {
    var note = form.querySelector("[data-form-note]");
    form.addEventListener("submit", function (event) {
      event.preventDefault();
      if (!form.reportValidity()) return;
      if (note) note.textContent = "Thank you — your request has been recorded.";
      form.reset();
    });
  });
})();
