// app.js — wizard logic, markdown generation, live preview const STEPS = ["Identity", "About", "Stack", "Socials", "Add-ons"]; const LS_KEY = "ghprofile.v2"; const defaultState = () => ({ name: "", username: "", tagline: "", tagline2: "", greeting: "Hello! I'm", headlineColor: "#a371f7", bio: "", working: "", learning: "", collab: "", help: "", ask: "", pronouns: "", fun: "", tech: {}, // { category: [names] } socials: {}, // { key: value } factOrder: ["working", "learning", "collab", "help", "ask", "pronouns", "fun"], socialOrder: ["linkedin", "x", "instagram", "tiktok", "youtube", "pinterest", "devto", "website", "email"], addons: { stats: true, langs: true, activity: true, quote: true }, statsHost: "", badgeStyle: "for-the-badge", accent: "#2ea043", // NEW CUSTOMIZATIONS: headerType: "transparent", // e.g., transparent, wave, rect, slice, soft headerFont: "Auto", // e.g., Auto, Montserrat, Pacifico, Bungee animateHeader: false // Toggle animations if supported by render engine }); // About fact fields (drag-reorderable) const ABOUT_FIELDS = { working: { emoji: "\uD83D\uDD2D", label: "Currently working on", lead: "I'm currently working on", placeholder: "a real-time collaboration engine" }, learning: { emoji: "\uD83C\uDF31", label: "Currently learning", lead: "I'm currently learning", placeholder: "Rust and distributed systems" }, collab: { emoji: "\uD83D\uDC6F", label: "Looking to collaborate on", lead: "I'm looking to collaborate on", placeholder: "open-source developer tools" }, help: { emoji: "\uD83E\uDD14", label: "Looking for help with", lead: "I'm looking for help with", placeholder: "scaling WebSocket infrastructure" }, ask: { emoji: "\uD83D\uDCAC", label: "Ask me about", lead: "Ask me about", placeholder: "React, Node.js, API design" }, pronouns: { emoji: "\uD83D\uDE04", label: "Pronouns", lead: "Pronouns:", placeholder: "she/her" }, fun: { emoji: "\u26A1", label: "Fun fact", lead: "Fun fact:", placeholder: "I once debugged from a mountaintop" }, }; // ─── color themes (accent palette) ─── const THEME_COLORS = [ { name: "GitHub Green", hex: "#2ea043" }, { name: "Ocean", hex: "#2f81f7" }, { name: "Indigo", hex: "#4f46e5" }, { name: "Violet", hex: "#8957e5" }, { name: "Purple", hex: "#a855f7" }, { name: "Magenta", hex: "#db61a2" }, { name: "Rose", hex: "#f43f5e" }, { name: "Ruby", hex: "#e5484d" }, { name: "Orange", hex: "#e36209" }, { name: "Amber", hex: "#d29922" }, { name: "Lime", hex: "#65a30d" }, { name: "Teal", hex: "#1f9c8f" }, { name: "Cyan", hex: "#0891b2" }, { name: "Slate", hex: "#5b6573" }, ]; let state = load(); let step = 0; let previewMode = "preview"; function load() { try { const raw = JSON.parse(localStorage.getItem(LS_KEY)); if (raw && typeof raw === "object") { const s = Object.assign(defaultState(), raw); s.addons = Object.assign(defaultState().addons, raw.addons || {}); delete s.addons.views; delete s.addons.trophies; // removed features delete s.reach; // ensure order arrays are present & complete (migration) const def = defaultState(); s.factOrder = mergeOrder(raw.factOrder, def.factOrder); s.socialOrder = mergeOrder(raw.socialOrder, def.socialOrder); return s; } } catch (e) {} return defaultState(); } function mergeOrder(saved, def) { const a = Array.isArray(saved) ? saved.filter((k) => def.includes(k)) : []; def.forEach((k) => { if (!a.includes(k)) a.push(k); }); return a; } function persist() { try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } catch (e) {} } // ─── output-safety helpers ─────────────────────────────────────────────────── // User input flows into generated HTML/markdown that is later parsed with // innerHTML, so every user-derived value must be escaped before it is emitted. // escapeHtml: full escape for HTML *attribute* contexts (href, alt, ...). function escapeHtml(str) { return String(str == null ? "" : str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // escapeMdText: lighter escape for markdown body text (keeps raw output clean // while still neutralizing raw-HTML injection). function escapeMdText(str) { return String(str == null ? "" : str) .replace(/&/g, "&") .replace(//g, ">"); } // Build a safe github.com profile URL from a (possibly empty) username. function ghProfileUrl(user) { return "https://github.com/" + encodeURIComponent((user || "your-username").trim()); } // ─── theme ─── function applyThemeIcon() { const dark = document.documentElement.getAttribute("data-theme") === "dark"; document.getElementById("themeBtn").innerHTML = dark ? '' : ''; } function toggleTheme() { const cur = document.documentElement.getAttribute("data-theme"); const next = cur === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", next); try { localStorage.setItem("ghprofile.theme", next); } catch (e) {} applyThemeIcon(); render(); } (function initTheme() { try { const saved = localStorage.getItem("ghprofile.theme"); if (saved) document.documentElement.setAttribute("data-theme", saved); } catch (e) {} })(); // ─── accent color ─── function setAccent(hex) { state.accent = hex; const r = document.documentElement; r.style.setProperty("--accent", hex); r.style.setProperty("--green", hex); r.style.setProperty("--green-emph", `color-mix(in oklab, ${hex}, #000 18%)`); r.style.setProperty("--green-hover", `color-mix(in oklab, ${hex}, #000 4%)`); document.querySelectorAll(".swatch").forEach((s) => s.classList.toggle("on", s.dataset.hex && s.dataset.hex.toLowerCase() === hex.toLowerCase())); const ci = document.getElementById("customColor"); if (ci) ci.value = hex; persist(); render(); } function buildPalette() { const wrap = document.getElementById("swatches"); if (!wrap) return; wrap.innerHTML = ""; THEME_COLORS.forEach((c) => { const b = document.createElement("button"); b.className = "swatch"; b.dataset.hex = c.hex; b.style.background = c.hex; b.title = c.name; b.setAttribute("aria-label", c.name); b.innerHTML = ''; b.onclick = () => setAccent(c.hex); wrap.appendChild(b); }); } function togglePalette(force) { const pop = document.getElementById("palettePop"); const open = force === false ? false : (force === true ? true : pop.classList.contains("open") ? false : true); pop.classList.toggle("open", open); } document.addEventListener("click", (e) => { const pop = document.getElementById("palettePop"); if (pop && pop.classList.contains("open") && !pop.contains(e.target) && !e.target.closest("#paletteBtn")) { pop.classList.remove("open"); } }); // ─── stepper ─── function buildStepper() { const el = document.getElementById("stepper"); el.innerHTML = ""; STEPS.forEach((name, i) => { const item = document.createElement("div"); item.className = "stepper-item" + (i === step ? " active" : "") + (i < step ? " done" : ""); item.onclick = () => goStep(i); const done = i < step; item.innerHTML = `
${done ? '' : (i + 1)}
` + `${name}`; el.appendChild(item); if (i < STEPS.length - 1) { const line = document.createElement("div"); line.className = "step-line" + (i < step ? " filled" : ""); el.appendChild(line); } }); } function goStep(i) { step = Math.max(0, Math.min(STEPS.length - 1, i)); document.querySelectorAll(".step").forEach((s, idx) => s.classList.toggle("active", idx === step)); buildStepper(); document.getElementById("backBtn").style.visibility = step === 0 ? "hidden" : "visible"; document.getElementById("nextLabel").textContent = step === STEPS.length - 1 ? "Finish" : "Continue"; document.querySelector(".pane-form").scrollTo({ top: 0, behavior: "smooth" }); } function next() { if (step === STEPS.length - 1) { showFinish(); return; } goStep(step + 1); } function prev() { goStep(step - 1); } // ─── bind text inputs ─── function bindInputs() { document.querySelectorAll("[data-key]").forEach((inp) => { const k = inp.getAttribute("data-key"); if (state[k]) inp.value = state[k]; inp.addEventListener("input", () => { state[k] = inp.value; persist(); render(); }); }); } // ─── tech chips ─── function buildCatalog() { const wrap = document.getElementById("catalog"); wrap.innerHTML = ""; Object.entries(TECH).forEach(([cat, items]) => { state.tech[cat] = state.tech[cat] || []; const sel = state.tech[cat]; const block = document.createElement("div"); block.className = "cat"; block.innerHTML = `
${cat}${sel.length} selected
`; const chips = document.createElement("div"); chips.className = "chips"; Object.entries(items).forEach(([name, cfg]) => { const chip = document.createElement("button"); chip.className = "chip" + (sel.includes(name) ? " on" : ""); chip.innerHTML = `${name}` + ``; chip.onclick = () => { const arr = state.tech[cat]; const at = arr.indexOf(name); if (at >= 0) arr.splice(at, 1); else arr.push(name); chip.classList.toggle("on", at < 0); document.querySelector(`[data-cat="${CSS.escape(cat)}"]`).textContent = arr.length + " selected"; persist(); render(); }; chips.appendChild(chip); }); block.appendChild(chips); wrap.appendChild(block); }); } // ─── generic drag-to-reorder helper (pointer-based, works on touch + mouse) ─── function makeSortable(container, getOrder, setOrder) { let dragEl = null; container.querySelectorAll("[data-sort-id]").forEach((row) => { const handle = row.querySelector(".drag-handle"); if (!handle) return; handle.addEventListener("pointerdown", (e) => { e.preventDefault(); dragEl = row; row.classList.add("dragging"); const move = (ev) => { const after = [...container.querySelectorAll("[data-sort-id]:not(.dragging)")].find((el) => { const r = el.getBoundingClientRect(); return ev.clientY < r.top + r.height / 2; }); if (after) container.insertBefore(dragEl, after); else container.appendChild(dragEl); }; const up = () => { document.removeEventListener("pointermove", move); document.removeEventListener("pointerup", up); row.classList.remove("dragging"); const newOrder = [...container.querySelectorAll("[data-sort-id]")].map((el) => el.getAttribute("data-sort-id")); setOrder(newOrder); dragEl = null; persist(); render(); }; document.addEventListener("pointermove", move); document.addEventListener("pointerup", up); }); }); } // ─── about fact fields (dynamic + draggable) ─── function buildAboutFields() { const wrap = document.getElementById("aboutFields"); if (!wrap) return; wrap.innerHTML = ""; state.factOrder.forEach((key) => { const cfg = ABOUT_FIELDS[key]; if (!cfg) return; const row = document.createElement("div"); row.className = "sort-row"; row.setAttribute("data-sort-id", key); row.innerHTML = `` + `
` + `
`; wrap.appendChild(row); const inp = row.querySelector("input"); if (state[key]) inp.value = state[key]; inp.addEventListener("input", () => { state[key] = inp.value; persist(); render(); }); }); makeSortable(wrap, () => state.factOrder, (o) => { state.factOrder = o; }); } // ─── socials (dynamic + draggable) ─── function buildSocials() { const wrap = document.getElementById("socials"); wrap.innerHTML = ""; state.socialOrder.forEach((key) => { const cfg = SOCIALS[key]; if (!cfg) return; const row = document.createElement("div"); row.className = "sort-row social-row"; row.setAttribute("data-sort-id", key); row.innerHTML = `` + `
${cfg.label.charAt(0)}
` + `
`; wrap.appendChild(row); const inp = row.querySelector("input"); if (state.socials[key]) inp.value = state.socials[key]; inp.addEventListener("input", () => { const v = inp.value.trim(); if (v) state.socials[key] = v; else delete state.socials[key]; persist(); render(); }); }); makeSortable(wrap, () => state.socialOrder, (o) => { state.socialOrder = o; }); } // ─── add-ons ─── const ADDONS = [ { key: "stats", title: "GitHub stats card", desc: "Stars, commits, PRs and issues at a glance." }, { key: "langs", title: "Top languages", desc: "Most-used languages across your repositories." }, { key: "activity", title: "Contribution activity graph", desc: "A themed line graph of your recent activity." }, { key: "quote", title: "Random dev quote", desc: "A developer quote that refreshes on every visit." }, // New customizable widgets: { key: "trophy", title: "GitHub Trophies", desc: "Display your achievements as shiny git trophies." }, { key: "visitors", title: "Profile Visitor Badge", desc: "Track how many unique users view your profile page." } ]; function buildAddons() { const wrap = document.getElementById("addons"); wrap.innerHTML = ""; ADDONS.forEach((a) => { const row = document.createElement("div"); row.className = "toggle-row" + (state.addons[a.key] ? " on" : ""); row.innerHTML = `
${a.title}
${a.desc}
`; row.onclick = () => { state.addons[a.key] = !state.addons[a.key]; row.classList.toggle("on", state.addons[a.key]); persist(); render(); }; wrap.appendChild(row); }); } // strip protocol, path, and stray whitespace from a user-entered host function normalizeHost(v) { return String(v || "").trim().replace(/^https?:\/\//i, "").replace(/\/.*$/, "").replace(/\s+/g, ""); } // ─── markdown generation ─── function statTheme() { return document.documentElement.getAttribute("data-theme") === "dark" ? "tokyonight" : "default"; } function generate() { const s = state; const user = (s.username || "").trim(); const uEnc = encodeURIComponent(user); // safe for API query strings const ac = (s.accent || "#2ea043").replace("#", ""); const a = s.addons; const th = statTheme(); const L = []; // ───────── HERO: static colored header + moving colored headlines ───────── const typeLines = []; if (s.tagline.trim()) typeLines.push(s.tagline.trim()); if (s.tagline2.trim()) typeLines.push(s.tagline2.trim()); const linesParam = typeLines.map((l) => encodeURIComponent(l)).join(";"); const longest = typeLines.reduce((m, l) => Math.max(m, l.length), 0); const subWidth = Math.min(900, Math.max(360, longest * 15 + 40)); const hlc = (s.headlineColor || "#a371f7").replace("#", ""); // headline color // Big header: whatever the user types in the single Header field. const greeting = (s.greeting == null ? "Hello! I'm" : s.greeting).trim(); // Header — STATIC, colored (no motion), one line, larger than the headlines. if (greeting) { const headerWidth = Math.min(1200, Math.max(420, greeting.length * 32 + 90)); L.push(`

`); L.push(` `); L.push(` ${escapeHtml(greeting)}`); L.push(` `); L.push(`

`); } // Headlines 1 & 2 — MOVING (typed), a different color, smaller than the header. if (typeLines.length) { L.push(""); L.push(`

`); L.push(` Typing headlines`); L.push(`

`); } L.push(""); L.push(""); // ───────── ABOUT ───────── const facts = []; state.factOrder.forEach((key) => { const cfg = ABOUT_FIELDS[key]; const val = (s[key] || "").trim(); if (!cfg || !val) return; facts.push(`${cfg.emoji}  ${cfg.lead} **${escapeMdText(val)}**`); }); if (s.bio.trim() || facts.length) { L.push("### 🚀 About Me"); L.push(""); if (s.bio.trim()) { L.push(escapeMdText(s.bio.trim()).split("\n").filter((l) => l.trim()).join(" \n")); L.push(""); } if (facts.length) { L.push(facts.join(" \n")); L.push(""); } } // ───────── TECH STACK (one combined block) ───────── const allTech = []; Object.entries(TECH).forEach(([cat, items]) => { (s.tech[cat] || []).forEach((n) => { if (items[n]) allTech.push([n, items[n]]); }); }); if (allTech.length) { L.push("### 🛠️ Tech Stack"); L.push(""); L.push(`

`); L.push(allTech.map(([n, cfg]) => ` ${escapeHtml(n)}`).join("\n")); L.push(`

`); L.push(""); } // ───────── CONNECT ───────── const socialBadges = []; state.socialOrder.forEach((key) => { const cfg = SOCIALS[key]; let v = (s.socials[key] || "").trim(); if (!cfg || !v) return; const href = /^https?:\/\//.test(v) || v.startsWith("mailto:") ? v : cfg.prefix + v; socialBadges.push(` ${escapeHtml(cfg.label)}`); }); if (socialBadges.length) { L.push("### 🔗 Connect With Me"); L.push(""); L.push(`

`); L.push(socialBadges.join("\n")); L.push(`

`); L.push(""); } // ───────── STATS ───────── const cardColors = `&title_color=${ac}&icon_color=${ac}&hide_border=true&bg_color=00000000`; const sText = th === "default" ? "1f2328" : "c9d1d9"; const sDate = th === "default" ? "656d76" : "8b949e"; // Your own github-readme-stats instance (reliable for all users of this app). const statsHost = normalizeHost(s.statsHost) || "github-readme-stats-five-sigma-99.vercel.app"; if (user && (a.stats || a.langs)) { L.push("### 📊 GitHub Stats"); L.push(""); L.push(`

`); if (a.stats) L.push(` stats`); if (a.langs) L.push(` top langs`); L.push(`

`); L.push(""); } if (user && a.activity) { L.push("### 📈 Contribution Graph"); L.push(""); L.push(`

`); L.push(` activity graph`); L.push(`

`); L.push(""); } // ───────── DEV QUOTE ───────── // Plain centered quote — no decorative frame (the service's own border // clips on tall quotes, so we render the quote borderless). if (a.quote) { L.push("### 💭 Dev Quote"); L.push(""); L.push(`

`); L.push(` Dev quote`); L.push(`

`); L.push(""); } // ───────── FOOTER ───────── L.push("---"); L.push(`

⭐️ From ${escapeMdText(user || "your-username")}

`); return L.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n"; } // ─── render preview ─── function render() { const md = generate(); document.getElementById("previewRaw").textContent = md; const rendered = document.getElementById("previewRendered"); const hasUser = (state.username || "").trim(); if (!hasUser) { rendered.innerHTML = `
Enter your GitHub username to start —
your README preview appears here, live.
`; return; } if (window.marked) { rendered.innerHTML = marked.parse(md); // tag badge paragraphs for spacing rendered.querySelectorAll("p, h3").forEach((p) => { if (p.querySelector('img[src*="shields.io"]')) p.classList.add("md-badges"); }); // external widget images can 404 / cold-start — retry once, then show a // tidy placeholder (these services rate-limit their public demo instances, // but the widget still renders on the user's real GitHub profile). rendered.querySelectorAll('img[src*="vercel.app"], img[src*="herokuapp.com"], img[src*="demolab.com"], img[src*="komarev.com"], img[src*="githubusercontent.com"]').forEach((img) => { img.referrerPolicy = "no-referrer"; let retried = false; img.addEventListener("error", () => { if (!retried) { retried = true; let base = img.src.split(/[?&]_retry=/)[0]; // if a stats card on our own instance is down, fall back to the // public github-readme-stats service before giving up. if (base.includes("github-readme-stats-five-sigma-99.vercel.app")) { base = base.replace("github-readme-stats-five-sigma-99.vercel.app", "github-readme-stats.vercel.app"); } const sep = base.includes("?") ? "&" : "?"; setTimeout(() => { img.src = base + sep + "_retry=" + Date.now(); }, 1200); return; } // give up: swap for a labeled placeholder const label = img.alt || "widget"; const ph = document.createElement("span"); ph.className = "widget-fallback"; ph.innerHTML = '' + '' + escapeHtml(label) + 'Renders on your live profile'; if (img.parentNode) img.parentNode.replaceChild(ph, img); }); }); // dev quote — click to shuffle a fresh one in the preview const quote = rendered.querySelector('img[src*="quotes-github-readme"]'); if (quote) { const wrap = quote.closest("p") || quote.parentElement; if (wrap && !(wrap.nextElementSibling && wrap.nextElementSibling.classList.contains("quote-refresh"))) { quote.style.cursor = "pointer"; quote.title = "Click for a new quote"; const btn = document.createElement("button"); btn.className = "quote-refresh"; btn.type = "button"; btn.innerHTML = ' New quote'; const shuffle = () => { const base = quote.src.split("&_r=")[0]; quote.src = base + "&_r=" + Date.now(); }; quote.addEventListener("click", shuffle); btn.addEventListener("click", shuffle); wrap.insertAdjacentElement("afterend", btn); } } } else { rendered.innerHTML = "
" + escapeHtml(md) + "
"; } } // ─── preview tabs ─── function setPreviewMode(mode) { previewMode = mode; document.getElementById("tabPreview").classList.toggle("on", mode === "preview"); document.getElementById("tabRaw").classList.toggle("on", mode === "raw"); document.getElementById("previewRendered").hidden = mode !== "preview"; document.getElementById("previewRaw").hidden = mode !== "raw"; } // ─── actions ─── function flash(msg) { const old = document.querySelector(".toast"); if (old) old.remove(); const t = document.createElement("div"); t.className = "toast"; t.innerHTML = `${msg}`; document.body.appendChild(t); setTimeout(() => t.remove(), 2200); } function copyMd() { navigator.clipboard.writeText(generate()).then(() => flash("Markdown copied to clipboard")).catch(() => flash("Copy failed — select the Markdown tab")); } function downloadMd() { const blob = new Blob([generate()], { type: "text/markdown" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "README.md"; a.click(); URL.revokeObjectURL(a.href); flash("README.md downloaded"); } function resetAll() { if (!confirm("Clear all fields and start over?")) return; doReset(); flash("Cleared"); } function doReset() { const accent = state.accent; state = defaultState(); state.accent = accent; // keep chosen theme persist(); document.querySelectorAll("[data-key]").forEach((i) => (i.value = "")); document.querySelectorAll("#socials input").forEach((i) => (i.value = "")); const gEl = document.getElementById("f_greeting"); if (gEl) gEl.value = state.greeting; buildCatalog(); buildSocials(); buildAboutFields(); buildAddons(); buildHeadlineColors(); goStep(0); render(); } // ─── finish view (in the left pane) ─── function showFinish() { const u = (state.username || "your-username").trim(); document.getElementById("finishUser").textContent = u; document.getElementById("finishRepo").textContent = u; document.getElementById("wizardView").style.display = "none"; document.getElementById("finishView").classList.add("show"); const pane = document.querySelector(".pane-form"); if (pane) pane.scrollTo({ top: 0 }); } function closeFinish() { document.getElementById("finishView").classList.remove("show"); document.getElementById("wizardView").style.display = ""; } function createAnother() { closeFinish(); if (confirm("Start a fresh profile? This clears the current one.")) { doReset(); flash("New profile started"); } } async function shareProfile() { const md = generate(); if (navigator.share) { try { await navigator.share({ title: "My GitHub profile README", text: md }); return; } catch (e) {} } try { await navigator.clipboard.writeText(md); flash("README copied — paste it anywhere to share"); } catch (e) { flash("Select the Markdown tab to copy"); } } function toggleMobilePreview(force) { const on = force === true ? true : !document.body.classList.contains("show-preview"); document.body.classList.toggle("show-preview", on); document.getElementById("mobilePreviewBtn").innerHTML = on ? ' Edit' : ' Preview'; } // ─── wire up DOM events (no inline handlers — CSP-friendly) ─── function on(id, evt, fn) { const el = document.getElementById(id); if (el) el.addEventListener(evt, fn); } function wireEvents() { on("mobilePreviewBtn", "click", () => toggleMobilePreview()); on("paletteBtn", "click", () => togglePalette()); on("customColor", "input", (e) => setAccent(e.target.value)); on("themeBtn", "click", toggleTheme); on("resetBtn", "click", resetAll); on("backBtn", "click", prev); on("nextBtn", "click", next); on("tabPreview", "click", () => setPreviewMode("preview")); on("tabRaw", "click", () => setPreviewMode("raw")); on("copyBtn", "click", copyMd); on("downloadBtn", "click", downloadMd); on("finishDownloadBtn", "click", downloadMd); on("finishCopyBtn", "click", copyMd); on("finishShareBtn", "click", shareProfile); on("finishBackBtn", "click", closeFinish); on("finishAnotherBtn", "click", createAnother); // hero: greeting text + animate toggle const g = document.getElementById("f_greeting"); if (g) { g.value = state.greeting == null ? "Hello! I'm" : state.greeting; g.addEventListener("input", () => { state.greeting = g.value; persist(); render(); }); } buildHeadlineColors(); } // swatches to pick the (separate) color for the moving headline lines const HL_COLORS = ["#a371f7", "#2f81f7", "#f778ba", "#3fb950", "#e3b341", "#ff7b72", "#56d4dd", "#ff9bce"]; function buildHeadlineColors() { const wrap = document.getElementById("hlSwatches"); if (!wrap) return; wrap.innerHTML = ""; HL_COLORS.forEach((c) => { const b = document.createElement("button"); b.type = "button"; b.className = "hl-swatch" + (String(state.headlineColor).toLowerCase() === c ? " on" : ""); b.style.background = c; b.style.color = c; b.title = c; b.innerHTML = ''; b.addEventListener("click", () => { state.headlineColor = c; buildHeadlineColors(); persist(); render(); }); wrap.appendChild(b); }); } // ─── init ─── if (window.marked) marked.setOptions({ breaks: true, gfm: true }); wireEvents(); buildStepper(); bindInputs(); buildAboutFields(); buildCatalog(); buildSocials(); buildAddons(); buildPalette(); applyThemeIcon(); setAccent(state.accent || "#2ea043"); goStep(0); render();