Title:
URL Source: https://clonewars.rushos.com/app.js
Markdown Content:
/* AI Clone Wars 前端 v2: 战场图谱 / 排行榜 / 机会板 + 筛选 + 被克隆进度 + 提交 */
const API = location.hostname === "localhost" || location.hostname === "127.0.0.1"
? "https://clonewars.rushos.com" : ""; // 本地预览时提交打到线上
const CATMETA = {
ai: { title: "AI 战场", sub: "a16z Gen AI 100 · 第6期 (2026-03)" },
traffic: { title: "流量战场", sub: "Cloudflare Radar 全球 Top 1000 域名" },
other: { title: "其他战场", sub: "两榜之外的经典克隆战争(遗留保留区)" },
};
let DATA = null, LB = null, OPS = null;
let state = { view: "map", cat: "all", q: "", onlyCurated: true, status: null, family: null, ncat: null };
const NCAT_ZH = {};
let ALL_ITEMS = [];
const EXPANDED = new Set();
const NAME_BY_ID = {};
const $ = (s, el) => (el || document).querySelector(s);
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
const fmtStars = (n) => (n >= 1000 ? (n / 1000).toFixed(n >= 10000 ? 0 : 1) + "k" : n);
const tierWord = (p) => p >= 100 ? "已被超越/开源" : p >= 80 ? "主流可迁移" : p >= 60 ? "小规模生产" : p >= 40 ? "自用可行" : p >= 20 ? "Demo 可跑" : p > 0 ? "萌芽" : "空白";
function starGlyphs(progress) {
const half = Math.round(progress / 10);
const full = Math.floor(half / 2), hasHalf = half % 2 === 1;
const off = 5 - full - (hasHalf ? 1 : 0);
return "★".repeat(full) + (hasHalf ? "½" : "") + (off > 0 ? `${"★".repeat(off)}` : "");
}
/* ---------- 战场卡 ---------- */
function clonedBar(b) {
if (b.no_battlefield) return "";
const p = b.cloned || 0;
const cls = p >= 60 ? "" : p >= 30 ? "mid" : "low";
return `
被克隆 ${p}%
${tierWord(p)} · ${b.clone_count} 个项目
`;
}
function entryHtml(e) {
const gh = `https://github.com/${e.repo}`;
const meter = e.relation === "self"
? `本体开源
`
: `${starGlyphs(e.progress)}
${e.progress}%${e.relation === "part" ? " · 部件" : ""}${e.unverified ? " · 未校验" : ""} `;
const badges = [
`${{ clone: "克隆", adjacent: "同生态位", self: "本体开源", part: "部件" }[e.relation]}`,
e.archived ? `archived` : "",
e.rechecked ? `✓ ${esc(e.rechecked)} 核` : "",
].join(" ");
return `${meter}
${esc(e.repo)}
★ ${fmtStars(e.stars ?? 0)}${e.low_star ? " ·未过千" : ""}${e.language ? `
${esc(e.language)}` : ""}${badges}
${esc(e.evidence)}${e.scope_note ? ` 〔${esc(e.scope_note)}〕` : ""}
`;
}
function bfHtml(b) {
const chips = [
b.family ? `${esc(b.family)}` : "",
b.parent ? `↑ ${esc(NAME_BY_ID[b.parent] || b.parent)}` : "",
b.is_layer ? `底层模型` : "",
b.n_category && NCAT_ZH[b.n_category] ? `${esc(NCAT_ZH[b.n_category])}` : "",
b.a16z ? `a16z` : "",
b.cf_rank ? `CF #${b.cf_rank}` : (b.category === "traffic" ? `CF Top1000` : ""),
...(b.domains || []).slice(0, 1).map((d) => `${esc(d)}`),
].join(" ");
const act = ``;
if (b.no_battlefield) {
return `${esc(b.name)}
${chips}${act}未设战场:${esc(b.no_battlefield)}
`;
}
if (b.alias_of) {
return `${esc(b.name)}
${chips}${act}${esc(b.note || "")} → 见 ${esc(NAME_BY_ID[b.alias_of] || b.alias_of)}
`;
}
const entries = b.entries.map(entryHtml).join("");
const related = b.related?.length ? `关联战场:${b.related.map((id) => `${esc(NAME_BY_ID[id] || id)}`).join(" ")}
` : "";
const nonoss = b.non_oss?.length ? `非开源回应(未来克隆参考)${b.non_oss.map((n) => `
${esc(n.name)}${n.by ? ` ${esc(n.by)}` : ""}${n.note ? ` — ${esc(n.note)}` : ""}
`).join("")}
` : "";
const auto = b.auto_candidates?.length ? `未评审候选:${b.auto_candidates.map((c) => `
${esc(c.repo)} ★${fmtStars(c.stars)}`).join(" · ")}
` : "";
const nothing = (!b.entries.length && !b.auto_candidates?.length && !nonoss) ? `暂无 ≥1000 星克隆 —— 空白战场就是机会,欢迎 提交。
` : "";
const kw = b.kw?.length ? `检索关键词${b.kw.map((k) => `${esc(k)}`).join("")}
` : "";
return `${esc(b.name)}
${chips}${act}
${clonedBar(b)}${b.note ? `
${esc(b.note)}
` : ""}${entries}${auto}${nonoss}${related}${nothing}${kw}
`;
}
/* ---------- 筛选 ---------- */
const LEDGER_STATUS = new Set(["infra", "adtech", "regional", "gambling", "unknown"]);
function statusPass(b) {
if (!state.status) return true;
return b.status === state.status;
}
// 浏览模式 = 有筛选/搜索时把台账服务也纳入; 否则只看战场
function browseMode() { return !!(state.status || state.ncat || state.family || state.q); }
function itemUniverse() { return browseMode() ? ALL_ITEMS : DATA.battlefields; }
function matches(b) {
if (state.cat !== "all" && b.category !== state.cat) return false;
if (!statusPass(b)) return false;
if (state.family && b.family !== state.family) return false;
if (state.ncat && b.n_category !== state.ncat && b.n_category2 !== state.ncat) return false;
if (state.q) {
const hay = [b.name, b.id, b.category_hint, b.why, ...(b.domains || []), ...(b.kw || []), ...((b.entries || []).map((e) => e.repo + " " + (e.evidence || "")))].join(" ").toLowerCase();
if (!hay.includes(state.q)) return false;
}
// 默认(非浏览模式)且勾了"只看有战事": 隐藏无克隆战场
if (!browseMode() && state.onlyCurated && !(b.entries && b.entries.length)) return false;
return true;
}
function renderFilter() {
const f = DATA.facets;
const seg = (label, items, key) => `${label}${items.map((it) =>
`${esc(it.label)}${it.count != null ? ` ${it.count}` : ""}`).join("")}
`;
$("#fpanel").innerHTML =
`筛选 ×
` +
seg("状态 / 类型", f.status, "status") +
(f.ncat?.length ? seg("需求大类", f.ncat.map((c) => ({ id: c.id, label: c.zh, count: c.count })), "ncat") : "") +
(f.family.length ? seg("家族", f.family.map((x) => ({ id: x, label: x })), "family") : "") +
(browseMode() ? `清除全部筛选
` : "");
const n = (state.status ? 1 : 0) + (state.family ? 1 : 0) + (state.ncat ? 1 : 0);
const fc = $("#fcount"); fc.textContent = n; fc.hidden = !n;
}
/* ---------- 台账服务(未成战场)紧凑卡 ---------- */
const STATUS_ZH = { infra: "基础设施", adtech: "广告技术", regional: "地区门户", gambling: "博彩/成人", unknown: "暂未识别", surpassed: "已超越", strong: "主流可迁移", early: "早期", empty: "空白" };
function ledgerCardHtml(b) {
const nc = b.n_category && NCAT_ZH[b.n_category] ? `${esc(NCAT_ZH[b.n_category])}` : "";
const doms = (b.domains || []).slice(0, 4).map((d) => `${esc(d)}`).join("") + ((b.domains || []).length > 4 ? `+${b.domains.length - 4}` : "");
return `${esc(b.name)}
${STATUS_ZH[b.status] || b.status}${nc}${doms}
${b.why ? `
${esc(b.why)}
` : ""}
未单独设战场。若你知道有对应开源克隆/部件,欢迎 提交修正。
`;
}
function cardHtml(b) { return b.ledger ? ledgerCardHtml(b) : bfHtml(b); }
/* ---------- 视图: 战场图谱 / 浏览 ---------- */
const NCAT_ORDER = (id) => (DATA.facets.ncat.findIndex((c) => c.id === id));
function renderMap() {
const uni = itemUniverse();
let list;
if (state.cat === "all") list = uni.filter(matches);
else list = uni.filter((b) => b.category === state.cat).filter(matches);
// 非浏览模式: 保持原三分类战场视图
if (!browseMode()) {
const cats = state.cat === "all" ? ["ai", "traffic", "other"] : [state.cat];
$("#leftIndex").innerHTML = ""; $("#main").classList.remove("withindex");
$("#main").innerHTML = cats.map((cat) => {
const l = list.filter((b) => b.category === cat);
if (!l.length) return "";
const m = CATMETA[cat];
const opened = l.filter((b) => b.entries && b.entries.length).length;
const avg = DATA.stats.cloned?.[cat];
return `${m.title}
${m.sub} · ${opened} 开战${avg != null ? ` · 平均被克隆 ${avg}%` : ""}${l.map(cardHtml).join("")}`;
}).join("") || ``;
return;
}
// 浏览模式: 按 N大类分组, 组内按状态细分; <100 出左侧索引, ≥100 折叠强制细化
const small = list.length < 100;
$("#main").classList.toggle("withindex", small && list.length > 0);
// 分组: 优先 N大类; 无 N大类的按状态归组(不显示"未归类")
const glabel = (gk) => gk.startsWith("S:") ? (STATUS_ZH[gk.slice(2)] || gk.slice(2)) : (NCAT_ZH[gk] || "未归类");
const groups = {};
for (const b of list) { const g = b.n_category || ("S:" + b.status); (groups[g] ||= []).push(b); }
const gkeys = Object.keys(groups).sort((a, c) => (a.startsWith("S:") - c.startsWith("S:")) || ((NCAT_ORDER(a) < 0 ? 99 : NCAT_ORDER(a)) - (NCAT_ORDER(c) < 0 ? 99 : NCAT_ORDER(c))));
const STATUS_SORT = ["surpassed", "strong", "early", "empty", "regional", "gambling", "adtech", "infra", "unknown"];
let html = `${list.length} 个匹配目标${small ? "" : "(超过 100,已折叠分组,请在右侧筛选里再选一个维度细化)"}
`;
for (const gk of gkeys) {
const items = groups[gk];
const zh = glabel(gk);
// 组内按状态细分
const sub = {};
for (const b of items) (sub[b.status] ||= []).push(b);
const subKeys = Object.keys(sub).sort((a, c) => STATUS_SORT.indexOf(a) - STATUS_SORT.indexOf(c));
const openGroup = small || items.length <= 24;
html += `
${esc(zh)} ${items.length}
${subKeys.map((s) => `${STATUS_ZH[s] || s} ${sub[s].length}`).join(" · ")}${openGroup ? "▾" : "▸"}
`;
for (const sk of subKeys) {
const arr = sub[sk];
const cap = small || EXPANDED.has(gk + ":" + sk) ? arr.length : 40;
html += arr.length && subKeys.length > 1 ? `
${STATUS_ZH[sk] || sk} · ${arr.length}
` : "";
html += arr.slice(0, cap).map(cardHtml).join("");
if (arr.length > cap) html += `
还有 ${arr.length - cap} 个,点此展开
`;
}
html += `
`;
}
$("#main").innerHTML = html;
// 左侧跳转索引(<100)
if (small && list.length > 0) {
$("#leftIndex").innerHTML = `${list.length} 项 · 点击跳转
` + gkeys.map((gk) =>
`${esc(glabel(gk))}
${groups[gk].map((b) =>
`
${esc(b.name)}
`).join("")}
`).join("");
} else { $("#leftIndex").innerHTML = ""; }
}
/* ---------- 视图: 排行榜 ---------- */
function lbCard(title, hint, rows, valFn, cls) {
return `${title}${hint}
${rows.map((r, i) =>
`
${i + 1}${esc(r.name || r.repo)}${valFn(r)}
`).join("")}
`;
}
function renderBoard() {
if (!LB) { $("#main").innerHTML = ``; return; }
$("#main").innerHTML = `
${lbCard("🏆 最被彻底克隆", "被克隆% 最高的战场", LB.most_cloned.slice(0, 12), (r) => r.cloned + "%", "pct")}
${lbCard("🛡️ 最抵抗克隆", "被克隆% 最低 = 最难复刻", LB.most_resistant.slice(0, 12), (r) => r.cloned + "%", "low")}
${lbCard("⚔️ 最激烈战场", "克隆项目数最多", LB.most_contested.slice(0, 12), (r) => r.clone_count + " 项")}
${lbCard("⭐ 开源星王", "参战项目按 star", LB.star_kings.slice(0, 12), (r) => "★" + fmtStars(r.stars))}
${lbCard("👑 已被超越", "被克隆 100% / 本体开源", LB.surpassed.slice(0, 12), (r) => r.clone_count + " 项", "pct")}
${lbCard("🕳️ 空白机会", "有资格但暂无 ≥1k 星克隆", LB.empty_targets.slice(0, 12), () => "0%", "low")}
平均被克隆度:AI ${LB.avg_cloned.ai}% · 流量 ${LB.avg_cloned.traffic}% · 其他 ${LB.avg_cloned.other}%。点任意一行跳到该战场。
`;
}
/* ---------- 视图: 机会板 ---------- */
function renderOps() {
if (!OPS) { $("#main").innerHTML = ``; return; }
const catBadge = (c) => `${c}`;
const opCard = (o) => `${esc(o.name)}${catBadge(o.category)}
${esc(o.why)}
`;
$("#main").innerHTML = `
🕳️ 待克隆机会
这些 AI / 流量目标有开源空间,但社区还没有 ≥1000 星项目。是机会——已有对应项目?点提交修正。
${OPS.open_targets.map(opCard).join("")}
🚫 明确不设战场
这些目标结构上没有「克隆」形态。
${OPS.no_battlefield.map(opCard).join("")}
❓ 为何 1000 域名里只有部分成了战场
流量 Top1000 全部逐个分析(共 ${OPS.ledger_total} 条判定${OPS.analyzed ? `,其中 ${OPS.analyzed.model} 条经模型/联网逐个核实` : ""})。点任一类展开看具体域名:
${OPS.ledger_summary.map((l) => `
${l.count}
${esc(l.label)} ▸ 查看
${esc(l.why)}
`).join("")}
+ 提交你的输入
发现漏掉的克隆项目、想修正评分、或想发起一个克隆需求?
`;
loadRecent("_recent");
}
function showKindDetail(kind) {
const list = OPS.domains_by_kind?.[kind] || [];
const meta = OPS.ledger_summary.find((l) => l.kind === kind);
const el = $("#kindDetail");
el.innerHTML = `${esc(meta?.label || kind)} · ${list.length} 个域名${esc(meta?.why || "")}
${list.map((d) =>
`
${esc(d.domain)}${esc(d.product || d.reason || "")}
`).join("")}
`;
el.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
/* ---------- 提交弹窗 ---------- */
let modalKind = "correction", modalBf = "", modalTarget = "";
function openModal(bfId, targetName) {
modalBf = bfId || ""; modalTarget = targetName || (bfId ? NAME_BY_ID[bfId] : "") || "";
modalKind = "correction";
document.querySelectorAll("#mseg .s").forEach((s) => s.classList.toggle("on", s.dataset.kind === "correction"));
$("#mmsg").textContent = "";
renderModalFields();
$("#modal").classList.add("open");
}
function renderModalFields() {
const isReq = modalKind === "request";
$("#mtitle").textContent = isReq ? "发起克隆需求" : "补充 / 修正克隆";
$("#mhint").textContent = isReq
? "希望社区开源复刻哪个产品?投一票、说说你的理由。"
: (modalTarget ? `战场:${modalTarget}` : "补充一个漏掉的开源克隆,或修正现有评分。");
$("#mfields").innerHTML = isReq
? `
`
: `
`;
}
async function doSubmit() {
const kind = modalKind;
const body = { kind, battlefield: modalBf || null, target_name: $("#f_target")?.value || modalTarget || null,
repo: ($("#f_repo")?.value || "").trim() || null, note: ($("#f_note")?.value || "").trim() || null, contact: ($("#f_contact")?.value || "").trim() || null };
const msg = $("#mmsg"); msg.className = "msg"; msg.textContent = "提交中…";
try {
const r = await fetch(API + "/api/submit", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
const j = await r.json();
if (r.ok && j.ok) { msg.className = "msg ok"; msg.textContent = "✓ 已收到,感谢!审核后会体现在战场里。"; setTimeout(() => $("#modal").classList.remove("open"), 1400); }
else { msg.className = "msg err"; msg.textContent = "提交失败:" + (j.error || r.status); }
} catch (e) { msg.className = "msg err"; msg.textContent = "网络错误,稍后再试。"; }
}
async function loadRecent(slot) {
try {
const r = await fetch(API + "/api/submissions?limit=8");
const j = await r.json();
const el = document.querySelector(`[data-usub="${slot}"]`);
if (el && j.submissions?.length) {
el.innerHTML = `最近提交 · ${j.submissions.map((s) => `${s.kind === "request" ? "需求" : "修正"}「${esc(s.target_name || s.battlefield || s.repo || "")}」`).join(" · ")}`;
}
} catch {}
}
/* ---------- 主渲染 ---------- */
function render() {
$("#subnav").style.display = state.view === "map" ? "flex" : "none";
if (state.view !== "map") { $("#fpanel").classList.remove("open"); $("#leftIndex").innerHTML = ""; }
if (state.view === "map") renderMap();
else if (state.view === "board") renderBoard();
else renderOps();
}
function renderStats() {
const s = DATA.stats;
$("#statbar").innerHTML = [
[s.battlefields.total, "战场"],
[s.entries.clone, "克隆项目"],
[s.entries.adjacent, "同生态位"],
[s.entries.self, "本体开源"],
[(s.dataset.repos / 1000).toFixed(1) + "k", "≥1k星数据集"],
[(s.opportunities?.open_targets ?? 0), "待克隆机会"],
].map(([b, t]) => `${b}${t}
`).join("");
const ds = $("#dscount"); if (ds) ds.textContent = s.dataset.repos.toLocaleString();
}
function jump(id) {
if (!id) return;
state.view = "map"; state.q = ""; $("#q").value = "";
document.querySelectorAll(".view").forEach((v) => v.classList.toggle("on", v.dataset.view === "map"));
state.status = null; state.family = null; state.ncat = null;
if ($("#onlyCurated").checked) { $("#onlyCurated").checked = false; state.onlyCurated = false; }
const t = DATA.battlefields.find((b) => b.id === id);
if (t) { state.cat = "all"; document.querySelectorAll(".tab").forEach((x) => x.classList.toggle("on", x.dataset.cat === "all")); }
renderFilter(); render();
const el = document.querySelector(`.bf[data-id="${id}"]`);
if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); el.classList.add("flash"); setTimeout(() => el.classList.remove("flash"), 1500); }
}
Promise.all([
fetch("data/battlefields.json").then((r) => r.json()),
fetch("data/leaderboards.json").then((r) => r.json()).catch(() => null),
fetch("data/opportunities.json").then((r) => r.json()).catch(() => null),
]).then(([d, lb, ops]) => {
DATA = d; LB = lb; OPS = ops;
(DATA.taxonomy || []).forEach((t) => { NCAT_ZH[t.id] = t.zh; });
DATA.battlefields.forEach((b) => { NAME_BY_ID[b.id] = b.name; });
const idx = new Map(DATA.battlefields.map((b, i) => [b.id, i]));
DATA.battlefields = DATA.battlefields.slice().sort((a, b) => {
if (a.category !== b.category) return 0;
if (a.category === "traffic") return (a.cf_rank ?? 9999) - (b.cf_rank ?? 9999);
const ak = a.parent ? (idx.get(a.parent) ?? idx.get(a.id)) * 100 + 1 : idx.get(a.id) * 100;
const bk = b.parent ? (idx.get(b.parent) ?? idx.get(b.id)) * 100 + 1 : idx.get(b.id) * 100;
return ak - bk;
});
// 可浏览全集 = 真正战场(非别名/非no_bf) + 台账去重服务
const activeBf = DATA.battlefields.filter((b) => !b.no_battlefield && !b.alias_of);
ALL_ITEMS = [...activeBf, ...(DATA.ledger_items || [])];
renderStats(); renderFilter(); render();
});
/* ---------- 事件 ---------- */
document.querySelectorAll(".view").forEach((v) => v.addEventListener("click", () => {
document.querySelectorAll(".view").forEach((x) => x.classList.remove("on"));
v.classList.add("on"); state.view = v.dataset.view; render();
}));
document.querySelectorAll(".tab[data-cat]").forEach((t) => t.addEventListener("click", (e) => {
if (e.target.tagName === "INPUT") return;
document.querySelectorAll(".tab[data-cat]").forEach((x) => x.classList.remove("on"));
t.classList.add("on"); state.cat = t.dataset.cat; render();
}));
$("#filterToggle").addEventListener("click", () => { $("#fpanel").classList.toggle("open"); });
$("#onlyCurated").addEventListener("change", (e) => { state.onlyCurated = e.target.checked; render(); });
$("#q").addEventListener("input", (e) => {
state.q = e.target.value.trim().toLowerCase();
if (state.q && state.view !== "map") { state.view = "map"; document.querySelectorAll(".view").forEach((v) => v.classList.toggle("on", v.dataset.view === "map")); }
render();
});
document.addEventListener("click", (e) => {
const kd = e.target.closest("[data-kind]");
if (kd && state.view === "ops") { showKindDetail(kd.dataset.kind); return; }
const nc = e.target.closest("[data-fset-ncat]");
if (nc) { e.preventDefault(); e.stopPropagation(); state.ncat = nc.dataset.fsetNcat; if (!$("#fpanel").classList.contains("open")) $("#fpanel").classList.add("open"); renderFilter(); render(); return; }
const kw = e.target.closest("[data-kw]");
if (kw) { e.preventDefault(); const w = kw.dataset.kw; $("#q").value = w; state.q = w.toLowerCase(); if (state.view !== "map") { state.view = "map"; document.querySelectorAll(".view").forEach((v) => v.classList.toggle("on", v.dataset.view === "map")); } render(); window.scrollTo({ top: 0, behavior: "smooth" }); return; }
const fc = e.target.closest("[data-fkey]");
if (fc) { const k = fc.dataset.fkey, v = fc.dataset.fval; state[k] = state[k] === v ? null : v; renderFilter(); render(); return; }
if (e.target.id === "fpClose") { $("#fpanel").classList.remove("open"); return; }
if (e.target.id === "fClear") { state.status = state.family = state.ncat = null; state.q = ""; $("#q").value = ""; renderFilter(); render(); return; }
const gt = e.target.closest("[data-gtoggle]");
if (gt) { const s = gt.closest(".gcollapse"); s.classList.toggle("open"); gt.querySelector(".gcaret").textContent = s.classList.contains("open") ? "▾" : "▸"; return; }
const mo = e.target.closest("[data-more]");
if (mo) { EXPANDED.add(mo.dataset.more); renderMap(); return; }
const ix = e.target.closest("[data-idx]");
if (ix) { const el = document.querySelector(`.bf[data-id="${CSS.escape(ix.dataset.idx)}"]`); if (el) { const s = el.closest(".gcollapse"); if (s && !s.classList.contains("open")) { s.classList.add("open"); s.querySelector(".gcaret").textContent = "▾"; } el.scrollIntoView({ behavior: "smooth", block: "center" }); el.classList.add("flash"); setTimeout(() => el.classList.remove("flash"), 1500); } return; }
const sub = e.target.closest("[data-submit]");
if (sub) { e.preventDefault(); openModal(sub.dataset.submit, sub.dataset.target); return; }
const j = e.target.closest("[data-jump]");
if (j) { e.preventDefault(); jump(j.dataset.jump); return; }
});
document.querySelectorAll("#mseg .s").forEach((s) => s.addEventListener("click", () => {
document.querySelectorAll("#mseg .s").forEach((x) => x.classList.remove("on"));
s.classList.add("on"); modalKind = s.dataset.kind; renderModalFields();
}));
$("#mcancel").addEventListener("click", () => $("#modal").classList.remove("open"));
$("#msubmit").addEventListener("click", doSubmit);
$("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") $("#modal").classList.remove("open"); });