(() => { document.addEventListener("DOMContentLoaded", () => { const bg = document.querySelector(".background"); // ---- Inject realistic clouds into .background ---- if (bg && !bg.querySelector(".cloud")) { const isMobile = window.matchMedia("(max-width:768px)").matches; if (!isMobile && !document.getElementById("cloud-fluff")) { const defs = document.createElementNS("http://www.w3.org/2000/svg", "svg"); defs.setAttribute("width", "0"); defs.setAttribute("height", "0"); defs.setAttribute("aria-hidden", "true"); defs.style.position = "absolute"; defs.innerHTML = '' + '' + '' + ""; document.body.appendChild(defs); } const cloudCount = isMobile ? 2 : 4; for (let i = 1; i <= cloudCount; i++) { const cloud = document.createElement("div"); cloud.className = "cloud cloud-" + i; cloud.setAttribute("aria-hidden", "true"); bg.appendChild(cloud); } } // ---- Grass strip along the bottom of the viewport ---- if (!document.querySelector(".grass")) { const NS = "http://www.w3.org/2000/svg"; const W = isMobile ? 1200 : 2400; const H = 70; const svg = document.createElementNS(NS, "svg"); svg.setAttribute("class", "grass"); svg.setAttribute("viewBox", "0 0 " + W + " " + H); svg.setAttribute("preserveAspectRatio", "xMidYMax slice"); svg.setAttribute("aria-hidden", "true"); // soft ground shading under the blades svg.innerHTML = '' + '' + '' + "" + ''; const isMobile = window.matchMedia("(max-width: 768px)").matches; const layers = isMobile ? [ { count: 50, hMin: 18, hMax: 32, colors: ["#4f9854"], opacity: 0.9 }, { count: 40, hMin: 24, hMax: 42, colors: ["#63ad64"], opacity: 1 }, ] : [ { count: 190, hMin: 18, hMax: 36, colors: ["#3e7a46","#457f4b","#4a8a50"], opacity: 0.8 }, { count: 160, hMin: 26, hMax: 52, colors: ["#4f9854","#57a05a","#63ad64","#6db36e"], opacity: 1 }, ]; const layerEls = []; layers.forEach((layer) => { const g = document.createElementNS(NS, "g"); g.setAttribute("opacity", layer.opacity); g.style.transformBox = "fill-box"; g.style.transformOrigin = "50% 100%"; layerEls.push(g); for (let i = 0; i < layer.count; i++) { const x = Math.random() * W; const h = layer.hMin + Math.random() * (layer.hMax - layer.hMin); const lean = (Math.random() - 0.5) * 16; const w = 3 + Math.random() * 3; const color = layer.colors[(Math.random() * layer.colors.length) | 0]; const blade = document.createElementNS(NS, "path"); blade.setAttribute( "d", "M " + x + " " + H + " Q " + (x + lean * 0.2) + " " + (H - h * 0.6) + " " + (x + lean) + " " + (H - h) + " Q " + (x + lean * 0.6 + w * 0.5) + " " + (H - h * 0.55) + " " + (x + w) + " " + H + " Z" ); blade.setAttribute("fill", color); blade.style.transformBox = "fill-box"; blade.style.transformOrigin = "50% 100%"; // stagger by x so a gust appears to travel across the lawn if (!isMobile) { blade.style.animation = "blade-sway " + (2.6 + Math.random() * 2.4).toFixed(2) + "s ease-in-out " + (-(x / W) * 3 - Math.random()).toFixed(2) + "s infinite alternate"; } g.appendChild(blade); } svg.appendChild(g); }); document.body.appendChild(svg); // ---- Scroll physics: scrolling down squashes the grass toward the // ground, scrolling up stretches it, and a damped spring makes it // boing back upright ---- if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) { let squash = 0; // offset from natural height (scaleY = 1 + squash) let squashVel = 0; let lastY = window.scrollY; let raf = null; const settle = () => { squashVel -= squash * 0.06; // spring pull toward natural height squashVel *= 0.88; // damping squash += squashVel; const s = Math.max(-0.3, Math.min(0.14, squash)); layerEls.forEach((g, i) => { // back layer reacts less than the front for depth const depth = i === 0 ? 0.55 : 1; g.style.transform = "scaleY(" + (1 + s * depth).toFixed(4) + ")"; }); if (Math.abs(squash) > 0.001 || Math.abs(squashVel) > 0.001) { raf = requestAnimationFrame(settle); } else { layerEls.forEach((g) => (g.style.transform = "")); raf = null; } }; const impulse = (v) => { squashVel += v; if (!raf) raf = requestAnimationFrame(settle); }; window.addEventListener( "scroll", () => { const y = window.scrollY; // scroll down -> stretch, scroll up -> duck impulse(Math.max(-0.08, Math.min(0.08, (y - lastY) * 0.0015))); lastY = y; }, { passive: true } ); // wheel fallback so short pages that can't scroll still react window.addEventListener( "wheel", (e) => { if (document.documentElement.scrollHeight <= window.innerHeight + 4) { impulse(Math.max(-0.05, Math.min(0.05, e.deltaY * 0.0005))); } }, { passive: true } ); } } // ---- Copy buttons on
 ----
    document.querySelectorAll("pre").forEach((pre) => {
      if (pre.querySelector(".copy-btn")) return;
      const code = pre.querySelector("code") || pre;
      const btn = document.createElement("button");
      btn.className = "copy-btn";
      btn.textContent = "Copy";
      btn.addEventListener("click", async () => {
        try {
          await navigator.clipboard.writeText(code.innerText);
          btn.textContent = "Copied!";
          setTimeout(() => (btn.textContent = "Copy"), 1200);
        } catch {
          btn.textContent = "Error";
          setTimeout(() => (btn.textContent = "Copy"), 1200);
        }
      });
      pre.appendChild(btn);
    });

    // ---- Article-only features: sidebar + collapsible code ----
    const article = document.querySelector(".article");
    if (!article) return;

    // Collect headings for TOC
    const headings = article.querySelectorAll("h1, h2, h3");
    const codeBlocks = article.querySelectorAll("pre");

    // Determine filenames from preceding h2
    const codeFiles = [];
    codeBlocks.forEach((pre, idx) => {
      let name = null;
      let el = pre.previousElementSibling;
      while (el) {
        if (el.tagName === "H2" || el.tagName === "H3") {
          const text = el.textContent.trim();
          if (text.match(/\.\w{1,5}$/) || text.match(/\.\w{1,5}\s/)) {
            name = text.replace(/\s*\(.*\)/, "").trim();
          }
          break;
        }
        el = el.previousElementSibling;
      }
      if (!name) name = `code-block-${idx + 1}.txt`;
      codeFiles.push({ name, pre });
    });

    // Make code blocks collapsible
    codeBlocks.forEach((pre, idx) => {
      const wrapper = document.createElement("div");
      wrapper.className = "code-collapsible";

      const toggle = document.createElement("button");
      toggle.className = "code-toggle";
      const fileName = codeFiles[idx].name;
      toggle.innerHTML = ` ${fileName}`;
      toggle.setAttribute("aria-expanded", "false");

      pre.parentNode.insertBefore(wrapper, pre);
      wrapper.appendChild(toggle);
      wrapper.appendChild(pre);
      pre.classList.add("collapsed");

      toggle.addEventListener("click", () => {
        const expanded = pre.classList.toggle("collapsed");
        toggle.setAttribute("aria-expanded", !expanded);
        toggle.querySelector(".code-toggle-arrow").textContent = expanded ? "▶" : "▼";
      });
    });

    // Build sidebar
    const sidebar = document.createElement("nav");
    sidebar.className = "article-sidebar";

    // TOC section
    const tocTitle = document.createElement("div");
    tocTitle.className = "sidebar-title";
    tocTitle.textContent = "Contents";
    sidebar.appendChild(tocTitle);

    const tocList = document.createElement("ul");
    tocList.className = "sidebar-toc";

    headings.forEach((h, i) => {
      if (!h.id) h.id = "heading-" + i;
      const li = document.createElement("li");
      li.className = "toc-" + h.tagName.toLowerCase();
      const a = document.createElement("a");
      a.href = "#" + h.id;
      a.textContent = h.textContent;
      a.addEventListener("click", (e) => {
        e.preventDefault();
        h.scrollIntoView({ behavior: "smooth", block: "start" });
      });
      li.appendChild(a);
      tocList.appendChild(li);
    });
    sidebar.appendChild(tocList);

    // Files section
    if (codeFiles.length > 0) {
      const filesTitle = document.createElement("div");
      filesTitle.className = "sidebar-title";
      filesTitle.textContent = "Files";
      sidebar.appendChild(filesTitle);

      const fileList = document.createElement("ul");
      fileList.className = "sidebar-files";

      codeFiles.forEach(({ name, pre }) => {
        const li = document.createElement("li");
        const btn = document.createElement("button");
        btn.className = "sidebar-file-btn";
        btn.innerHTML = ` ${name}`;

        btn.addEventListener("click", () => {
          const code = pre.querySelector("code") || pre;
          const text = code.innerText;
          const blob = new Blob([text], { type: "text/plain" });
          const url = URL.createObjectURL(blob);
          const a = document.createElement("a");
          a.href = url;
          a.download = name;
          a.click();
          URL.revokeObjectURL(url);
        });

        li.appendChild(btn);
        fileList.appendChild(li);
      });
      sidebar.appendChild(fileList);
    }

    document.body.appendChild(sidebar);

    // Highlight active TOC item on scroll
    const tocLinks = tocList.querySelectorAll("a");
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            tocLinks.forEach((l) => l.classList.remove("active"));
            const link = tocList.querySelector(`a[href="#${entry.target.id}"]`);
            if (link) link.classList.add("active");
          }
        });
      },
      { rootMargin: "-80px 0px -70% 0px" }
    );
    headings.forEach((h) => observer.observe(h));
  });
})();