/* ============================================================
PixelEstate β interactions (GSAP 3.15 + ScrollTrigger, Lenis 1.3)
Content is visible without JS; this file only enhances.
============================================================ */
(function () {
"use strict";
const root = document.documentElement;
root.classList.add("js");
const CFG = window.PIXELESTATE || {};
const LQIP = window.PE_LQIP || {};
const REDUCED = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const TOUCH = window.matchMedia("(hover: none), (pointer: coarse)").matches;
const $ = (s, c) => (c || document).querySelector(s);
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
gsap.registerPlugin(ScrollTrigger);
/* ---------- Lenis ---------- */
let lenis = null;
if (!REDUCED) {
lenis = new Lenis({ duration: 1.1, lerp: 0.1, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((t) => lenis.raf(t * 1000));
gsap.ticker.lagSmoothing(0);
}
const scrollToEl = (t) =>
lenis ? lenis.scrollTo(t, { offset: -64, duration: 1.1 })
: t.scrollIntoView({ behavior: REDUCED ? "auto" : "smooth", block: "start" });
/* ---------- blur-up ---------- */
function blurUp(scope) {
$$("img", scope).forEach((img) => {
const m = (img.getAttribute("src") || "").match(/\/(hero|g\d+|svc\d+)\.jpg$/);
const key = m && m[1];
if (key && LQIP[key] && !img.dataset.bu) {
img.dataset.bu = "1";
const holder = img.closest(".gal-item, .bg, .hero-media, .phone .screen") || img.parentElement;
holder.style.backgroundImage = `url(${LQIP[key]})`;
holder.style.backgroundSize = "cover";
holder.style.backgroundPosition = "center";
img.style.opacity = "0"; img.style.transition = "opacity .8s ease";
const show = () => (img.style.opacity = "1");
img.complete && img.naturalWidth ? show() : img.addEventListener("load", show, { once: true });
}
});
}
/* ---------- reveals ---------- */
function reveals() {
if (REDUCED) return;
$$(".reveal").forEach((el) =>
gsap.to(el, { opacity: 1, y: 0, filter: "blur(0px)", duration: 1.1, ease: "power4.out",
scrollTrigger: { trigger: el, start: "top 90%" } }));
const lines = $$(".statement .line i");
if (lines.length) {
gsap.set(lines, { yPercent: 110, opacity: 0 });
gsap.to(lines, { yPercent: 0, opacity: 1, duration: 1.2, ease: "power4.out", stagger: 0.12,
scrollTrigger: { trigger: "#statement", start: "top 72%" } });
}
}
/* ---------- brokerage logos ---------- */
function brokerages() {
const host = $("#logoGrid");
const list = CFG.brokerages || [];
// duplicate the set so the marquee can loop seamlessly
const one = list.map((b) => `
`).join("");
host.innerHTML = one + one;
if (REDUCED) { host.style.justifyContent = "center"; host.style.flexWrap = "wrap"; return; }
// continuous running marquee
let x = 0, half = 0, last = performance.now(), paused = false;
const speed = 42; // px per second
const measure = () => { half = host.scrollWidth / 2; };
measure(); window.addEventListener("resize", measure);
host.addEventListener("pointerenter", () => (paused = true));
host.addEventListener("pointerleave", () => (paused = false));
function tick(now) {
const dt = (now - last) / 1000; last = now;
if (!paused && half) { x -= speed * dt; if (-x >= half) x += half; host.style.transform = `translate3d(${x}px,0,0)`; }
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
/* ---------- HERO: real video scrub + fallback ---------- */
function hero() {
const heroImg = $("#heroImg");
const overlay = $("#heroOverlay");
const ind = $("#scrollInd");
const media = $("#heroMedia");
const vsrc = CFG.assets && CFG.assets.heroVideo;
const vwebm = CFG.assets && CFG.assets.heroVideoWebm;
let video = null, scrub = false;
const makeVideo = () => {
const v = document.createElement("video");
v.muted = true; v.playsInline = true; v.preload = "auto";
v.setAttribute("aria-hidden", "true");
if (CFG.assets.heroPoster) v.poster = CFG.assets.heroPoster;
// WebM first only as a fallback source; H.264 stays primary for universal support
const add = (src, type) => { const s = document.createElement("source"); s.src = src; s.type = type; v.appendChild(s); };
if (vsrc) add(vsrc, "video/mp4");
if (vwebm) add(vwebm, "video/webm");
return v;
};
if (vsrc && !TOUCH && !REDUCED) { // desktop: scrub the video
video = makeVideo();
media.replaceChild(video, heroImg);
scrub = true;
video.load();
} else if (vsrc && (TOUCH || REDUCED)) { // touch/reduced: autoplay loop
video = makeVideo();
video.loop = true; video.autoplay = true;
media.replaceChild(video, heroImg);
video.play().catch(() => {});
}
if (REDUCED) return;
const distance = TOUCH ? "+=140%" : "+=220%"; // ~300vh+ pinned
const target = video || heroImg;
gsap.timeline({
scrollTrigger: {
trigger: "#hero", start: "top top", end: distance, pin: "#heroPin", scrub: 0.6,
onUpdate: (self) => {
if (scrub && video && video.readyState >= 1 && video.duration) {
const t = self.progress * (video.duration - 0.05);
if (Math.abs(video.currentTime - t) > 0.01) video.currentTime = t;
}
},
},
})
.to(target, { scale: scrub ? 1.04 : 1.22, ease: "none" }, 0)
.to(overlay, { y: -90, opacity: 0, ease: "power2.in" }, 0)
.to(ind, { opacity: 0, ease: "power1.in" }, 0);
}
/* ---------- GALLERY: square grid + parallax + lightbox ---------- */
function gallery() {
const grid = $("#galGrid");
const items = CFG.gallery || [];
grid.innerHTML = items.map((g, i) =>
`
${g.t}${g.s}`).join("");
blurUp(grid);
if (!REDUCED) {
gsap.from($$(".gal-item", grid), { opacity: 0, y: 26, duration: 0.9, ease: "power3.out", stagger: 0.05,
scrollTrigger: { trigger: grid, start: "top 86%" } });
// scroll-linked parallax on each photo
$$(".gal-item img", grid).forEach((img) => {
gsap.fromTo(img, { yPercent: -2.4 }, { yPercent: 2.4, ease: "none",
scrollTrigger: { trigger: img.parentElement, start: "top bottom", end: "bottom top", scrub: true } });
});
}
// lightbox
const lb = $("#lightbox"), lbImg = $("#lbImg");
let idx = 0;
const open = (i) => { idx = (i + items.length) % items.length; lbImg.src = `assets/img/${items[idx].k}.jpg`;
lbImg.alt = items[idx].t; lb.classList.add("open"); lb.setAttribute("aria-hidden", "false"); };
const close = () => { lb.classList.remove("open"); lb.setAttribute("aria-hidden", "true"); };
grid.addEventListener("click", (e) => { const it = e.target.closest(".gal-item"); if (it) open(+it.dataset.i); });
$("#lbClose").addEventListener("click", close);
$("#lbPrev").addEventListener("click", (e) => { e.stopPropagation(); open(idx - 1); });
$("#lbNext").addEventListener("click", (e) => { e.stopPropagation(); open(idx + 1); });
lb.addEventListener("click", (e) => { if (e.target === lb) close(); });
document.addEventListener("keydown", (e) => {
if (!lb.classList.contains("open")) return;
if (e.key === "Escape") close(); else if (e.key === "ArrowRight") open(idx + 1); else if (e.key === "ArrowLeft") open(idx - 1);
});
}
/* ---------- reusable coverflow ---------- */
function makeCoverflow(cf, track, onCenter, startIndex) {
const items = () => Array.from(track.children);
function nearest() {
const r = cf.getBoundingClientRect(), cx = r.left + r.width / 2;
let b = 0, bd = 1e9;
items().forEach((f, i) => { const fr = f.getBoundingClientRect(); const d = Math.abs(fr.left + fr.width / 2 - cx); if (d < bd) { bd = d; b = i; } });
return b;
}
function mark() { const b = nearest(); items().forEach((f, i) => f.classList.toggle("is-center", i === b)); onCenter && onCenter(b); return b; }
function to(i) {
const els = items(); i = Math.max(0, Math.min(els.length - 1, i));
const f = els[i], fr = f.getBoundingClientRect(), cr = cf.getBoundingClientRect();
cf.scrollTo({ left: cf.scrollLeft + (fr.left - cr.left) - (cf.clientWidth - fr.width) / 2, behavior: "smooth" });
}
let raf = 0;
cf.addEventListener("scroll", () => { if (!raf) raf = requestAnimationFrame(() => { mark(); raf = 0; }); }, { passive: true });
let down = false, sx = 0, sy = 0, sl = 0, moved = false, pid = null, suppressClick = false;
cf.addEventListener("pointerdown", (e) => {
if (e.button && e.button !== 0) return;
down = true; moved = false; sx = e.clientX; sy = e.clientY; sl = cf.scrollLeft; pid = e.pointerId;
cf.classList.add("drag"); try { cf.setPointerCapture(e.pointerId); } catch (_) {}
});
cf.addEventListener("pointermove", (e) => {
if (!down) return; const dx = e.clientX - sx;
if (Math.abs(dx) > 10 || Math.abs(e.clientY - sy) > 10) moved = true;
cf.scrollLeft = sl - dx;
});
const end = (e) => {
if (!down) return; down = false; cf.classList.remove("drag");
try { cf.releasePointerCapture(pid); } catch (_) {}
if (!moved) {
// TAP: bring the tapped card to center (handled here, not via click β reliable with real mouse)
const el = document.elementFromPoint(e.clientX, e.clientY);
const card = el && el.closest(".pkg");
if (card && !card.classList.contains("is-center")) {
suppressClick = true;
to(items().indexOf(card));
}
}
};
cf.addEventListener("pointerup", end);
cf.addEventListener("pointercancel", end);
// suppress the click that follows a drag OR a tap-to-center (so buttons on a not-yet-centered card don't fire)
cf.addEventListener("click", (e) => { if (moved || suppressClick) { e.preventDefault(); e.stopPropagation(); suppressClick = false; } }, true);
window.addEventListener("resize", mark);
requestAnimationFrame(() => { to(startIndex || 0); setTimeout(mark, 80); });
return { mark, to, nearest };
}
/* ---------- PROCESS: step-to-step fill ---------- */
function process() {
const fill = $("#tlFill");
const steps = $$("#timeline [data-step]");
const vertical = () => window.matchMedia("(max-width:760px)").matches;
ScrollTrigger.create({
trigger: "#timeline", start: "top 78%", end: "bottom 32%", scrub: 0.7,
onUpdate: (self) => {
const p = REDUCED ? 1 : self.progress;
fill.style[vertical() ? "height" : "width"] = (p * 100).toFixed(1) + "%";
const n = steps.length;
steps.forEach((s, i) => s.classList.toggle("on", p >= i / (n - 1) - 0.02));
},
});
}
/* ---------- REELS ---------- */
function reels() {
const make = (sel, vsrc, poster) => {
const host = $(sel);
if (vsrc) {
const v = document.createElement("video");
v.src = vsrc; v.muted = true; v.loop = true; v.autoplay = true; v.playsInline = true; v.poster = poster || "";
const btn = document.createElement("button");
btn.className = "sound"; btn.textContent = "π"; btn.setAttribute("aria-label", "Unmute");
btn.addEventListener("click", () => { v.muted = !v.muted; btn.textContent = v.muted ? "π" : "π"; if (!v.muted) v.play(); });
host.append(v, btn); v.play().catch(() => {});
} else {
host.innerHTML =
`
βΆ
Drop reel1.mp4 / reel2.mp4
into /assets/video to go live `;
}
};
make("#reelA", CFG.assets.reel1, CFG.assets.reel1Poster);
make("#reelB", CFG.assets.reel2, CFG.assets.reel2Poster);
blurUp();
if (REDUCED) return;
$$(".phone").forEach((p) => {
const d = parseFloat(p.dataset.float) || 1;
gsap.to(p, { y: 18 * d, duration: 3.4, ease: "sine.inOut", yoyo: true, repeat: -1 });
gsap.to(p, { rotateZ: 1.4 * d, duration: 5, ease: "sine.inOut", yoyo: true, repeat: -1,
scrollTrigger: { trigger: "#reels", start: "top bottom", end: "bottom top", scrub: 1 } });
});
}
/* ---------- PACKAGES (real pricing + sqft tier selector) ---------- */
function packages() {
const track = $("#pkgTrack"), chipsHost = $("#pkgChips"), cf = $("#pkgCf");
const list = CFG.packages || [];
$("#pkgTotal").textContent = String(list.length).padStart(2, "0");
const check = ``;
track.innerHTML = list.map((p, pi) => {
const feats = (p.includes || []).map((f) => `${check}${f}`).join("");
const cta = (CFG.urls && CFG.urls.booking && CFG.urls.booking !== "#")
? `Book ${p.tier}`
: `Book ${p.tier}`;
return `
${p.badge ? `${p.badge}` : ""}
${p.tier}
${cta}`;
}).join("");
chipsHost.innerHTML = list.map((p, pi) => ``).join("");
const cards = () => [...track.children];
const feat = Math.max(0, list.findIndex((p) => p.featured));
const totalEl = $("#pkgTotal"), nowEl = $("#pkgNow");
if (totalEl) totalEl.textContent = String(list.length).padStart(2, "0");
const highlight = (i) => {
$$("#pkgChips button").forEach((c, j) => c.classList.toggle("active", j === i));
cards().forEach((c, j) => c.classList.toggle("is-center", j === i));
if (nowEl) nowEl.textContent = String(i + 1).padStart(2, "0");
};
// native horizontal scroll β nothing intercepts pointer events, so every card stays clickable
let lock = 0;
const goTo = (i) => {
const cs = cards(); i = Math.max(0, Math.min(cs.length - 1, i));
const card = cs[i]; if (!card) return;
highlight(i);
lock = Date.now() + 900; // keep the chosen card highlighted while it glides in
const max = cf.scrollWidth - cf.clientWidth;
const target = Math.max(0, Math.min(max, card.offsetLeft - (cf.clientWidth - card.offsetWidth) / 2));
cf.scrollTo({ left: target, behavior: REDUCED ? "auto" : "smooth" });
};
const nearest = () => {
const mid = cf.scrollLeft + cf.clientWidth / 2;
let best = 0, d = Infinity;
cards().forEach((c, i) => { const cm = c.offsetLeft + c.offsetWidth / 2; if (Math.abs(cm - mid) < d) { d = Math.abs(cm - mid); best = i; } });
return best;
};
chipsHost.addEventListener("click", (e) => {
const b = e.target.closest("button[data-chip]"); if (b) goTo(+b.dataset.chip);
});
const nextB = $("#pkgNext"), prevB = $("#pkgPrev");
if (nextB) nextB.addEventListener("click", () => goTo(nearest() + 1));
if (prevB) prevB.addEventListener("click", () => goTo(nearest() - 1));
let raf = 0;
cf.addEventListener("scroll", () => { if (!raf) raf = requestAnimationFrame(() => { if (Date.now() > lock) highlight(nearest()); raf = 0; }); }, { passive: true });
requestAnimationFrame(() => goTo(feat));
}
/* ---------- TESTIMONIALS: draggable, auto-scrolling carousel ---------- */
function testimonials() {
const scroll = $("#testiScroll"), track = $("#testiTrack");
if (!scroll || !track) return;
const list = CFG.reviews || [];
if (!list.length) return;
track.innerHTML = list.map((r) =>
`β
${r.q}
β
β
β
β
β
${r.n}${r.r}`).join("");
// drag to scroll
let down = false, sx = 0, sl = 0, moved = false;
scroll.addEventListener("pointerdown", (e) => { down = true; moved = false; sx = e.clientX; sl = scroll.scrollLeft; scroll.classList.add("drag"); scroll.setPointerCapture(e.pointerId); });
scroll.addEventListener("pointermove", (e) => { if (!down) return; const dx = e.clientX - sx; if (Math.abs(dx) > 10) moved = true; scroll.scrollLeft = sl - dx; });
const up = (e) => { if (!down) return; down = false; scroll.classList.remove("drag"); try { scroll.releasePointerCapture(e.pointerId); } catch (_) {} };
scroll.addEventListener("pointerup", up); scroll.addEventListener("pointercancel", up);
scroll.addEventListener("click", (e) => { if (moved) { e.preventDefault(); e.stopPropagation(); } }, true);
// gentle auto-advance; pauses on hover / interaction
if (!REDUCED) {
let paused = false, last = performance.now();
const speed = 26; // px/sec
scroll.addEventListener("pointerenter", () => (paused = true));
scroll.addEventListener("pointerleave", () => (paused = false));
scroll.addEventListener("pointerdown", () => (paused = true));
function tick(now) {
const dt = (now - last) / 1000; last = now;
if (!paused && !down) {
const max = scroll.scrollWidth - scroll.clientWidth;
if (max > 4) { scroll.scrollLeft += speed * dt; if (scroll.scrollLeft >= max - 1) scroll.scrollLeft = 0; }
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
if (!REDUCED)
gsap.from($$(".quote", track), { opacity: 0, y: 24, duration: 0.8, ease: "power3.out", stagger: 0.08,
scrollTrigger: { trigger: scroll, start: "top 88%" } });
}
/* ---------- tilt / magnetic ---------- */
function tilt() {
if (REDUCED || TOUCH) return;
$$("[data-tilt]").forEach((card) => {
card.addEventListener("pointermove", (e) => {
const r = card.getBoundingClientRect();
gsap.to(card, { rotateX: ((e.clientY - r.top) / r.height - 0.5) * -5, rotateY: ((e.clientX - r.left) / r.width - 0.5) * 5,
transformPerspective: 900, duration: 0.5, ease: "power3.out" });
});
card.addEventListener("pointerleave", () => gsap.to(card, { rotateX: 0, rotateY: 0, duration: 0.7, ease: "power3.out" }));
});
}
function magnetic() {
if (REDUCED || TOUCH) return;
$$("[data-magnetic]").forEach((el) => {
el.addEventListener("pointermove", (e) => {
const r = el.getBoundingClientRect();
gsap.to(el, { x: (e.clientX - r.left - r.width / 2) * 0.3, y: (e.clientY - r.top - r.height / 2) * 0.4, duration: 0.5, ease: "power3.out" });
});
el.addEventListener("pointerleave", () => gsap.to(el, { x: 0, y: 0, duration: 0.6, ease: "elastic.out(1,0.4)" }));
});
}
/* ---------- neon background fx attached to scroll ---------- */
function fx() {
const fillBar = $("#scrollFill");
// top neon progress bar
ScrollTrigger.create({
start: 0, end: "max",
onUpdate: (self) => { if (fillBar) fillBar.style.width = (self.progress * 100).toFixed(2) + "%"; },
});
if (REDUCED) return;
$$(".fx .orb").forEach((o) =>
gsap.to(o, { yPercent: (parseFloat(o.dataset.orb) || 0.05) * 600, ease: "none",
scrollTrigger: { trigger: "body", start: "top top", end: "bottom bottom", scrub: 1.2 } }));
$$(".fx .beam").forEach((b) =>
gsap.to(b, { xPercent: (parseFloat(b.dataset.fx) || 0.5) * 18, yPercent: (parseFloat(b.dataset.fx) || 0.5) * -40,
ease: "none", scrollTrigger: { trigger: "body", start: "top top", end: "bottom bottom", scrub: 1.5 } }));
}
/* ---------- nav / urls / form ---------- */
function chrome() {
const nav = $("#nav"), toggle = $("#navToggle");
ScrollTrigger.create({ start: "top -40", end: 99999,
onUpdate: (self) => nav.classList.toggle("scrolled", self.scroll() > 40),
onToggle: (self) => nav.classList.toggle("scrolled", self.isActive) });
nav.classList.toggle("scrolled", window.scrollY > 40);
toggle.addEventListener("click", () => {
const open = nav.classList.toggle("open");
toggle.setAttribute("aria-expanded", String(open));
});
document.addEventListener("click", (e) => {
const a = e.target.closest('a[href^="#"]');
if (!a) return;
const id = a.getAttribute("href");
if (id.length < 2) return;
const tgt = document.querySelector(id);
if (!tgt) return;
e.preventDefault();
nav.classList.remove("open"); toggle.setAttribute("aria-expanded", "false");
scrollToEl(tgt);
});
const booking = CFG.urls && CFG.urls.booking, portal = CFG.urls && CFG.urls.portal;
if (booking && booking !== "#") {
$$('.nav-cta, .mob-book, .hero-cta-row .btn:not(.ghost)').forEach((b) => {
b.setAttribute("href", booking); b.setAttribute("target", "_blank"); b.setAttribute("rel", "noopener");
});
}
const portalBtn = $("#portalBtn"), portalPh = $("#portalPh"), footPortal = $("#footPortal");
if (portal && portal !== "#") {
portalBtn.setAttribute("href", portal); footPortal.setAttribute("href", portal);
if (portalPh) portalPh.style.display = "none";
} else {
[portalBtn, footPortal].forEach((b) => b && b.addEventListener("click", (e) => e.preventDefault()));
}
const form = $("#bookingForm"), msg = $("#formMsg");
form.addEventListener("submit", (e) => {
e.preventDefault();
const name = $("#f-name").value.trim(), email = $("#f-email").value.trim(), addr = $("#f-addr").value.trim();
if (!name || !email || !addr) { msg.textContent = "Please add your name, email, and the property address."; return; }
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { msg.textContent = "That email doesn't look right β mind checking it?"; return; }
if (booking && booking !== "#") { msg.textContent = "Opening the booking systemβ¦"; window.open(booking, "_blank", "noopener"); }
else { msg.textContent = "Thanks, " + name + " β request captured. Connect a booking endpoint to send it automatically."; form.reset(); }
});
$("#yr").textContent = new Date().getFullYear();
}
/* ---------- WOW layer: hero char reveal Β· lateral scroll drift Β· neon cursor ---------- */
function splitHero() {
const h = $(".hero h1");
if (!h || REDUCED) return;
(function walk(node) {
[...node.childNodes].forEach((n) => {
if (n.nodeType === 3) {
const frag = document.createDocumentFragment();
n.textContent.split("").forEach((c) => {
const s = document.createElement("span"); s.className = "ch";
s.textContent = c === " " ? "\u00A0" : c; frag.appendChild(s);
});
node.replaceChild(frag, n);
} else if (n.nodeType === 1 && n.tagName !== "BR") walk(n);
});
})(h);
gsap.from(h.querySelectorAll(".ch"), { yPercent: 112, opacity: 0, filter: "blur(8px)",
duration: 1.05, ease: "power3.out", stagger: 0.022, delay: 0.12, clearProps: "filter" });
}
function lateral() {
if (REDUCED) return;
// statement lines drift sideways (alternating) as you scroll down
$$(".statement .line i").forEach((el, i) => {
gsap.fromTo(el, { xPercent: i % 2 ? 7 : -7 }, { xPercent: i % 2 ? -3 : 3, ease: "none",
scrollTrigger: { trigger: el, start: "top 96%", end: "bottom 10%", scrub: 0.8 } });
});
if (!window.matchMedia("(min-width:1025px)").matches) return;
// services cards slide in from alternate sides
$$(".svc").forEach((el, i) => {
gsap.fromTo(el, { x: i % 2 ? 70 : -70, opacity: 0.35 }, { x: 0, opacity: 1, ease: "power2.out",
scrollTrigger: { trigger: el, start: "top 92%", end: "top 46%", scrub: 0.7 } });
});
// reel phones converge toward center on scroll
const phones = $$(".reels .phone");
phones.forEach((el, i) => {
gsap.fromTo(el, { x: i % 2 ? 90 : -90, rotate: i % 2 ? 3 : -3 }, { x: 0, rotate: 0, ease: "power1.out",
scrollTrigger: { trigger: el, start: "top 96%", end: "top 40%", scrub: 0.8 } });
});
}
function about() {
const cfg = CFG.about; if (!cfg) return;
const txt = $("#aboutText"); if (txt) txt.textContent = cfg.text || "";
const st = $("#aboutStats");
if (st) st.innerHTML = (cfg.stats || []).map((s) =>
`${s.v}${s.l}
`).join("");
}
function init() {
brokerages();
hero();
gallery();
reels();
packages();
testimonials();
reveals();
about();
process();
tilt();
magnetic();
fx();
splitHero();
lateral();
chrome();
ScrollTrigger.refresh();
window.addEventListener("load", () => ScrollTrigger.refresh());
}
document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", init) : init();
})();