1(() => { 2 "use strict"; 3 4 const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 5 6 function prepareTitleReveal(element) { 7 const original = (element.textContent || "").replace(/\s+/g, " ").trim(); 8 if (!original) return false; 9 10 const words = original.split(" "); 11 const measurement = document.createDocumentFragment(); 12 words.forEach((word, index) => { 13 const span = document.createElement("span"); 14 span.className = "cr-title-reveal__measure"; 15 span.textContent = word; 16 measurement.append(span); 17 if (index < words.length - 1) { 18 measurement.append(document.createTextNode(" ")); 19 } 20 }); 21 element.replaceChildren(measurement); 22 23 const measuredWords = [...element.querySelectorAll(".cr-title-reveal__measure")]; 24 const lines = []; 25 measuredWords.forEach((word) => { 26 const offset = Math.round(word.offsetTop); 27 const current = lines.at(-1); 28 if (!current || Math.abs(current.offset - offset) > 1) { 29 lines.push({ offset, words: [word.textContent] }); 30 } else { 31 current.words.push(word.textContent); 32 } 33 }); 34 35 const fragment = document.createDocumentFragment(); 36 lines.forEach(({ words }) => { 37 const wrapper = document.createElement("span"); 38 const line = document.createElement("span"); 39 const box = document.createElement("span"); 40 wrapper.className = "cr-title-reveal__line-wrapper"; 41 wrapper.setAttribute("aria-hidden", "true"); 42 line.className = "cr-title-reveal__line"; 43 line.textContent = words.join(" "); 44 box.className = "cr-title-reveal__box"; 45 wrapper.append(line, box); 46 fragment.append(wrapper); 47 }); 48 49 element.setAttribute("aria-label", original); 50 element.replaceChildren(fragment); 51 element.classList.add("is-title-ready"); 52 return true; 53 } 54 55 async function setupTitleReveals() { 56 const titles = [...document.querySelectorAll("[data-cr-title-reveal]")]; 57 if (!titles.length || reduceMotion) return; 58 59 document.documentElement.classList.add("cr-title-motion"); 60 try { 61 if (document.fonts?.ready) await document.fonts.ready; 62 } catch { 63 // The system font fallback still provides stable line measurements. 64 } 65 66 const prepared = titles.filter(prepareTitleReveal); 67 if (!prepared.length) return; 68 69 if (!("IntersectionObserver" in window)) { 70 prepared.forEach((title) => title.classList.add("is-title-revealed")); 71 return; 72 } 73 74 const observer = new IntersectionObserver( 75 (entries) => { 76 entries.forEach((entry) => { 77 if (!entry.isIntersecting) return; 78 entry.target.classList.add("is-title-revealed"); 79 observer.unobserve(entry.target); 80 }); 81 }, 82 { threshold: 0 }, 83 ); 84 85 prepared.forEach((title) => observer.observe(title)); 86 } 87 88 function setupReveals() { 89 const items = [...document.querySelectorAll("[data-cr-reveal]")]; 90 if (!items.length) return; 91 92 if (reduceMotion) { 93 items.forEach((item) => item.classList.add("is-visible")); 94 return; 95 } 96 97 if (!("IntersectionObserver" in window)) { 98 items.forEach((item) => item.classList.add("is-visible")); 99 return; 100 } 101 102 document.documentElement.classList.add("cr-motion"); 103 const observer = new IntersectionObserver( 104 (entries) => { 105 entries.forEach((entry) => { 106 if (!entry.isIntersecting) return; 107 entry.target.classList.add("is-visible"); 108 observer.unobserve(entry.target); 109 }); 110 }, 111 { rootMargin: "0px 0px -10%", threshold: 0.08 }, 112 ); 113 114 items.forEach((item) => observer.observe(item)); 115 } 116 117 function setupProcessDeck() { 118 const deck = document.querySelector("[data-cr-process-deck]"); 119 const stages = [...(deck?.querySelectorAll("[data-cr-stage]") ?? [])]; 120 if (!deck || !stages.length) return; 121 122 let activeIndex = 0; 123 124 function setActive(index, moveFocus = false) { 125 const next = Math.max(0, Math.min(stages.length - 1, index)); 126 activeIndex = next; 127
128 stages.forEach((stage, stageIndex) => { 129 const isActive = stageIndex === activeIndex; 130 stage.dataset.state = isActive ? "open" : "closed"; 131 stage.setAttribute("aria-expanded", String(isActive)); 132 if (isActive) { 133 stage.setAttribute("aria-current", "step"); 134 } else { 135 stage.removeAttribute("aria-current"); 136 } 137 }); 138 if (moveFocus) stages[activeIndex]?.focus(); 139 } 140 141 stages.forEach((stage, index) => { 142 stage.addEventListener("click", () => setActive(index)); 143 stage.addEventListener("focus", () => setActive(index)); 144 stage.addEventListener("pointerenter", () => { 145 if (window.matchMedia("(hover: hover) and (pointer: fine)").matches) setActive(index); 146 }); 147 stage.addEventListener("keydown", (event) => { 148 let next = null; 149 if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (index + 1) % stages.length; 150 if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (index - 1 + stages.length) % stages.length; 151 if (event.key === "Home") next = 0; 152 if (event.key === "End") next = stages.length - 1; 153 if (event.key === "Enter" || event.key === " ") next = index; 154 if (next === null) return; 155 event.preventDefault(); 156 setActive(next, true); 157 }); 158 }); 159 160 setActive(0); 161 } 162 163 function init() { 164 void setupTitleReveals(); 165 setupReveals(); 166 setupProcessDeck(); 167 } 168 169 if (document.readyState === "loading") { 170 document.addEventListener("DOMContentLoaded", init, { once: true }); 171 } else { 172 init(); 173 } 174})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.