Skip to content
`${ic("phone")}${label || "Call " + phone()}`;
// Secondary action: external booking (salons, shops) or the request form on #/contact.
const BOOK = B.booking?.url ? B.booking : null;
const actionHref = () => BOOK ? url(BOOK.url) : "#/contact";
const actionAttrs = () => BOOK ? ` target="_blank" rel="noopener noreferrer" data-track="tap_book"` : "";
const actionLabel = (short) => esc(BOOK ? (short ? BOOK.short || S.bookShort : BOOK.label || S.bookLabel) : short ? S.ctaShort : S.ctaLabel);
const secondaryCta = (cls = "btn-ghost btn-lg") => `${ic(BOOK ? "cal" : S.ctaIcon || "doc")}${actionLabel()}`;
const proofHtml = () => list(B.proof).map(([i, t]) => `${ic(i)}${esc(t)}`).join("");
const head = (eyebrow, id, title, lead, href, linkText) => `
${eyebrow && esc(eyebrow).toLowerCase() !== String(title).toLowerCase() ? `
${esc(eyebrow)}` : ""}
${title}
${lead ? `
${lead}
` : ""}
${href ? `
${esc(linkText)} ${ic("arrow")}` : ""}
`;
/* ---------- section renderers (home + reused on pages) ---------- */
function heroHome(overlap){
const h = B.hero, img = h.image?.src;
const copy = `
${h.kicker ? `
${esc(h.kicker)}` : ""}
${escEm(h.h1)}
${h.sub ? `
${esc(h.sub)}
` : ""}
${callBtn()}${secondaryCta()}
${has(B.proof) ? `
` : ""}
`;
const bg = img ? `
` : "";
const cls = `hero v-hero-${L.hero} on-dark ${overlap ? "has-overlap" : ""}`;
if (L.hero === "split") return ``;
if (L.hero === "form") return `${bg}${copy}
${quoteForm(true)}
`;
return ``;
}
const SECTIONS = {
hero: (o) => heroHome(o.overlap),
picker: () => has(B.picker) ? `${t("pickerTitle")}
${S.pickerSub ? `
${t("pickerSub")}
` : ""}
` : "",
audience: () => has(B.audiences) ? `${t("audienceTitle")}
${S.audienceSub ? `
${t("audienceSub")}
` : ""}
` : "",
trust: () => trustSection(),
services: () => `${head(fmt(S.servicesEyebrow), "h-svc", t("servicesTitle"), S.servicesLead ? t("servicesLead") : "", "#/services", fmt(S.allServices))}${servicesBlock(B.services)}
`,
band: () => B.flags?.emergency247 || B.flags?.band ? `${t("bandTitle")}
${t("bandText")}
${callBtn()}
` : "",
work: () => has(B.projects, 3) ? `${head(fmt(S.workEyebrow), "h-work", t("workTitle"), S.workLead ? t("workLead") : "", "#/work", fmt(S.seeAll, { n:B.projects.length }))}${workGrid(B.projects.slice(0, L.work === "mosaic" ? 5 : 4).map((_, i) => i), true)}
` : "",
process: () => processSection(),
about: () => B.about ? `${esc(B.about.eyebrow || fmt(S.aboutEyebrow))}${esc(B.about.title)}
${list(B.about.text).slice(0, 2).map(p => `
${esc(p)}
`).join("")}${B.about.person ? personHtml(B.about.person) : ""}
${B.images?.about ? `
` : ""}
` : "",
reviews: () => hasPage("reviews") || has(B.reviews?.items) ? reviewsSection(true) : "",
clients: () => has(B.clients) ? `${head(fmt(S.clientsEyebrow), "h-clients", t("clientsTitle"), "", "", "")}${clientsHtml()}
` : "",
specials: () => has(B.specials) ? `${head(fmt(S.specialsEyebrow), "h-spec", t("specialsTitle"), "", "", "")}${couponsHtml()}
` : "",
area: () => has(B.serviceArea?.cities) ? `${head(fmt(S.areaEyebrow), "h-area", t("areaTitle", { area:B.serviceArea.label || "" }), "", "", "")}
${B.serviceArea.cities.map(c => `- ${ic("pin")}${esc(c)}
`).join("")}
` : "",
cta: () => ctaSection()
};
function trustSection(){
if (L.trust === "stats" && has(B.stats)) return `${B.stats.map(([b, s]) => `
${esc(b)}${esc(s)}
`).join("")}
`;
if (!has(B.trust)) return "";
const v = L.trust === "inline" ? "inline" : "strip";
return `${B.trust.map(([i, t, d]) => `- ${v === "strip" ? `${ic(i)}` : ic(i)}${esc(t)}${d ? `${esc(d)}` : ""}
`).join("")}
`;
}
function servicesBlock(items, variant = L.services){
const n = items.length;
if (variant === "rows") return ``;
if (variant === "featured" && n >= 3){
const f = items.find(s => s.featured) || items[0], rest = items.filter(s => s !== f);
const fimg = f.image || (has(f.projects) ? B.projects[f.projects[0]]?.src : "");
return ``;
}
return ``;
}
function workGrid(idx, teaser){
return `${idx.map(i => { const p = B.projects[i], s = p.service ? svcBy(p.service) : null; return `
${p.title || p.where || s ? `${p.title ? `${esc(p.title)}` : ""}${p.where ? esc(p.where) : ""}${s && !teaser ? ` · ${esc(s.title)}` : ""}` : ""}`; }).join("")}
`;
}
function processSection(alt = true){
const pr = B.process; if (!pr || !has(pr.steps)) return "";
const steps = `${pr.steps.map(s => `${esc(s.t)}${esc(s.d)}
`).join("")}
`;
const title = pr.title ? esc(pr.title) : t("processTitle");
if (L.process === "timeline") return `${S.processEyebrow !== (pr.title || S.processTitle) ? `${t("processEyebrow")}` : ""}
${title}
${pr.quote ? `
${esc(pr.quote)}
— ${esc(biz.name)}` : ""}
${steps}
`;
return `${head(fmt(S.processEyebrow), "h-proc", title, pr.quote ? esc(pr.quote) : "", "", "")}${steps}
`;
}
const revCard = (r, light) => `${esc(r.text)}
${esc(initials(r.author))}${esc(r.author)}${esc([r.where, B.reviews.source, r.date ? fmtDate(r.date) : ""].filter(Boolean).join(" · "))}`;
function reviewsBody(items, light){
if (L.reviews === "spotlight" && items.length >= 2) return `${revCard(items[0], light)}
${items.slice(1, 4).map(r => revCard(r, light)).join("")}
`;
if (L.reviews === "wall") return `${items.map(r => revCard(r, light)).join("")}
`;
return `${items.map(r => revCard(r, light)).join("")}
`;
}
function reviewsSection(teaser){
const R = B.reviews; if (!has(R?.items)) return "";
const items = teaser ? R.items.slice(0, L.reviews === "spotlight" ? 4 : 3) : R.items;
return ``;
}
const clientsHtml = () => ``;
const couponsHtml = () => `${B.specials.map(c => `
${c.amount ? `
${esc(c.amount)}` : ""}
${esc(c.title)}${esc(c.terms || "")}
`).join("")}
`;
const personHtml = (p) => `${esc(initials(p.name))}${esc(p.name)}${esc(p.role || "")}
`;
function ctaSection(title){
const tt = title || t("ctaTitle");
const sub = B.copy?.ctaText || B.labels?.ctaText ? t("ctaText") : BOOK ? t("ctaTextBook") : B.flags?.emergency247 ? t("ctaText247") : t("ctaText");
if (L.cta === "card") return ``;
return `${callBtn()}${secondaryCta()}
`;
}
function quoteForm(compact){
const name = "quote-" + B.slug;
const svcOpts = list(B.services).map(s => ``).join("");
const id = compact ? "hf" : "cf";
return ``;
}
/* ---------- pages ---------- */
function pageHero(titleHtml, lead, crumbs, opts = {}){
const trail = [[fmt(S.home), "#/"], ...crumbs];
return `
${opts.img && L.pageHero === "dark" ? `
` : ""}
${titleHtml}
${lead ? `
${lead}
` : ""}
${callBtn()}${opts.noQuote ? "" : secondaryCta(L.pageHero === "dark" ? "btn-ghost btn-lg" : "btn-outline btn-lg")}
${opts.proof && has(B.proof) ? `
` : ""}
`;
}
function pHome(){
const order = has(D.home) ? D.home : ["hero","picker","trust","services","band","work","process","about","reviews","clients","cta"];
return order.map((id, i) => {
if (id === "hero") return SECTIONS.hero({ overlap: ["picker","audience"].includes(order[i + 1]) && (order[i + 1] === "picker" ? has(B.picker) : has(B.audiences)) });
return SECTIONS[id] ? SECTIONS[id]({}) : "";
}).join("");
}
function pServices(){
return pageHero(t("servicesPageTitle"), S.servicesLead ? t("servicesLead") : "", [[fmt(S.servicesNav)]], { proof:true, img:B.hero.image?.src }) +
`${B.copy?.servicesText ? `
${esc(B.copy.servicesText)}
` : ""}${servicesBlock(B.services, L.services === "featured" ? "cards" : L.services)}${B.servicesNote ? `
${esc(B.servicesNote)}
` : ""}
` +
SECTIONS.band() + processSection() + ctaSection();
}
function pService(s){
const others = B.services.filter(x => x !== s);
const img = s.image || (has(s.projects) ? B.projects[s.projects[0]]?.full || B.projects[s.projects[0]]?.src : B.hero.image?.src);
const steps = has(s.steps) ? s.steps : B.process?.steps || [];
const aside = ``;
return pageHero(`${esc(s.title)}${biz.address?.city ? ` in ${city()}, ${st()}` : ""}`, esc(s.intro || s.short), [[fmt(S.servicesNav), "#/services"], [s.title]], { proof:true, img }) + `
${t("whatWeDo")}
${list(s.body).map(p => `
${esc(p)}
`).join("")}
${has(s.list) ? `
${s.listTitle ? esc(s.listTitle) : t("listTitle")}
${s.list.map(x => `- ${ic(s.warn ? "alert" : "ok")}${esc(x)}
`).join("")}
` : ""}
${has(steps) ? `
${t("stepsTitle")}
${steps.map(x => `${esc(x.t)}${esc(x.d)}
`).join("")}
` : ""}
${has(s.projects) ? `
${t("relatedWork")}
${workGrid(s.projects, false)}
` : ""}
${has(s.faq) ? `
${t("questions")}
${s.faq.map(([q, a], i) => `
${esc(q)}${ic("plus")}
${esc(a)}
`).join("")}
` : ""}
${aside}
` + SECTIONS.band() + `
${head(fmt(S.moreEyebrow), "h-more", t("moreTitle"), "", "#/services", fmt(S.allServices))}${servicesBlock(others, "cards")}
` + ctaSection();
}
function pWork(){
return pageHero(t("workTitle"), S.workLead ? t("workLead") : "", [[fmt(S.workNav)]], { img:B.projects[0]?.full || B.projects[0]?.src }) +
`${workGrid(B.projects.map((_, i) => i), false)}
` +
(has(B.clients) ? SECTIONS.clients() : "") + ctaSection();
}
function pReviews(){
const R = B.reviews;
return pageHero(B.copy?.reviewsTitle ? t("reviewsTitle") : t("reviewsPageTitle"), esc([R.headline, R.summary].filter(Boolean).join(" — ")), [[fmt(S.reviewsNav)]]) +
`` + ctaSection();
}
function pFaq(){
return pageHero(t("faqTitle"), "", [[fmt(S.faqNav)]]) + `
${t("faqAsideTitle")}
${t("faqAsideText")}
${callBtn("btn-accent btn-lg")}
${B.faq.map(([q, a], i) => `
${esc(q)}${ic("plus")}
`).join("")}
` + ctaSection();
}
function pAbout(){
const A = B.about;
return pageHero(esc(A.pageTitle || A.title), esc(A.lead || ""), [[fmt(S.aboutNav)]], { proof:true, img:B.images?.about }) + `
${esc(A.eyebrow || fmt(S.storyEyebrow))}${esc(A.title)}
${list(A.text).map(p => `
${esc(p)}
`).join("")}${A.person ? personHtml(A.person) : ""}
${B.images?.about ? `
` : ""}
${has(B.stats) ? `${B.stats.map(([b, s]) => `
${esc(b)}${esc(s)}
`).join("")}
` : ""}
${has(A.values) ? `${head("", "h-values", (A.valuesTitle ? esc(A.valuesTitle) : t("valuesTitle")), "", "", "")}
${A.values.map(([t, d]) => `
`).join("")}
` : ""}
${A.mission ? `${t("missionEyebrow")}
${esc(A.mission.title)}
` : ""}
${has(A.team) ? `${head("", "h-team", t("teamTitle"), "", "", "")}
${A.team.map(([n, r]) => `
${esc(initials(n))}${esc(n)}${esc(r)}
`).join("")}
` : ""}
${has(B.specials) ? SECTIONS.specials() : ""}` + ctaSection();
}
function pContact(){
const c = B.contact || {}, s = biz.socials || [];
return pageHero(esc(c.pageTitle || `Contact ${biz.name}`), esc(c.lead || ""), [[fmt(S.contactNav)]], { noQuote:true }) + `
`;
}
/* ---------- routes ---------- */
const ROUTES = {};
const PAGES = []; // nav pages in order
const add = (path, nav, title, desc, render, label) => { ROUTES[path] = { nav, title, desc, render }; if (label) PAGES.push([path, label, nav]); };
const T = (t) => `${t} | ${biz.name}`;
function hasPage(k){ return Object.prototype.hasOwnProperty.call(ROUTES, k); }
function buildRoutes(){
add("", "home", B.seo?.title || biz.name, B.seo?.description || "", pHome);
add("services", "services", T(fmt(S.servicesMetaTitle)), `${list(B.services).map(s => s.title).join(", ")}. Call ${biz.phone.display}.`, pServices);
list(B.services).forEach(s => add("services/" + s.slug, "services", T(`${s.title}${biz.address?.city ? ` in ${biz.address.city}, ${biz.address.state}` : ""}`), `${s.intro || s.short} Call ${biz.phone.display}.`, () => pService(s)));
if (has(B.projects, 3)) add("work", "work", T(fmt(S.workNav)), `${fmt(S.workTitle)} — ${biz.name}.`, pWork, fmt(S.workNav));
if (has(B.reviews?.items, 2)) add("reviews", "reviews", T(fmt(S.reviewsNav)), `What customers say about ${biz.name}.`, pReviews, fmt(S.reviewsNav));
if (B.about) add("about", "about", T(fmt(S.aboutNav)), B.about.lead || `About ${biz.name}.`, pAbout, fmt(S.aboutNav));
if (has(B.faq, 3)) add("faq", "faq", T(fmt(S.faqNav)), `Answers about ${biz.name}.`, pFaq, fmt(S.faqNav));
add("contact", "contact", T(fmt(S.contactNav)), `Call ${biz.phone.display} or send a request.`, pContact, fmt(S.contactNav));
}
/* ---------- chrome ---------- */
const logoHtml = () => B.logo?.src
? `
`
: `${esc(B.logo?.text || biz.name)}${B.logo?.sub ? `${esc(B.logo.sub)}` : ""}`;
function navHtml(){
const dd = ``;
return ``;
}
function renderTop(){
const banner = B.mode === "demo" ? `` : "";
const ubar = has(B.utility) ? `${B.utility.filter(u => !u.right).map(u => `- ${u.strong ? `${ic(u.icon)}${esc(u.t)}` : `${ic(u.icon)}${esc(u.t)}`}
`).join("")}
${B.utility.filter(u => u.right).map(u => `- ${u.href ? `${ic(u.icon)}${esc(u.t)}` : `${ic(u.icon)}${esc(u.t)}`}
`).join("")}
` : "";
const phoneBtn = L.header === "stacked"
? `${ic("phone")}${B.flags?.emergency247 ? t("call247") : t("callUs")}${phone()}`
: `${ic("phone")}${phone()}`;
const mnav = `${t("servicesNav")} ${ic("chev")}
${PAGES.map(([p, label]) => `${esc(label)} ${ic("right")}`).join("")}`;
$("#top").innerHTML = banner + ubar + `
`;
}
function renderBottom(){
const s = biz.socials || [];
const nap = `${phone()}${biz.email ? `
${esc(biz.email)}` : ""}
${esc(biz.address?.street ? addrLine() : [biz.address?.city, biz.address?.state].filter(Boolean).join(", "))}${has(biz.hours) ? `
${biz.hours.map(h => `${esc(h.days)} ${esc(h.open)}–${esc(h.close)}`).join(" · ")}` : ""}${B.license?.number ? `
${esc(B.license.type)} #${esc(B.license.number)}` : ""}
`;
const pages = PAGES.map(([p, label]) => `${esc(label)}`).join("");
const foot = `© ${new Date().getFullYear()} ${esc(biz.name)}. All rights reserved.${B.mode === "demo" ? `Redesign concept by ${esc(B.demo?.author || "")} — not the official website.` : ""}
`;
const body = L.footer === "compact"
? `${logoHtml().replace('class="brand"', 'class="brand" style="margin:0"')}${nap}
`
: `${logoHtml().replace('class="brand"', 'class="brand" style="margin:0"')}${biz.tagline ? `
${esc(biz.tagline)}
` : ""}${nap}
${has(s) ? `
${B.contact?.socialTitle ? esc(B.contact.socialTitle) : t("followUs")}
` : ""}${directions() ? `
${t("directions")}
` : ""}
`;
$("#bottom").innerHTML = `
`;
}
/* ---------- router ---------- */
function route(){
const path = location.hash.replace(/^#\/?/, "").replace(/\/+$/, "");
const r = ROUTES[path] || ROUTES[""];
closeMenus();
$("#app").innerHTML = r.render();
document.title = r.title;
$('meta[name="description"]')?.setAttribute("content", r.desc);
const cur = "#/" + (ROUTES[path] ? path : "");
$$("[data-nav]").forEach(el => { const on = el.dataset.nav === r.nav; if (el.tagName === "BUTTON") el.classList.toggle("is-current", on); else if (on) el.setAttribute("aria-current", "page"); else el.removeAttribute("aria-current"); });
$$("[data-nav-path]").forEach(a => { if (a.dataset.navPath === cur) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current"); });
window.scrollTo({ top:0, behavior:"instant" });
if (path) $("#h1")?.focus({ preventScroll:true });
afterRender();
}
/* ---------- interactions ---------- */
let io;
function afterRender(){
io?.disconnect();
const cb = $("#callbar"), heroCta = $("[data-hero-cta]");
if (cb && heroCta){ io = new IntersectionObserver(([e]) => { const show = !e.isIntersecting && e.boundingClientRect.top < 0; cb.classList.toggle("show", show); cb.setAttribute("aria-hidden", String(!show)); $$("a", cb).forEach(a => a.tabIndex = show ? 0 : -1); }); io.observe(heroCta); }
$$("form[data-form]").forEach(initForm);
}
const hdr = () => $("#hdr");
let engaged = false;
function onScroll(){ hdr()?.classList.toggle("scrolled", scrollY > 4); if (!engaged && (scrollY + innerHeight) / document.documentElement.scrollHeight > .5){ engaged = true; track("engaged"); } }
function openMenu(open){
const btn = $("#burger"), nav = $("#mnav"), scrim = $("#scrim");
if (!btn || open === (btn.getAttribute("aria-expanded") === "true")) return;
if (open){ nav.style.setProperty("--mnav-top", Math.max(0, hdr().getBoundingClientRect().bottom) + "px"); nav.hidden = false; $$("details", nav).forEach(d => { d.open = !!d.querySelector('[aria-current="page"]'); }); requestAnimationFrame(() => { nav.classList.add("open"); scrim.classList.add("on"); }); document.documentElement.classList.add("lock"); btn.setAttribute("aria-expanded", "true"); btn.setAttribute("aria-label", "Close menu"); }
else { nav.classList.remove("open"); scrim.classList.remove("on"); document.documentElement.classList.remove("lock"); btn.setAttribute("aria-expanded", "false"); btn.setAttribute("aria-label", "Menu"); setTimeout(() => { if (!nav.classList.contains("open")) nav.hidden = true; }, 260); }
}
let ddTimer, ddHoverAt = 0;
function openDD(dd, open){ clearTimeout(ddTimer); $$(".dd").forEach(d => { const on = open && d === dd; d.classList.toggle("open", on); $("button", d).setAttribute("aria-expanded", String(on)); }); }
function closeMenus(){ openMenu(false); openDD(null, false); }
let lbList = [], lbIdx = 0, lbReturn = null;
function lbShow(i){ lbIdx = (i + lbList.length) % lbList.length; const p = B.projects[lbList[lbIdx]], img = $("#lb-img"); img.src = p.full || p.src; img.alt = p.alt; $("#lb-cap").textContent = `${p.title ? p.title + " · " : ""}${lbIdx + 1} / ${lbList.length}`; }
function lbOpen(btn){ const grid = btn.closest(".work-grid"); lbList = $$("[data-photo]", grid).map(b => +b.dataset.photo); lbReturn = btn; lbShow(lbList.indexOf(+btn.dataset.photo)); const lb = $("#lb"); lb.showModal ? lb.showModal() : lb.setAttribute("open", ""); document.documentElement.classList.add("lock"); }
function initForm(f){
if (f.dataset.ready) return; f.dataset.ready = "1";
const rules = { name: v => v.trim().length >= 2 || "Please enter your name.", phone: v => v.replace(/\D/g, "").length >= 10 || "Please enter a 10-digit phone number.", email: v => !v || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || "That email doesn’t look right." };
const check = el => { const r = rules[el.name]; if (!r) return true; const res = r(el.value), field = el.closest(".field"), err = $(".err", field); if (res === true){ field.removeAttribute("data-invalid"); el.removeAttribute("aria-invalid"); if (err) err.textContent = ""; return true; } field.setAttribute("data-invalid", ""); el.setAttribute("aria-invalid", "true"); if (err) err.textContent = res; return false; };
const fields = () => $$("input[name=name], input[name=phone], input[name=email]", f);
fields().forEach(el => el.addEventListener("blur", () => { if (el.value || el.required) check(el); }));
f.addEventListener("submit", async e => {
e.preventDefault();
if (!fields().map(check).every(Boolean)){ fields().find(el => el.getAttribute("aria-invalid") === "true")?.focus(); return; }
if (f.company.value) return;
const status = $(".form-status", f), btn = $("button[type=submit]", f), label = btn.textContent;
btn.disabled = true; btn.textContent = "Sending…";
const fd = new FormData(f); fd.append("form-name", f.dataset.form);
try { if (location.protocol === "file:") console.info("[form] local file — submission skipped"); else { const res = await fetch("/", { method:"POST", headers:{ "Content-Type":"application/x-www-form-urlencoded" }, body:new URLSearchParams(fd).toString() }); if (!res.ok) throw new Error(res.status); }
status.className = "form-status ok"; status.innerHTML = `Thanks — we’ll get back to you shortly. Need help now? Call ${phone()}.`; f.reset(); track("form_submit"); }
catch { status.className = "form-status bad"; status.innerHTML = `Sorry, that didn’t go through. Please call us: ${phone()}.`; }
finally { btn.disabled = false; btn.textContent = label; }
});
}
/* ---------- tracking (ntfy.sh) ---------- */
function isOwner(){ const p = new URLSearchParams(location.search); try { if (p.get("me") === "1") localStorage.setItem("demo-owner", "1"); if (p.get("me") === "0") localStorage.removeItem("demo-owner"); return localStorage.getItem("demo-owner") === "1"; } catch { return false; } }
function track(event){
const t = B.tracking;
if (!t?.enabled || !t.ntfyTopic || isOwner() || navigator.webdriver || location.protocol === "file:") return;
try { if (sessionStorage.getItem("trk:" + event)) return; sessionStorage.setItem("trk:" + event, "1"); } catch {}
const msg = `${biz.name} — ${event} — ${new Date().toISOString()}`, endpoint = "https://ntfy.sh/" + encodeURIComponent(t.ntfyTopic);
if (!(navigator.sendBeacon && navigator.sendBeacon(endpoint, msg))) fetch(endpoint, { method:"POST", body:msg, keepalive:true, mode:"no-cors" }).catch(() => {});
}
function initTracking(){ isOwner(); let shown = 0; const tick = () => { if (document.visibilityState === "visible" && ++shown === 3) track("opened"); if (shown < 3) setTimeout(tick, 1000); }; setTimeout(tick, 1000); setTimeout(() => { if (!engaged && document.visibilityState === "visible"){ engaged = true; track("engaged"); } }, 20000); }
function bindGlobal(){
addEventListener("hashchange", route);
addEventListener("scroll", onScroll, { passive:true });
document.addEventListener("click", e => {
const t = e.target;
const tr = t.closest("[data-track]"); if (tr) track(tr.dataset.track);
if (t.closest("#burger")){ openMenu($("#burger").getAttribute("aria-expanded") !== "true"); return; }
if (t.closest("#scrim")){ openMenu(false); return; }
const ddBtn = t.closest(".dd > button"); if (ddBtn){ const dd = ddBtn.parentElement; openDD(dd, Date.now() - ddHoverAt < 500 ? true : !dd.classList.contains("open")); return; }
if (!t.closest(".dd")) openDD(null, false);
const ph = t.closest("[data-photo]"); if (ph){ lbOpen(ph); return; }
const lbb = t.closest("[data-lb]"); if (lbb){ const a = lbb.dataset.lb; if (a === "close") $("#lb").close(); else lbShow(lbIdx + (a === "next" ? 1 : -1)); return; }
if (t.id === "lb") $("#lb").close();
const a = t.closest('a[href^="#/"]'); if (a && a.getAttribute("href") === location.hash){ e.preventDefault(); route(); }
});
if (matchMedia("(hover: hover)").matches) $$(".dd").forEach(dd => { dd.addEventListener("mouseenter", () => { if (!dd.classList.contains("open")) ddHoverAt = Date.now(); openDD(dd, true); }); dd.addEventListener("mouseleave", () => { ddTimer = setTimeout(() => openDD(null, false), 160); }); });
document.addEventListener("keydown", e => {
const openDd = $(".dd.open");
if (e.key === "Escape"){ if (openDd){ openDD(null, false); $("button", openDd).focus(); } if ($("#burger")?.getAttribute("aria-expanded") === "true"){ openMenu(false); $("#burger").focus(); } }
if (openDd && (e.key === "ArrowDown" || e.key === "ArrowUp")){ const items = $$(".dd-panel a", openDd), i = items.indexOf(document.activeElement); e.preventDefault(); items[(i + (e.key === "ArrowDown" ? 1 : -1) + items.length) % items.length]?.focus(); }
if (e.key === "Tab" && $("#burger")?.getAttribute("aria-expanded") === "true"){ const f = [$("#burger"), ...$$("#mnav summary, #mnav a")].filter(el => el.offsetParent !== null); const first = f[0], last = f[f.length - 1]; if (e.shiftKey && document.activeElement === first){ e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last){ e.preventDefault(); first.focus(); } }
if ($("#lb")?.open && (e.key === "ArrowRight" || e.key === "ArrowLeft")) lbShow(lbIdx + (e.key === "ArrowRight" ? 1 : -1));
});
$("#lb").addEventListener("close", () => { document.documentElement.classList.remove("lock"); lbReturn?.focus(); });
let sx = null; $("#lb-stage").addEventListener("pointerdown", e => { sx = e.clientX; }); $("#lb-stage").addEventListener("pointerup", e => { if (sx === null) return; const dx = e.clientX - sx; sx = null; if (Math.abs(dx) > 40) lbShow(lbIdx + (dx < 0 ? 1 : -1)); });
matchMedia("(min-width:1100px)").addEventListener("change", e => { if (e.matches) openMenu(false); });
}
function jsonLd(){
const a = biz.address || {};
const data = { "@context":"https://schema.org", "@type":B.business.schemaType || IND.schemaType || "LocalBusiness", name:biz.name, telephone:biz.phone.e164, email:biz.email || undefined, url:B.sourceSite,
address:{ "@type":"PostalAddress", streetAddress:a.street || undefined, addressLocality:a.city, addressRegion:a.state, postalCode:a.zip || undefined, addressCountry:"US" },
foundingDate: biz.founded ? String(biz.founded) : undefined, sameAs:list(biz.socials).map(s => s[1]),
aggregateRating: B.reviews?.rating && B.reviews?.count ? { "@type":"AggregateRating", ratingValue:B.reviews.rating, reviewCount:B.reviews.count } : undefined,
hasOfferCatalog:{ "@type":"OfferCatalog", name:fmt(S.catalogName), itemListElement:list(B.services).map(s => ({ "@type":"Offer", itemOffered:{ "@type":"Service", name:s.title } })) } };
const el = document.createElement("script"); el.type = "application/ld+json"; el.textContent = JSON.stringify(data).replace(/BUSINESS validation — ${errors.length} issue(s)
` + errors.map(x => "• " + esc(x)).join("
"); document.body.appendChild(d); } }
jsonLd(); bindGlobal(); route(); onScroll(); initTracking();
})();