1/* Brand New Church - site behaviors + hot-swap rendering */ 2 3(function () { 4 "use strict"; 5 6 var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 7 8 /* ââ Nav: frosted bar after scroll âââââââââââââââââââââââââââââââââââ */ 9 10 var nav = document.querySelector(".nav"); 11 12 function onScrollNav() { 13 if (!nav) return; 14 if (window.scrollY > 24) nav.classList.add("solid"); 15 else nav.classList.remove("solid"); 16 } 17 window.addEventListener("scroll", onScrollNav, { passive: true }); 18 onScrollNav(); 19 20 /* ââ Mobile menu âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 21 22 var burger = document.querySelector(".nav-burger"); 23 if (burger) { 24 burger.addEventListener("click", function () { 25 var open = document.body.classList.toggle("menu-open"); 26 burger.setAttribute("aria-expanded", open ? "true" : "false"); 27 }); 28 document.querySelectorAll(".menu-overlay a").forEach(function (a) { 29 a.addEventListener("click", function () { 30 document.body.classList.remove("menu-open"); 31 burger.setAttribute("aria-expanded", "false"); 32 }); 33 }); 34 } 35 36 /* ââ Cinematic hero scroll (home only) âââââââââââââââââââââââââââââââ */ 37 38 var hero = document.querySelector(".hero"); 39 var heroImg = document.querySelector(".hero-img"); 40 var heroCopy = document.querySelector(".hero-copy"); 41 var heroTimes = document.querySelector(".hero-times"); 42 var cue = document.querySelector(".scroll-cue"); 43 44 if (hero && heroImg && !reduceMotion) { 45 var ticking = false; 46 // Cache the scroll runway so we never read layout (offsetHeight) inside the 47 // scroll loop; reading it per frame forces a reflow and causes jitter. 48 var runway = 0; 49 var measureHero = function () { runway = hero.offsetHeight - window.innerHeight; }; 50 51 var updateHero = function () { 52 ticking = false; 53 if (runway <= 0) return; 54 var p = window.scrollY / runway; 55 p = p < 0 ? 0 : p > 1 ? 1 : p; 56 57 // Slow push-in on the campus photo (translate3d keeps it on the GPU) 58 heroImg.style.transform = "scale(" + (1 + p * 0.09) + ") translate3d(0," + (p * -14) + "px,0)"; 59 60 // Headline drifts up into the sky and fades 61 if (heroCopy) { 62 heroCopy.style.transform = "translate3d(0," + (p * -70) + "px,0)"; 63 heroCopy.style.opacity = String(1 - Math.max(0, (p - 0.35)) * 2.1); 64 } 65 if (heroTimes) heroTimes.style.opacity = String(1 - p * 2.4); 66 if (cue) cue.style.opacity = String(1 - p * 3); 67 }; 68 69 var onHeroScroll = function () { 70 if (!ticking) { ticking = true; window.requestAnimationFrame(updateHero); } 71 }; 72 73 window.addEventListener("scroll", onHeroScroll, { passive: true }); 74 window.addEventListener("resize", function () { measureHero(); updateHero(); }, { passive: true }); 75 measureHero(); 76 updateHero(); 77 } 78 79 /* ââ Scroll reveals ââââââââââââââââââââââââââââââââââââââââââââââââââ */ 80 81 var revealEls = document.querySelectorAll(".reveal"); 82 if ("IntersectionObserver" in window && !reduceMotion) { 83 var io = new IntersectionObserver(function (entries) { 84 entries.forEach(function (e) { 85 if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } 86 }); 87 }, { rootMargin: "0px 0px -8% 0px", threshold: 0.08 }); 88 revealEls.forEach(function (el) { io.observe(el); }); 89 } else { 90 revealEls.forEach(function (el) { el.classList.add("in"); }); 91 } 92 93 /* ââ Marquee: duplicate track content for a seamless loop ââââââââââââ */ 94 95 document.querySelectorAll(".marquee-row .track").forEach(function (track) { 96 track.innerHTML += track.innerHTML; 97 }); 98 99 /* ââ Hot-swap rendering from js/content.js âââââââââââââââââââââââââââ */ 100 101 var C = window.BNC || {}; 102 103 function el(tag, cls, html) { 104 var n = document.createElement(tag); 105 if (cls) n.className = cls; 106 if (html !== undefined) n.innerHTML = html; 107 return n; 108 } 109 110 function esc(s) { 111 return String(s == null ? "" : s) 112 .replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") 113 .replace(/"/g, """); 114 } 115 116 function btnClass(style) { 117 return style === "ghost" ? "btn btn-ghost" : style === "solid" ? "btn btn-solid" : "btn btn-white"; 118 } 119 120 /* Current series panel */ 121 var seriesRoot = document.getElementById("series-root"); 122 if (seriesRoot && C.series) { 123 var s = C.series; 124 var art = s.logo 125 ? '<div class="panel-art reveal"><img decoding="async" src="' + esc(s.logo) + '" alt="' + esc(s.title) + ' series artwork"></div>' 126 : ""; 127 var titleBlock = s.logo ? "" : "<h3>" + esc(s.title) + "</h3>"; 128 var ctas = (s.ctas || []).map(function (c) { 129 return '<a class="' + btnClass(c.style) + '" href="' + esc(c.href) + '"' + 130 (/^https?:/.test(c.href) ? ' target="_blank" rel="noopener"' : "") + ">" + esc(c.label) + "</a>"; 131 }).join(""); 132 133 if (s.artworkFillsFrame) { 134 // .series-split lays its children out as flex rows so panels f
134ill their 135 // grid cell; this variant adds a second child (the buttons) that has to 136 // stack under the frame instead of sitting beside it. 137 seriesRoot.classList.add("series-root--artwork"); 138 // The artwork is a finished, self-contained graphic (it already carries 139 // the series name and its own background) rather than the usual photo + 140 // transparent-logo pairing. So it becomes the frame itself: rendered in 141 // flow at full width, which lets the panel take the image's own height - 142 // no cropping at any viewport, and no hardcoded aspect ratio. No scrim 143 // either; that exists to keep white text legible over a photo, and here 144 // it would just grey down a clean graphic. The buttons move below the 145 // frame, where they can't sit on top of the artwork's own type. 146 seriesRoot.innerHTML = 147 '<div class="panel series panel--artwork reveal">' + 148 '<img class="panel-artwork" decoding="async" src="' + esc(s.image) + '" alt="' + esc(s.title) + '">' + 149 "</div>" + 150 '<div class="series-ctas reveal" style="--d:.1s">' + ctas + "</div>"; 151 } else { 152 seriesRoot.innerHTML = 153 '<div class="panel series">' + 154 '<div class="panel-bg"><img decoding="async" src="' + esc(s.image) + '" alt=""></div>' + 155 '<div class="panel-scrim"></div>' + 156 '<div class="panel-content">' + 157 art + 158 '<div class="panel-copy reveal" style="--d:.1s">' + 159 (s.badgeImage 160 ? '<img decoding="async" class="panel-badge-img" src="' + esc(s.badgeImage) + '" alt="">' 161 : (s.badge ? '<span class="panel-badge">' + esc(s.badge) + "</span>" : "")) + 162 titleBlock + 163 (s.tagline ? "<p>" + esc(s.tagline) + "</p>" : "") + 164 '<div class="panel-ctas">' + ctas + "</div>" + 165 "</div>" + 166 "</div>" + 167 "</div>"; 168 } 169 } 170 171 /* Side promo panel, sits beside the series. The whole frame is the link and 172 the "button" is a span, so there is no anchor nested inside an anchor. */ 173 var promoRoot = document.getElementById("promo-root"); 174 if (promoRoot && C.promo && C.promo.image) { 175 var pr = C.promo; 176 var dark = pr.tone === "dark"; 177 var external = /^https?:/.test(pr.href || ""); 178 179 promoRoot.innerHTML = 180 '<a class="panel promo' + (dark ? " promo-dark" : "") + ' reveal" style="--d:.1s" href="' + esc(pr.href || "#") + '"' + 181 (external ? ' target="_blank" rel="noopener"' : ' download') + ">" + 182 '<div class="panel-bg"><img decoding="async" src="' + esc(pr.image) + '" alt="' + esc(pr.alt || "") + '"></div>' + 183 '<div class="promo-scrim"></div>' + 184 '<div class="promo-content">' + 185 '<span class="btn ' + (dark ? "btn-white" : "btn-solid") + '">' + esc(pr.cta || "Download") + "</span>" + 186 "</div>" + 187 "</a>"; 188 } 189 190 // No promo configured: let the series panel span the full width again. 191 var splitEl = document.querySelector(".series-split"); 192 if (splitEl && (!C.promo || !C.promo.image)) splitEl.classList.add("series-split--solo"); 193 194 /* Events: one featured banner + cards */ 195 var featRoot = document.getElementById("event-featured-root"); 196 var gridRoot = document.getElementById("event-grid-root"); 197 if (C.events && (featRoot || gridRoot)) { 198 var feats = C.events.filter(function (e) { return e.featured; }); 199 var rest = C.events.filter(function (e) { return !e.featured; }); 200 201 if (featRoot && feats.length) { 202 featRoot.innerHTML = feats.map(function (e) { 203 var logo = e.logo 204 ? '<div class="panel-art reveal"><img decoding="async" src="' + esc(e.logo) + '" alt="' + esc(e.title) + '"></div>' 205 : ""; 206 // Same rule as the series panel: when there is a logo it already 207 // carries the name, so printing the title too just says it twice. 208 var featTitle = e.logo ? "" : "<h3>" + esc(e.title) + "</h3>"; 209 // Only send visitors to a new tab when the link actually leaves us. 210 var featExternal = /^https?:/.test(e.href || ""); 211 212 return '<div class="panel">' + 213 '<div class="panel-bg"><img decoding="async" src="' + esc(e.image) + '" alt=""></div>' + 214 '<div class="panel-scrim"></div>' + 215 '<div class="panel-content">' + 216 '<div class="panel-copy reveal">
216' + 217 (e.badge ? '<span class="panel-badge">' + esc(e.badge) + "</span>" : "") + 218 featTitle + 219 (e.blurb ? "<p>" + esc(e.blurb) + "</p>" : "") + 220 '<div class="panel-ctas"><a class="btn btn-white" href="' + esc(e.href) + '"' + 221 (featExternal ? ' target="_blank" rel="noopener"' : "") + ">" + esc(e.cta || "Learn More") + "</a></div>" + 222 "</div>" + logo + 223 "</div></div>"; 224 }).join(""); 225 } 226 227 if (gridRoot) { 228 gridRoot.innerHTML = rest.map(function (e, i) { 229 // A card can carry a link before it's meant to go live (e.g. tickets 230 // queued ahead of an on-sale time). liveAt is an ISO timestamp; until 231 // it passes, the card renders exactly as if href were never set. 232 var isLive = !e.liveAt || Date.now() >= new Date(e.liveAt).getTime(); 233 var href = isLive ? e.href : null; 234 var external = /^https?:/.test(href || ""); 235 // Some events have nothing to sign up for. Those render as a plain 236 // card with no link and no call to action, rather than a link that 237 // dumps people on a generic page. 238 var tag = href ? "a" : "div"; 239 return "<" + tag + ' class="event-card reveal' + (href ? "" : " is-static") + 240 '" style="--d:' + (i * 0.08) + 's"' + 241 (href ? ' href="' + esc(href) + '"' : "") + 242 (external ? ' target="_blank" rel="noopener"' : "") + ">" + 243 '<div class="event-media"><img decoding="async" src="' + esc(e.image) + '" alt="' + esc(e.title) + '" loading="lazy"></div>' + 244 '<div class="event-body">' + 245 '<span class="event-date">' + esc(e.date || "") + "</span>" + 246 "<h4>" + esc(e.title) + "</h4>" + 247 "<p>" + esc(e.blurb || "") + "</p>" + 248 (href ? '<span class="text-link">' + esc(e.cta || "Details") + ' <span class="chev">âº</span></span>' : "") + 249 "</div></" + tag + ">"; 250 }).join(""); 251 } 252 } 253 254 /* Service times grid */ 255 var timesRoot = document.getElementById("times-root"); 256 if (timesRoot && C.serviceTimes) { 257 timesRoot.innerHTML = C.serviceTimes.map(function (t, i) { 258 return '<div class="time-card reveal" style="--d:' + (i * 0.05) + 's">' + 259 '<div class="tc-day">' + esc(t.day) + "</div>" + 260 '<div class="tc-name">' + esc(t.name) + "</div>" + 261 '<div class="tc-time">' + esc(t.time) + "</div>" + 262 "</div>"; 263 }).join(""); 264 } 265 266 /* Hero times pill */ 267 var heroTimesText = document.getElementById("hero-times-text"); 268 if (heroTimesText && C.heroTimesLine) heroTimesText.textContent = C.heroTimesLine; 269 270 /* Re-observe anything we just injected */ 271 if ("IntersectionObserver" in window && !reduceMotion) { 272 var io2 = new IntersectionObserver(function (entries) { 273 entries.forEach(function (e) { 274 if (e.isIntersecting) { e.target.classList.add("in"); io2.unobserve(e.target); } 275 }); 276 }, { rootMargin: "0px 0px -8% 0px", threshold: 0.08 }); 277 document.querySelectorAll(".reveal:not(.in)").forEach(function (n) { io2.observe(n); }); 278 } else { 279 document.querySelectorAll(".reveal").forEach(function (n) { n.classList.add("in"); }); 280 } 281 282 /* ââ Contact form â Netlify Forms (AJAX, in-page success) ââââââââââââ */ 283 284 var contactForm = document.getElementById("contact-form"); 285 if (contactForm) { 286 contactForm.addEventListener("submit", function (ev) { 287 ev.preventDefault(); 288 var btn = contactForm.querySelector('button[type="submit"]'); 289 var note = contactForm.querySelector(".form-note"); 290 var body = new URLSearchParams(new FormData(contactForm)).toString(); 291 292 if (btn) { btn.disabled = true; btn.dataset.label = btn.textContent; btn.textContent = "Sendingâ¦"; } 293 if (note) { note.style.color = ""; note.textContent = "Weâll reply by email as soon as we can."; } 294 295 fetch("/", { 296 method: "POST", 297 headers: { "Content-Type": "application/x-www-form-urlencoded" }, 298 body: body 299 }).then(function (res) { 300 if (!res.ok) throw new Error("HTTP " + res.status); 301 contactForm.innerHTML = 302 '<div class="form-success">' + 303 "<h3>Thank you!</h3>" + 304 "<p>Your message is on its way. One of our team members will reach out by email as soon as we can.</p>" + 305 "</div>"; 306 }).catch(function () { 307 if (btn) { btn.disabled = false; btn.textContent = btn.dataset.label || "Send Message"; } 308 if (note) { 309 note.style.color = "#c0392b";
310 note.textContent = "Sorry, something went wrong. Please email [email protected] or call (888) 391-2262."; 311 } 312 }); 313 }); 314 } 315 316 /* ââ Midweek schedule: strike out Wednesdays once they have happened âââ 317 Each row carries a data-date. A night stays un-struck all day Wednesday 318 and only reads as done from the Thursday, so the current week never looks 319 finished while the service is still going on. Uses the visitor's own 320 local date, which is what they would expect to see. */ 321 322 var schedRows = document.querySelectorAll(".sched-list li[data-date]"); 323 if (schedRows.length) { 324 var startOfToday = new Date(); 325 startOfToday.setHours(0, 0, 0, 0); 326 327 schedRows.forEach(function (li) { 328 var p = li.getAttribute("data-date").split("-"); 329 // Build it from parts rather than new Date("2026-08-12"): the string 330 // form is parsed as UTC midnight, which lands on the previous day for 331 // anyone west of Greenwich (including Arkansas). 332 var doneFrom = new Date(+p[0], +p[1] - 1, +p[2]); 333 doneFrom.setDate(doneFrom.getDate() + 1); 334 if (startOfToday >= doneFrom) li.classList.add("is-past"); 335 }); 336 } 337 338 /* ââ Footer year âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 339 340 document.querySelectorAll("[data-year]").forEach(function (n) { 341 n.textContent = String(new Date().getFullYear()); 342 }); 343 344 /* ââ Smooth scroll (Lenis): DESKTOP ONLY ââââââââââââââââââââââââââââââ 345 Interpolates the scroll position so wheel/trackpad input glides instead 346 of jumping in discrete steps. Phones keep their native momentum scroll 347 (better on touch), and it's skipped for reduced-motion users. */ 348 349 // Enable on real-pointer devices (mouse/trackpad) regardless of window size, 350 // so a narrow desktop window still gets smooth scroll. Touch phones/tablets 351 // report (pointer: coarse)/(hover: none) and keep native momentum scrolling. 352 var hasFinePointer = window.matchMedia("(hover: hover) and (pointer: fine)").matches; 353 354 if (hasFinePointer && !reduceMotion && typeof window.Lenis === "function") { 355 var lenis = new window.Lenis({ 356 lerp: 0.075, // lower = more glide/fluidity; higher = snappier 357 wheelMultiplier: 0.75, // lower = less distance per wheel notch/flick 358 smoothWheel: true 359 }); 360 361 function raf(time) { 362 lenis.raf(time); 363 window.requestAnimationFrame(raf); 364 } 365 window.requestAnimationFrame(raf); 366 367 // Route same-page anchor links (#section) through Lenis so they glide too. 368 // Skips bare "#" links (e.g. the Pushpay campus tiles, which manage their 369 // own clicks) and anything explicitly opted out with data-lenis-external. 370 document.querySelectorAll('a[href^="#"]').forEach(function (link) { 371 var hash = link.getAttribute("href"); 372 if (hash.length <= 1 || link.hasAttribute("data-lenis-external")) return; 373 link.addEventListener("click", function (e) { 374 var target = document.querySelector(hash); 375 if (!target) return; 376 e.preventDefault(); 377 lenis.scrollTo(target, { offset: -70 }); 378 }); 379 }); 380 381 // When arriving from another page with a #hash (e.g. midweek.html#youth), 382 // glide to it once the layout settles. 383 if (window.location.hash) { 384 var deep = document.querySelector(window.location.hash); 385 if (deep) { 386 window.requestAnimationFrame(function () { 387 lenis.scrollTo(deep, { offset: -70, immediate: true }); 388 }); 389 } 390 } 391 } 392 393 /* ââ Giving page: remove the Pushpay widget's internal scrollbar ââââââââ 394 Pushpay's embedded form renders inside a shadow root and caps its own 395 content area at a fixed height (a ".scollable-content" div with its own 396 overflow-y: auto) â so the form scrolls inside a small box instead of 397 showing in full. That style lives inside their shadow DOM, so it can't be 398 reached with a normal stylesheet selector; we neutralize it in JS with an 399 !important inline override instead. Reruns on every campus 400 switch/reselect, since Pushpay tears down and rebuilds the widget each 401 time. */ 402 403 if (document.getElementById("pushpay-embedded-giving")) { 404 // Only touch the element if it isn't already flattened, and never observe 405 // "attributes" on it â since our own fix writes to the style attribute,
406 // watching attributes would make the observer re-trigger itself in an 407 // infinite loop (this froze the tab, worst on Safari). childList/subtree 408 // alone still catches Pushpay replacing the whole node when the form loads. 409 var flattenGivingWidget = function () { 410 var host = document.getElementById("pushpay-embedded-giving"); 411 var root = host && host.shadowRoot; 412 var el = root && root.querySelector(".scollable-content"); 413 if (el && el.style.maxHeight !== "none") { 414 el.style.setProperty("max-height", "none", "important"); 415 el.style.setProperty("overflow-y", "visible", "important"); 416 el.style.setProperty("overflow", "visible", "important"); 417 } 418 }; 419 420 var activeGivingObserver = null; 421 422 // Pushpay rebuilds the shadow DOM in stages (skeleton -> loaded form), so 423 // a one-shot fix can lose the race. Watch the shadow root and reapply the 424 // override for a while after each campus (re)selection. 425 var watchGivingWidget = function () { 426 if (activeGivingObserver) { activeGivingObserver.disconnect(); activeGivingObserver = null; } 427 428 var attachTries = 0; 429 var attachId = setInterval(function () { 430 attachTries++; 431 var host = document.getElementById("pushpay-embedded-giving"); 432 var root = host && host.shadowRoot; 433 if (root) { 434 clearInterval(attachId); 435 flattenGivingWidget(); 436 var mo = new MutationObserver(flattenGivingWidget); 437 mo.observe(root, { childList: true, subtree: true }); 438 activeGivingObserver = mo; 439 window.setTimeout(function () { // widget is static once loaded 440 if (activeGivingObserver === mo) { mo.disconnect(); activeGivingObserver = null; } 441 }, 15000); 442 } else if (attachTries > 60) { 443 clearInterval(attachId); // ~9s safety timeout, no widget host found 444 } 445 }, 150); 446 }; 447 448 watchGivingWidget(); // covers a cookie-restored campus on page load 449 document.querySelectorAll('.pushpay-listing-select-menu li a, #pushpay-change-campus-link') 450 .forEach(function (el) { el.addEventListener("click", watchGivingWidget); }); 451 } 452})();
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.