1const DEFAULT_SITE_DATA = { 2 releases: [ 3 { 4 title: "Walk Away", 5 meta: "2026 release", 6 image: "assets/images/releases/walk-away.jpg", 7 alt: "Walk Away cover art", 8 links: { 9 Spotify: "https://open.spotify.com/album/7pfwOHCIY1nB8Oaa4AzyQB", 10 Beatport: "https://www.beatport.com/artist/tritonal/75268/releases" 11 } 12 }, 13 { 14 title: "Watch Us Glow", 15 meta: "Tritonal feat. Angel Taylor", 16 image: "assets/images/releases/watch-us-glow.jpg", 17 alt: "Watch Us Glow cover art", 18 links: { 19 Spotify: "https://open.spotify.com/album/0sin3mAcwISmJjPNOpVQeF", 20 Beatport: "https://www.beatport.com/release/watch-us-glow/4907251" 21 } 22 }, 23 { 24 title: "From The Inside", 25 meta: "Tritonal & Kuala", 26 links: { 27 spotify: "https://open.spotify.com/artist/521qvhdobR0GzhvU6TFw76", 28 beatport: "https://www.beatport.com/artist/tritonal/75268/releases" 29 } 30 }, 31 { 32 title: "U & Me", 33 meta: "Artist album", 34 links: { 35 spotify: "https://open.spotify.com/artist/521qvhdobR0GzhvU6TFw76", 36 beatport: "https://www.beatport.com/artist/tritonal/75268/releases" 37 } 38 } 39 ], 40 videos: [ 41 { 42 id: "5HM2WEgXNFk", 43 title: "Tritonal feat. Angel Taylor - Watch Us Glow" 44 }, 45 { 46 id: "1Xx07D2Suuk", 47 title: "Tritonia 524" 48 }, 49 { 50 id: "VhjYGtmhH90", 51 title: "Watch Us Glow - Hyperbits Remix" 52 }, 53 { 54 id: "DtkW-o2En00", 55 title: "U Found Me - Official Music Video" 56 } 57 ], 58 instagramFallback: [ 59 { 60 image: "assets/images/a7401425.jpg", 61 alt: "Tritonal silhouetted in red stage light", 62 url: "https://www.instagram.com/tritonal/" 63 }, 64 { 65 image: "assets/images/a7401629.jpg", 66 alt: "Tritonal posing against an orange stage visual", 67 url: "https://www.instagram.com/tritonal/" 68 }, 69 { 70 image: "assets/images/a7402072.jpg", 71 alt: "Tritonal in front of a triangular stage visual", 72 url: "https://www.instagram.com/tritonal/" 73 }, 74 { 75 image: "assets/images/a7402238.jpg", 76 alt: "Tritonal silhouettes in front of a glowing triangle", 77 url: "https://www.instagram.com/tritonal/" 78 } 79 ] 80}; 81 82const qs = (selector, scope = document) => scope.querySelector(selector); 83const qsa = (selector, scope = document) => [...scope.querySelectorAll(selector)]; 84 85function escapeHtml(value = "") { 86 return value 87 .replaceAll("&", "&") 88 .replaceAll("<", "<") 89 .replaceAll(">", ">") 90 .replaceAll('"', """) 91 .replaceAll("'", "'"); 92} 93 94async function loadJson(path, fallback) { 95 try { 96 const response = await fetch(path, { cache: "no-store" }); 97 if (!response.ok) { 98 throw new Error(`Unable to load ${path}`); 99 } 100 return await response.json(); 101 } catch { 102 return fallback; 103 } 104} 105 106function initHeader() { 107 const header = qs("[data-header]"); 108 const toggle = qs(".nav-toggle"); 109 const navLinks = qsa(".primary-nav a"); 110 111 const updateHeader = () => { 112 header?.classList.toggle("is-scrolled", window.scrollY > 18); 113 }; 114 115 toggle?.addEventListener("click", () => { 116 const isOpen = document.body.classList.toggle("nav-open"); 117 toggle.setAttribute("aria-expanded", String(isOpen)); 118 }); 119 120 navLinks.forEach((link) => { 121 link.addEventListener("click", () => { 122 document.body.classList.remove("nav-open"); 123 toggle?.setAttribute("aria-expanded", "false"); 124 }); 125 }); 126 127 updateHeader(); 128 window.addEventListener("scroll", updateHeader, { passive: true }); 129} 130 131function initActiveNavigation() { 132 const navLinks = qsa(".primary-nav a").filter((link) => 133 link.getAttribute("href")?.startsWith("#") 134 ); 135 const sections = navLinks 136 .map((link) => qs(link.getAttribute("href"))) 137 .filter(Boolean); 138 139 if (!sections.length) { 140 return; 141 } 142 143 const observer = new IntersectionObserver( 144 (entries) => { 145 const visible = entries 146 .filter((entry) => entry.isIntersecting) 147 .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]; 148 149 if (!visible) { 150 return; 151 } 152 153 navLinks.forEach((link) => { 154 link.classList.toggle("is-active", link.getAttribute("href") === `#${visible.target.id}`); 155 }); 156 }, 157 { 158 rootMargin: "-35% 0px -55% 0px", 159 threshold: [0, 0.2, 0.6] 160 } 161 ); 162 163 sections.forEach((section) => observer.observe(section)); 164} 165 166function renderReleases(releases = []) { 167 const container = qs("[data-releases]"); 168 if (!container) { 169 return; 170 } 171 172 container.innerHTML = releases 173 .map((release) => { 174 const links = Object.entries(release.links || {}) 175 .map(([label, url]) => { 176 const slug = label.toLowerCase().includes("spotify") ? "spotify" : "beatport"; 177 return `<a class="pill-link pill-link--${slug}" href="${escapeHtml(url)}" target="_blank" rel="noreferrer">${escapeHtml(label)}</a>`; 178 }) 179 .join(""); 180 181 return ` 182 <article class="release-card"> 183 <div> 184 <p class="release-card__meta">${escapeHtml(release.meta || "")}</p> 185 <h3>${escapeHtml(release.title || "")}</h3> 186 </div> 187 <div class="release-card__links">${links}</div> 188 </article> 189 `; 190 }) 191 .join(""); 192} 193 194function initContactForm() { 195 const form = qs("[data-contact-form]"); 196 if (!form) { 197 return; 198 } 199 200 const message = qs("[data-form-message]", form); 201 const captcha = qs("[data-captcha-answer]", form); 202 203 captcha?.addEventListener("input", () => { 204 if (Number(captcha.value) === 12) { 205 captcha.removeAttribute("aria-invalid"); 206 if (message) { 207 message.textContent = ""; 208 } 209 } 210 }); 211 212 form.addEventListener("submit", (event) => { 213 const honey = form.querySelector("[name='_honey']"); 214 215 if (honey?.value) { 216 event.preventDefault(); 217 return; 218 } 219 220 if (Number(captcha?.value) !== 12) { 221 event.preventDefault(); 222 captcha?.setAttribute("aria-invalid", "true"); 223 if (message) { 224 message.textContent = "Please answer the security check before sending."; 225 } 226 captcha?.focus(); 227 } 228 }); 229} 230 231async function initContent() { 232 const siteData = await loadJson("assets/data/site.json", DEFAULT_SITE_DATA); 233 renderReleases(siteData.releases || DEFAULT_SITE_DATA.releases); 234} 235 236function initFooterYear() { 237 const year = qs("[data-year]"); 238 if (year) { 239 year.textContent = new Date().getFullYear(); 240 } 241} 242 243initHeader(); 244initActiveNavigation(); 245initContent(); 246initContactForm(); 247initFooterYear();
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.