PageSourceSearch

https://singularitychallenge.com/assets/app.5.4.2.js

js singularitychallenge.com collected 2026-09-25 00:56:17 UTC 41,359 bytes, 730 lines download raw bytes

1/* iChallenge Arena v5 client. No framework. All user/game text is inserted with textContent. */
2(function () {
3  "use strict";
4  var bootEl = document.getElementById("boot");
5  if (!bootEl) return;
6  var BOOT = JSON.parse(bootEl.textContent);
7  var REDUCED = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8  var LS = {
9    get: function (k) { try { return localStorage.getItem(k); } catch (e) { return null; } },
10    set: function (k, v) { try { localStorage.setItem(k, v); } catch (e) {} },
11    del: function (k) { try { localStorage.removeItem(k); } catch (e) {} },
12  };
13  var TOKEN_KEY = BOOT.preview ? "ic_t_preview" : "ic_t";
14  var state = { player: null, token: LS.get(TOKEN_KEY) };
15
16  // ---------- helpers ----------
17  function $(s, r) { return (r || document).querySelector(s); }
18  function $$(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }
19  function h(tag, attrs) {
20    var el = document.createElement(tag);
21    if (attrs) for (var k in attrs) {
22      if (attrs[k] == null) continue;
23      if (k === "class") el.className = attrs[k];
24      else if (k === "text") el.textContent = attrs[k];
25      else if (k.slice(0, 2) === "on") el.addEventListener(k.slice(2), attrs[k]);
26      else el.setAttribute(k, attrs[k]);
27    }
28    for (var i = 2; i < arguments.length; i++) {
29      var c = arguments[i];
30      if (c == null || c === false) continue;
31      if (Array.isArray(c)) { c.forEach(function (x) { if (x) el.appendChild(x); }); continue; }
32      el.appendChild(typeof c === "string" || typeof c === "number" ? document.createTextNode(String(c)) : c);
33    }
34    return el;
35  }
36  function svgEl(markup) {
37    var t = document.createElement("template");
38    t.innerHTML = markup; // static, code-owned markup only
39    return t.content.firstChild;
40  }
41  function fmt(n) { return Number(n || 0).toLocaleString("en-US"); }
42  function withArena(path) {
43    if (!BOOT.preview) return path;
44    var hash = "";
45    var i = path.indexOf("#");
46    if (i > -1) { hash = path.slice(i); path = path.slice(0, i); }
47    return path + (path.indexOf("?") > -1 ? "&" : "?") + "arena=" + encodeURIComponent(BOOT.host) + hash;
48  }
49
50  function api(path, body, method) {
51    var headers = { "content-type": "application/json" };
52    if (state.token) headers.authorization = "Bearer " + state.token;
53    if (BOOT.preview) headers["x-arena-host"] = BOOT.host;
54    return fetch(path, { method: method || (body ? "POST" : "GET"), headers: headers, body: body ? JSON.stringify(body) : undefined, credentials: "omit" })
55      .then(function (r) {
56        return r.json().catch(function () { return {}; }).then(function (j) { j._status = r.status; return j; });
57      })
58      .catch(function () { return { error: "You're offline. Check your connection and try again.", _status: 0 }; });
59  }
60
61  function toast(title, sub, badgeId) {
62    var box = $("#toasts");
63    if (!box) return;
64    var t = h("div", { class: "toast" });
65    if (badgeId) t.appendChild(svgEl(badgeSvg(badgeId)));
66    t.appendChild(h("div", null, h("b", { text: title }), sub ? h("span", { text: sub }) : null));
67    box.appendChild(t);
68    setTimeout(function () { t.remove(); }, 4200);
69  }
70
71  var GLYPHS = {
72    first_round: "M8 4h8M12 4v16M8 20h8", flawless: "M5 12.5l4.5 4.5L19 7.5", lightning: "M13 3L5 13.5h6L10 21l9-11h-6z",
73    on_a_roll: "M12 3c1 3.5 5 5.5 5 10a5 5 0 01-10 0c0-2.5 1.5-4 2.5-5.5.5 2 1.5 3 2.5 3 0-2.5-1-5 0-7.5z",
74    week_warrior: "M4 6h16v14H4zM4 10h16M8 3v5M16 3v5", unbreakable: "M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6z",
75    explorer: "M12 21a9 9 0 110-18 9 9 0 010 18zM15.5 8.5l-2 5-5 2 2-5z",
76    globetrotter: "M12 21a9 9 0 110-18 9 9 0 010 18zM3 12h18M12 3c2.5 2.5 3.5 5.5 3.5 9s-1 6.5-3.5 9c-2.5-2.5-3.5-5.5-3.5-9s1-6.5 3.5-9z",
77    polymath: "M4 5.5C6.5 4 9.5 4 12 5.5 14.5 4 17.5 4 20 5.5V19c-2.5-1.5-5.5-1.5-8 0-2.5-1.5-5.5-1.5-8 0zM12 5.5V19",
78    in_the_arena: "M5 21V4M5 4h11l-2 4 2 4H5", finisher: "M12 15a6 6 0 100-12 6 6 0 000 12zM8.5 14L7 21l5-2.5L17 21l-1.5-7",
79    recruiter: "M9 11a3.5 3.5 0 100-7 3.5 3.5 0 000 7zM3 20c0-3.5 2.5-6 6-6s6 2.5 6 6M18 8v6M15 11h6",
80    captain: "M4 18h16M5 18L4 8l4.5 4L12 5l3.5 7L20 8l-1 10", podium: "M4 20V14h5v6M9 20V9h6v11M15 20v-8h5v8",
81    challenger: "M3 20l6.5-11 4 6 2.5-4 5 9z", saved: "M4 6h16v12H4zM4 7l8 6 8-6",
82  };
83  function badgeSvg(id) {
84    return '<svg class="medal-svg" width="44" height="44" viewBox="0 0 48 48" aria-hidden="true"><circle cx="24" cy="24" r="22" class="medal-ring"/><circle cx="24" cy="24" r="17" class="medal-core"/><g transform="translate(12 12)">
84<path d="' + (GLYPHS[id] || GLYPHS.first_round) + '" class="medal-glyph"/></g></svg>';
85  }
86
87  // ---------- player ----------
88  function ensurePlayer() {
89    if (state.token && state.player) return Promise.resolve(state.player);
90    if (state.token) return loadMe().then(function (p) { return p || createPlayer(); });
91    return createPlayer();
92  }
93  function createPlayer() {
94    return api("/api/player", { ref: LS.get("ic_ref") || undefined, hp_field: "" }).then(function (r) {
95      if (!r.token) { toast("Couldn't start your profile", r.error || "Try again in a moment."); return null; }
96      state.token = r.token; LS.set(TOKEN_KEY, r.token); state.player = r.player; renderPlayer();
97      return r.player;
98    });
99  }
100  function loadMe() {
101    if (!state.token) { renderPlayer(); return Promise.resolve(null); }
102    return api("/api/me").then(function (r) {
103      if (r._status === 0) return state.player;
104      if (!r.player) { state.token = null; LS.del(TOKEN_KEY); }
105      state.player = r.player || null;
106      renderPlayer();
107      return state.player;
108    });
109  }
110
111  function setRing(el, frac) {
112    var c = 2 * Math.PI * 15;
113    var f = Math.max(0, Math.min(1, frac));
114    el.style.strokeDasharray = (f * c).toFixed(1) + " " + c.toFixed(1);
115  }
116
117  function renderPlayer() {
118    var p = state.player;
119    var span = p ? Math.max(1, p.level_next - p.level_floor) : 1;
120    var frac = p ? (p.xp - p.level_floor) / span : 0;
121    $$("[data-lvl]").forEach(function (e) { e.textContent = p ? p.level : 1; });
122    $$("[data-lvl-text]").forEach(function (e) { e.textContent = p ? p.level : 1; });
123    $$("[data-title]").forEach(function (e) { e.textContent = p ? p.title : "Rookie"; });
124    $$("[data-handle]").forEach(function (e) { e.textContent = p ? p.handle : "Guest"; });
125    $$("[data-ring-fill]").forEach(function (e) { setRing(e, frac); });
126    $$("[data-xp-bar]").forEach(function (e) { e.style.width = (frac * 100).toFixed(1) + "%"; });
127    $$("[data-xp-text]").forEach(function (e) { e.textContent = p && p.xp ? fmt(p.xp - p.level_floor) + " / " + fmt(span) + " XP to level " + (p.level + 1) : "Play a round to earn XP"; });
128    var streak = p && p.streak_alive ? p.streak : 0;
129    $$("[data-streak]").forEach(function (e) { e.textContent = streak; });
130    $$("[data-streak-text]").forEach(function (e) { e.textContent = streak; });
131    $$("[data-streak-sub]").forEach(function (e) { e.textContent = !p ? "days" : p.played_today ? "days · safe today" : streak ? "days · play today" : "days"; });
132    $$("[data-streak-chip]").forEach(function (e) { e.classList.toggle("is-hot", !!(p && p.played_today)); });
133    $$("[data-league]").forEach(function (e) { var l = p ? p.league : "bronze"; e.textContent = l.charAt(0).toUpperCase() + l.slice(1); e.setAttribute("data-l", l); });
134    var owned = {};
135    if (p && p.badges) p.badges.forEach(function (b) { owned[b.id] = true; });
136    $$("[data-badge]").forEach(function (e) { e.classList.toggle("is-locked", !owned[e.getAttribute("data-badge")]); });
137    if (p && p.email_saved) {
138      $$(".save").forEach(function (s) { s.classList.add("is-saved"); });
139      var note = $("[data-save-note]");
140      if (note && !note.textContent) note.textContent = p.email_verified ? "Your profile is saved." : "Email saved.";
141    }
142    var profile = $("[data-profile]");
143    if (profile && p) {
144      var set = function (sel, v) { var e = $(sel, profile); if (e) e.textContent = v; };
145      set("[data-xp-total]", fmt(p.xp)); set("[data-best-streak]", p.best_streak); set("[data-rounds]", p.rounds_played);
146      set("[data-perfect]", p.perfect_rounds); set("[data-freezes]", p.freezes);
147      var link = $("[data-invite-link]", profile);
148      if (link) link.textContent = location.origin + "/?ref=" + p.ref_code;
149    }
150  }
151
152  // ---------- round engine ----------
153  var round = null;
154  var timerRAF = 0;
155  var roundEl = $("[data-round]");
156
157  function showPane(name) {
158    if (!roundEl) return;
159    ["idle", "live", "done"].forEach(function (n) { $("[data-round-" + n + "]", roundEl).hidden = n !== name; });
160  }
161
162  function refreshToday() {
163    if (!roundEl) return;
164    api("/api/round/today").then(function (t) {
165      if (t._status !== 200) return;
166      var pt = $("[data-players-today]", roundEl); if (pt) pt.textContent = fmt(t.players_today);
167      var daily = $("[data-start='daily']", roundEl), practice = $("[data-start='practice']", roundEl), note = $("[data-round-note]", roundEl);
168      if (t.mine && t.mine.status === "done") {
169        daily.textContent = "See today's result"; daily.dataset.showResult = "1";
170        practice.hidden = false;
171        note.textContent = "You scored " + fmt(t.mine.score) + " today (" + t.mine.correct + "/" + t.mine.total + "). Next round in " + hm(t.resets_in_ms) + ".";
172        state.todayMine = t.mine;
173      } else if (t.mine && t.mine.status === "open") {
174        daily.textContent = "Resume today's round";
175      }
176    });
177  }
178  function hm(ms) { var m = Math.max(0, Math.round(ms / 60000)); return Math.floor(m / 60) + "h " + (m % 60) + "m"; }
179
180  function start(mode) {
181    var btn = $("[data-start='" + mode + "']", roundEl);
182    if (btn && btn.dataset.showResult && state.todayMine) return showSavedResult(state.todayMine);
183    if (btn) btn.disabled = true;
184    ensurePlayer().then(function (p) {
185      if (!p) { if (btn) btn.disabled = false; return null; }
186      return api("/api/round/start", { mode: mode }).then(function (r) {
187        if (btn) btn.disabled = false;
188        if (r._status === 409 && r.today && r.today.mine) { state.todayMine = r.today.mine; return showSavedResult(r.today.mine); }
189        if (!r.round_id) { toast("Couldn't start the round", r.error || "Try again."); return; }
190        round = { id: r.round_id, mode: mode, score: r.score || 0, combo: r.combo || 0, results: [], total: r.question.total, locked: false };
191        showPane("live");
192        renderQuestion(r.question, r.elapsed_ms || 0);
193        roundEl.scrollIntoView({ behavior: REDUCED ? "auto" : "smooth", block: "nearest" });
194      });
195    });
196  }
197
198  function renderQuestion(q, elapsed) {
199    var live = $("[data-round-live]", roundEl);
200    live.textContent = "";
201    var steps = h("div", { class: "q-steps", "aria-hidden": "true" });
202    for (var i = 0; i < q.total; i++) {
203      var cls = i < round.results.length ? (round.results[i] ? "is-ok" : "is-bad") : i === q.i ? "is-on" : "";
204      steps.appendChild(h("i", { class: cls }));
205    }
206    var C = 2 * Math.PI * 20;
207    var timer = svgEl('<div class="q-timer" aria-hidden="true"><svg viewBox="0 0 46 46" width="46" height="46"><circle cx="23" cy="23" r="20" class="t-track"/><circle cx="23" cy="23" r="20" class="t-fill" style="stroke-dasharray:' + C + " " + C + '"/></svg><b>20</b></div>');
208    live.appendChild(h("div", { class: "q-head" }, steps, timer));
209    live.appendChild(h("div", { class: "q-meta" },
210      h("span", { text: (round.mode === "daily" ? "Daily" : "Practice") + " · Question " + (q.i + 1) + " of " + q.total }),
211      round.combo > 0 ? h("span", { class: "q-combo", text: "Combo ×" + [1, 1.1, 1.2, 1.35, 1.5][Math.min(round.combo, 4)] }) : h("span", { class: "q-score", text: fmt(round.score) + " pts" })
212    ));
213    live.appendChild(h("p", { class: "q-text", id: "qText", text: q.q }));
214    var opts = h("div", { class: "opts", role: "group", "aria-labelledby": "qText" });
215    q.opts.forEach(function (o, idx) {
216      opts.appendChild(h("button", { class: "opt", type: "button", "data-choice": idx, onclick: function () { answer(idx); } }, h("kbd", { text: String(idx + 1) }), h("span", { text: o })));
217    });
218    live.appendChild(opts);
219    var firstBtn = $(".opt", opts); if (firstBtn && !REDUCED) firstBtn.focus({ preventScroll: true });
220
221    var deadline = performance.now() + Math.max(0, q.time_ms - elapsed);
222    var fill = $(".t-fill", timer), label = $("b", timer);
223    cancelAnimationFrame(timerRAF);
224    (function tick() {
225      var left = deadline - performance.now();
226      var frac = Math.max(0, left / q.time_ms);
227      fill.style.strokeDasharray = (frac * C).toFixed(1) + " " + C;
228      label.textContent = Math.max(0, Math.ceil(left / 1000));
229      timer.classList.toggle("is-low", left < 5000);
230      if (left <= 0) { if (!round.locked) answer(-1); return; }
231      timerRAF = requestAnimationFrame(tick);
232    })();
233  }
234
235  function answer(choice) {
236    if (!round || round.locked) return;
237    round.locked = true;
238    cancelAnimationFrame(timerRAF);
239    var live = $("[data-round-live]", roundEl);
240    $$(".opt", live).forEach(function (b) { b.disabled = true; });
241    api("/api/round/answer", { round_id: round.id, choice: choice }).then(function (r) {
242      if (r.error) { toast("That answer didn't go through", r.error); round.locked = false; return; }
243      round.score = r.score; round.combo = r.combo; round.results.push(r.ok);
244      $$(".opt", live).forEach(function (b) {
245        var i = +b.getAttribute("data-choice");
246        if (i === r.correct_index) b.classList.add("is-right");
247        else if (i === choice) { b.classList.add("is-wrong"); if (!REDUCED) b.classList.add("is-shake"); }
248        else b.classList.add("is-dim");
249      });
250      var steps = $$(".q-steps i", live);
251      if (steps[round.results.length - 1]) steps[round.results.length - 1].className = r.ok ? "is-ok" : "is-bad";
252      var msg = r.ok ? (r.combo > 1 ? "Correct — " + r.combo + " in a row" : "Correct") : r.timed_out ? "Time's up" : "Not quite";
253      var reveal = h("div", { class: "reveal" + (r.ok ? "" : " is-bad"), role: "status" },
254        h("div", null, h("b", { text: msg }), r.why ? h("div", { text: r.why }) : null),
255        h("span", { class: "pts" + (r.ok && !REDUCED ? " is-pop" : ""), text: r.ok ? "+" + r.points : "+0" }));
256      live.appendChild(reveal);
257      var go = function () {
258        if (r.done) return renderDone(r.done);
259        nextBtn.disabled = true;
260        api("/api/round/next", { round_id: round.id }).then(function (n) {
261          if (!n.question) { nextBtn.disabled = false; return toast("Couldn't load the next question", n.error || "Try again."); }
262          round.locked = false;
263          renderQuestion(n.question, n.elapsed_ms || 0);
264        });
265      };
266      var nextBtn = h("button", { class: "btn btn-primary q-next", type: "button", text: r.done ? "See results" : "Next question", onclick: go });
267      live.appendChild(nextBtn);
268      nextBtn.focus({ preventScroll: true });
269    });
270  }
271
272  function countUp(el, to) {
273    if (REDUCED) { el.textContent = fmt(to); return; }
274    var t0 = performance.now(), dur = 900;
275    (function step(t) {
276      var k = Math.min(1, (t - t0) / dur);
277      el.textContent = fmt(Math.round(to * (1 - Math.pow(1 - k, 3))));
278      if (k < 1) requestAnimationFrame(step);
279    })(t0);
280  }
281
282  function renderDone(d) {
283    round = null;
284    showPane("done");
285    var box = $("[data-round-done]", roundEl);
286    box.textContent = "";
287    box.appendChild(h("div", { class: "round-top" }, h("span", { class: "pill pill-accent", text: d.mode === "daily" ? "Daily result" : "Practice result" }), d.rank_today ? h("span", { class: "round-reset", text: "#" + d.rank_today.rank + " of " + fmt(d.rank_today.of) + " today" }) : null));
288    if (d.level_up) box.appendChild(h("div", { class: "levelup" + (REDUCED ? "" : " is-pop") }, h("b", { text: "Level " + d.level }), h("span", { text: "You're now a " + d.title + "." })));
289    var scoreB = h("b", { text: "0" });
290    box.appendChild(h("div", { class: "done-score" }, scoreB, h("span", { text: "points" })));
291    countUp(scoreB, d.score);
292    var grid = h("div", { class: "done-grid", "aria-label": d.correct + " of " + d.total + " correct" });
293    d.grid.forEach(function (ok) { grid.appendChild(h("i", { class: ok ? "ok" : "" })); });
294    box.appendChild(grid);
295    var rows = h("div", { class: "done-rows" });
296    rows.appendChild(h("div", { class: "done-row" }, h("span", { text: "Correct" }), h("b", { text: d.correct + " / " + d.total })));
297    if (d.perfect_bonus) rows.appendChild(h("div", { class: "done-row" }, h("span", { text: "Perfect round bonus" }), h("b", { text: "+" + d.perfect_bonus })));
298    rows.appendChild(h("div", { class: "done-row" }, h("span", { text: "XP earned" }), h("b", { text: "+" + fmt(d.xp_gained) })));
299    if (d.mode === "daily") rows.appendChild(h("div", { class: "done-row" }, h("span", { text: d.freeze_used ? "Streak (a freeze saved it)" : "Streak" }), h("b", { text: d.streak + (d.streak === 1 ? " day" : " days") })));
300    box.appendChild(rows);
301    var span = Math.max(1, d.level_next - d.level_floor);
302    var bar = h("i", { style: "width:0%" });
303    box.appendChild(h("div", { class: "done-xp" }, h("div", { class: "bar bar-xp" }, bar), h("p", null, h("span", { text: "Level " + d.level }), h("span", { text: fmt(d.xp_total - d.level_floor) + " / " + fmt(span) + " XP" }))));
304    requestAnimationFrame(function () { bar.style.width = (((d.xp_total - d.level_floor) / span) * 100).toFixed(1) + "%"; });
305    if (d.badges_new && d.badges_new.length) {
306      var bl = h("div", { class: "done-badges" });
307      d.badges_new.forEach(function (b, i) {
308        var m = h("div", { class: "medal is-new" });
309        m.appendChild(svgEl(badgeSvg(b.id)));
310        m.appendChild(h("div", null, h("b", { text: b.name }), h("span", { text: b.desc })));
311        bl.appendChild(m);
312        setTimeout(function () { toast("Badge unlocked: " + b.name, b.desc, b.id); }, 500 + i * 700);
313      });
314      box.appendChild(bl);
315    }
316    var actions = h("div", { class: "done-actions" });
317    if (d.share) actions.appendChild(h("button", { class: "btn btn-primary", type: "button", text: "Challenge a friend", onclick: function () { share(d.share); } }));
318    actions.appendChild(h("button", { class: "btn btn-secondary", type: "button", text: "Practice round", onclick: function () { start("practice"); } }));
319    if (!state.player || !state.player.email_saved) actions.appendChild(h("a", { class: "btn btn-quiet span", href: "#save", text: "Save your progress" }));
320    else if (!BOOT.hub) actions.appendChild(h("a", { class: "btn btn-quiet span", href: withArena("/#challenges"), text: "Join a challenge" }));
321    box.appendChild(actions);
322    var tgt = beatTarget();
323    if (d.mode === "daily" && tgt && tgt.today && (!state.player || state.player.ref_code !== tgt.code)) {
324      var diff = d.score - tgt.score;
325      box.insertBefore(h("div", { class: "h2h " + (diff > 0 ? "is-win" : diff < 0 ? "is-loss" : "") },
326        h("b", { text: diff > 0 ? "You beat " + tgt.handle + " by " + fmt(diff) : diff < 0 ? tgt.handle + " wins by " + fmt(-diff) : "Dead heat with " + tgt.handle }),
327        h("span", { text: "You " + fmt(d.score) + " · " + tgt.handle + " " + fmt(tgt.score) })), rows);
328      if (!LS.get("ic_beat_played_" + tgt.code + tgt.day)) { LS.set("ic_beat_played_" + tgt.code + tgt.day, "1"); track("beat_play"); }
329    }
330    loadMe();
331    loadLeaderboard();
332    if (d.mode === "daily") {
333      state.todayMine = { status: "done", score: d.score, correct: d.correct, total: d.total, grid: d.grid, xp: d.xp_gained };
334      var daily = $("[data-start='daily']", roundEl);
335      daily.textContent = "See today's result"; daily.dataset.showResult = "1";
336      $("[data-start='practice']", roundEl).hidden = false;
337    }
338  }
339
340  function showSavedResult(m) {
341    showPane("done");
342    var box = $("[data-round-done]", roundEl);
343    box.textContent = "";
344    box.appendChild(h("div", { class: "round-top" }, h("span", { class: "pill pill-accent", text: "Today's result" })));
345    box.appendChild(h("div", { class: "done-score" }, h("b", { text: fmt(m.score) }), h("span", { text: "points" })));
346    var grid = h("div", { class: "done-grid" });
347    (m.grid || []).forEach(function (ok) { grid.appendChild(h("i", { class: ok ? "ok" : "" })); });
348    box.appendChild(grid);
349    box.appendChild(h("p", { class: "round-note", text: "You've played today's ranked round on " + BOOT.brand + ". A new one opens at midnight UTC." }));
350    var gridTxt = (m.grid || []).map(function (ok) { return ok ? "🟩" : "🟥"; }).join("");
351    box.appendChild(h("div", { class: "done-actions" },
352      h("button", { class: "btn btn-primary", type: "button", text: "Challenge a friend", onclick: function () { share(BOOT.brand + " · Daily #" + (BOOT.today ? BOOT.today.number : "") + "\n" + gridTxt + " " + m.correct + "/" + m.total + " · " + m.score + " pts"); } }),
353      h("button", { class: "btn btn-secondary", type: "button", text: "Practice round", onclick: function () { start("practice"); } })));
354  }
355
356  function track(t) {
357    try { fetch("/api/event", { method: "POST", keepalive: true, headers: { "content-type": "application/json", "x-arena-host": BOOT.host }, body: JSON.stringify({ t: t }) }); } catch (e) {}
358  }
359  function beatUrl() {
360    var day = (BOOT.today && BOOT.today.day) || new Date().toISOString().slice(0, 10);
361    var q = state.player ? "?beat=" + state.player.ref_code + "&d=" + day : "";
362    return withArena(location.origin + "/" + q);
363  }
364  // Share sheet: native share when available, otherwise one-tap links. Every share counts once toward arena stats.
365  function share(textMsg, opts) {
366    opts = opts || {};
367    var url = opts.url || beatUrl();
368    var msg = textMsg + (opts.url ? "" : "\nCan you beat me?");
369    var old = $(".sharesheet"); if (old) old.remove();
370    var counted = false;
371    var count = function () { if (!counted) { counted = true; track("share"); } };
372    var enc = encodeURIComponent;
373    var links = [
374      ["X", "https://twitter.com/intent/tweet?text=" + enc(msg) + "&url=" + enc(url)],
375      ["WhatsApp", "https://wa.me/?text=" + enc(msg + "\n" + url)],
376      ["LinkedIn", "https://www.linkedin.com/sharing/share-offsite/?url=" + enc(url)],
377      ["Facebook", "https://www.facebook.com/sharer/sharer.php?u=" + enc(url)],
378      ["Email", "mailto:?subject=" + enc(opts.subject || "Can you beat my score on " + BOOT.brand + "?") + "&body=" + enc(msg + "\n\n" + url)],
379    ];
380    var sheet = h("div", { class: "sharesheet", role: "dialog", "aria-label": "Share" },
381      h("div", { class: "sharesheet-card" },
382        h("div", { class: "sharesheet-top" }, h("b", { text: opts.title || "Challenge a friend" }), h("button", { class: "btn btn-quiet", type: "button", text: "Close", onclick: function () { sheet.remove(); } })),
383        h("pre", { class: "sharesheet-text", text: msg + "\n" + url }),
384        h("div", { class: "sharesheet-links" },
385          navigator.share ? h("button", { class: "btn btn-primary", type: "button", text: "Share…", onclick: function () { count(); navigator.share({ text: msg, url: url }).catch(function () {}); } }) : null,
386          h("button", { class: "btn btn-secondary", type: "button", text: "Copy", onclick: function () { count(); if (navigator.clipboard) navigator.clipboard.writeText(msg + "\n" + url).then(function () { toast("Copied", "Paste it anywhere."); }); } }),
387          links.map(function (l) { return h("a", { class: "btn btn-quiet", href: l[1], target: "_blank", rel: "noopener", text: l[0], onclick: count }); }))));
388    sheet.addEventListener("click", function (e) { if (e.target === sheet) sheet.remove(); });
389    document.body.appendChild(sheet);
390  }
391
392  document.addEventListener("keydown", function (e) {
393    if (!round || round.locked || e.metaKey || e.ctrlKey || e.altKey) return;
394    var tag = (document.activeElement && document.activeElement.tagName) || "";
395    if (tag === "INPUT" || tag === "TEXTAREA") return;
396    var n = parseInt(e.key, 10);
397    if (n >= 1 && n <= 4) { e.preventDefault(); answer(n - 1); }
398  });
399
400  // ---------- leaderboard ----------
401  function loadLeaderboard() {
402    var lb = $("[data-lb]");
403    if (!lb) return;
404    var period = lb.getAttribute("data-period"), scope = lb.getAttribute("data-scope");
405    api("/api/leaderboard?period=" + period + "&scope=" + scope).then(function (r) {
406      var list = $("[data-lb-list]", lb);
407      list.textContent = "";
408      if (!r.rows || !r.rows.length) {
409        list.appendChild(h("li", { class: "lb-empty", text: period === "day" ? "No scores yet today. Play the round and take first place." : "No scores yet. Be the first on the board." }));
410      } else r.rows.forEach(function (row) {
411        var mine = state.player && row.handle === state.player.handle;
412        list.appendChild(h("li", { class: mine ? "is-me" : "" },
413          h("span", { class: "lb-rank", text: row.rank }),
414          h("span", { class: "lb-who" }, h("b", { text: row.handle }), h("span", { text: "Level " + row.level + " · " + row.league.charAt(0).toUpperCase() + row.league.slice(1) + (row.streak > 1 ? " · " + row.streak + "-day streak" : "") })),
415          h("span", { class: "lb-xp" }, fmt(row.xp), h("small", { text: "XP" }))));
416      });
417      var me = $("[data-lb-me]", lb);
418      if (r.me && r.me.rank > r.rows.length) { me.hidden = false; me.textContent = "You're #" + r.me.rank + " of " + fmt(r.players) + " with " + fmt(r.me.xp) + " XP"; }
419      else me.hidden = true;
420    });
421  }
422  $$("[data-lb-period]").forEach(function (b) {
423    b.addEventListener("click", function () {
424      $$("[data-lb-period]").forEach(function (x) { x.setAttribute("aria-selected", x === b ? "true" : "false"); });
425      $("[data-lb]").setAttribute("data-period", b.getAttribute("data-lb-period"));
426      loadLeaderboard();
427    });
428  });
429  $$("[data-lb-scope]").forEach(function (b) {
430    b.addEventListener("click", function () {
431      $$("[data-lb-scope]").forEach(function (x) { x.setAttribute("aria-selected", x === b ? "true" : "false"); });
432      $("[data-lb]").setAttribute("data-scope", b.getAttribute("data-lb-scope"));
433      loadLeaderboard();
434    });
435  });
436
437  // ---------- challenges ----------
438  function loadChallenges() {
439    var cards = $$("[data-challenge]");
440    if (!cards.length) return;
441    api("/api/challenges").then(function (r) {
442      if (!r.challenges) return;
443      var byId = {};
444      r.challenges.forEach(function (c) { byId[c.id] = c; });
445      cards.forEach(function (card) { renderChallenge(card, byId[card.getAttribute("data-challenge")]); });
446    });
447  }
448  function renderChallenge(card, c) {
449    if (!c) return;
450    var count = $("[data-ch-count]", card);
451    count.textContent = c.players ? fmt(c.players) + (c.players === 1 ? " player" : " players") + (c.finished ? " · " + fmt(c.finished) + " finished" : "") : "Be the first to join";
452    var actions = $("[data-ch-actions]", card);
453    actions.textContent = "";
454    var prog = $("[data-ch-progress]", card);
455    var m = c.mine;
456    card.classList.toggle("is-done", !!(m && m.status === "completed"));
457    if (!m) {
458      prog.hidden = true;
459      actions.appendChild(h("button", { class: "btn btn-secondary", type: "button", text: "Join", onclick: function () { act("/api/challenge/join", c, card); } }));
460      return;
461    }
462    if (m.status === "completed") { prog.hidden = true; actions.appendChild(h("span", { class: "ch-done", text: "Completed" })); return; }
463    if (c.kind === "streak") {
464      prog.hidden = false;
465      $("[data-ch-bar]", card).style.width = Math.min(100, (m.checkins / c.days) * 100) + "%";
466      $("[data-ch-label]", card).textContent = "Day " + m.checkins + " of " + c.days;
467      actions.appendChild(m.checked_in_today
468        ? h("span", { class: "ch-count", text: "Checked in today" })
469        : h("button", { class: "btn btn-primary", type: "button", text: "Check in", onclick: function () { act("/api/challenge/checkin", c, card); } }));
470    } else {
471      prog.hidden = true;
472      if (m.status === "submitted") actions.appendChild(h("span", { class: "ch-count", text: "Entry under review" }));
473      else if (m.status === "rejected") actions.appendChild(h("a", { class: "btn btn-secondary", href: withArena("/c/" + c.slug), text: "Resubmit" }));
474      else actions.appendChild(h("a", { class: "btn btn-primary", href: withArena("/c/" + c.slug), text: "Post proof" }));
475    }
476  }
477  function act(path, c, card) {
478    ensurePlayer().then(function (p) {
479      if (!p) return;
480      api(path, { id: c.id }).then(function (r) {
481        if (r.error) return toast("Hmm", r.error);
482        if (r.badges_new) r.badges_new.forEach(function (b) { toast("Badge unlocked: " + b.name, b.desc, b.id); });
483        if (path.indexOf("join") > -1) toast("You're in", c.kind === "streak" ? "Check in once a day for " + c.days + " days." : "Post your proof when you're done.");
484        if (path.indexOf("checkin") > -1 && r.status === "completed") toast("Challenge complete", "+" + c.xp_complete + " XP");
485        else if (path.indexOf("checkin") > -1 && !r.already) toast("Checked in", "Day " + r.checkins + " of " + r.days);
486        loadChallenges(); loadMe();
487      });
488    });
489  }
490
491  function loadGallery() {
492    var g = $("[data-gallery]");
493    if (!g) return;
494    api("/api/challenge/gallery?id=" + encodeURIComponent(g.getAttribute("data-challenge-id"))).then(function (r) {
495      g.textContent = "";
496      if (!r.entries || !r.entries.length) { g.appendChild(h("p", { class: "muted", text: "No entries yet. Yours could be the first." })); return; }
497      r.entries.forEach(function (e) {
498        var foot = h("div", { class: "entry-foot" },
499          e.link ? h("a", { href: e.link, rel: "nofollow ugc noopener", target: "_blank", text: e.link_host || "View link" }) : h("span"),
500          e.status === "completed" ? h("span", { class: "ok", text: "Completed" }) :
501          h("button", { class: "btn btn-quiet", type: "button", text: "▲ " + e.votes, "aria-label": "Vote for " + e.handle + "'s entry", onclick: function (ev) {
502            var btn = ev.currentTarget;
503            ensurePlayer().then(function (p) {
504              if (!p) return;
505              api("/api/challenge/vote", { entry_id: e.entry_id }).then(function (v) {
506                if (v.error) return toast("Can't vote yet", v.error);
507                btn.textContent = "▲ " + v.votes; btn.disabled = true;
508              });
509            });
510          } }));
511        g.appendChild(h("div", { class: "entry" }, h("div", { class: "entry-top" }, h("b", { text: e.handle }), h("span", { class: "muted", text: "Level " + e.level })), e.note ? h("p", { text: e.note }) : null, foot));
512      });
513    });
514  }
515
516  // ---------- forms ----------
517  function formData(form) {
518    var o = {};
519    $$("input,select,textarea", form).forEach(function (el) { if (el.name) o[el.name] = el.value; });
520    return o;
521  }
522  function note(sel, msg, kind) {
523    var n = $(sel); if (!n) return;
524    n.textContent = msg; n.classList.toggle("is-bad", kind === "bad"); n.classList.toggle("is-good", kind === "good");
525  }
526  var savedEmail = "";
527  $$("[data-form='email']").forEach(function (form) {
528    form.addEventListener("submit", function (e) {
529      e.preventDefault();
530      var d = formData(form);
531      if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(d.email)) return note("[data-save-note]", "Enter a valid email address.", "bad");
532      var btn = $("button", form); btn.disabled = true;
533      ensurePlayer().then(function (p) {
534        if (!p) { btn.disabled = false; return; }
535        api("/api/player/email", d).then(function (r) {
536          btn.disabled = false;
537          if (r.error) return note("[data-save-note]", r.error, "bad");
538          savedEmail = d.email;
539          if (r.sent) {
540            form.hidden = true;
541            var v = $("[data-form='verify']"); if (v) { v.hidden = false; $("input", v).focus(); }
542            note("[data-save-note]", "We sent a 6-digit code to " + d.email + ". Enter it to finish.", "good");
543          } else {
544            note("[data-save-note]", r.merged_into_existing ? "That email already has a profile. We'll add sign-in codes soon so you can load it here." : "Saved. Your progress is linked to " + d.email + ".", "good");
545          }
546          loadMe();
547        });
548      });
549    });
550  });
551  $$("[data-form='verify']").forEach(function (form) {
552    form.addEventListener("submit", function (e) {
553      e.preventDefault();
554      var d = formData(form);
555      api("/api/player/verify", { email: savedEmail, code: d.code }).then(function (r) {
556        if (r.error) return note("[data-save-note]", r.error, "bad");
557        form.hidden = true;
558        note("[data-save-note]", r.merged ? "Welcome back. Your saved profile is loaded on this device." : "Your profile is saved.", "good");
559        loadMe().then(loadLeaderboard);
560      });
561    });
562  });
563  $$("[data-form='sponsor']").forEach(function (form) {
564    form.addEventListener("submit", function (e) {
565      e.preventDefault();
566      var d = formData(form);
567      if (!d.name || !d.message || !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(d.email)) return note("[data-sponsor-note]", "Add your name, a work email and what you'd like to sponsor.", "bad");
568      var btn = $("button", form); btn.disabled = true;
569      api("/api/sponsor", d).then(function (r) {
570        btn.disabled = false;
571        if (r.error) return note("[data-sponsor-note]", r.error, "bad");
572        form.reset();
573        note("[data-sponsor-note]", "Thanks. We'll reply within two business days.", "good");
574      });
575    });
576  });
577  $$("[data-form='proof']").forEach(function (form) {
578    form.addEventListener("submit", function (e) {
579      e.preventDefault();
580      var d = formData(form);
581      d.id = form.getAttribute("data-challenge-id");
582      ensurePlayer().then(function (p) {
583        if (!p) return;
584        api("/api/challenge/submit", d).then(function (r) {
585          if (r.need_email) { note("[data-proof-note]", r.error, "bad"); var s = $("#saveEmail"); if (s) s.focus(); return; }
586          if (r.error) return note("[data-proof-note]", r.error, "bad");
587          form.reset();
588          note("[data-proof-note]", "Entry posted. It completes when approved or when it reaches 5 votes.", "good");
589          loadGallery(); loadChallenges();
590        });
591      });
592    });
593  });
594  $$("[data-form='partner']").forEach(function (form) {
595    form.addEventListener("submit", function (e) {
596      e.preventDefault();
597      var d = formData(form);
598      if (!d.name || !d.message || !d.interest || !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(d.email)) return note("[data-partner-note]", "Add your name, email, what you're interested in and a short message.", "bad");
599      var btn = $("button", form); btn.disabled = true;
600      api("/api/partner", d).then(function (r) {
601        btn.disabled = false;
602        if (r.error) return note("[data-partner-note]", r.error, "bad");
603        form.reset();
604        note("[data-partner-note]", "Thanks. Your inquiry is in, and a person will reply within two business days.", "good");
605      });
606    });
607  });
608  $$("[data-share-page]").forEach(function (b) {
609    b.addEventListener("click", function () {
610      share(document.title, { url: withArena(location.origin + location.pathname), title: "Share this page", subject: document.title });
611    });
612  });
613
614  // ---------- warm-up question ----------
615  $$("[data-warmup]").forEach(function (w) {
616    var ans = +w.getAttribute("data-answer");
617    $$("[data-wchoice]", w).forEach(function (b) {
618      b.addEventListener("click", function () {
619        if (w.classList.contains("is-answered")) return;
620        w.classList.add("is-answered");
621        var pick = +b.getAttribute("data-wchoice");
622        $$("[data-wchoice]", w).forEach(function (x) {
623          var i = +x.getAttribute("data-wchoice");
624          x.disabled = true;
625          x.classList.add(i === ans ? "is-right" : i === pick ? "is-wrong" : "is-dim");
626        });
627        var ok = pick === ans;
628        $("[data-warmup-verdict]", w).textContent = ok ? "Correct. You're warmed up." : "Not quite. The answer is " + $("[data-wchoice='" + ans + "'] span", w).textContent + ".";
629        $("[data-warmup-verdict]", w).classList.toggle("is-good", ok);
630        $("[data-warmup-after]", w).hidden = false;
631        track("warmup"); if (ok) track("warmup_ok");
632      });
633    });
634    var play = $("[data-warmup-play]", w);
635    if (play) play.addEventListener("click", playFromHero);
636  });
637  function playFromHero() {
638    if (!roundEl) { location.href = withArena("/#play"); return; }
639    roundEl.scrollIntoView({ behavior: REDUCED ? "auto" : "smooth", block: "center" });
640    var daily = $("[data-start='daily']", roundEl);
641    if (daily && !daily.disabled && !daily.hidden) daily.click();
642  }
643
644  // ---------- head-to-head ----------
645  function beatTarget() {
646    if (BOOT.beat) return BOOT.beat;
647    try { var t = JSON.parse(LS.get("ic_beat") || "null"); return t && t.day === (BOOT.today && BOOT.today.day) && t.host === BOOT.host ? t : null; } catch (e) { return null; }
648  }
649  if (BOOT.beat) {
650    var bt = BOOT.beat;
651    LS.set("ic_beat", JSON.stringify({ code: bt.code, handle: bt.handle, score: bt.score, day: bt.day, today: bt.today, host: BOOT.host }));
652    if (!LS.get("ic_ref")) LS.set("ic_ref", bt.code);
653    if (!LS.get("ic_beat_seen_" + bt.code + bt.day)) { LS.set("ic_beat_seen_" + bt.code + bt.day, "1"); track("beat_view"); }
654  }
655  $$("[data-beat-play]").forEach(function (b) { b.addEventListener("click", playFromHero); });
656
657  $$("[data-form='handle']").forEach(function (form) {
658    form.addEventListener("submit", function (e) {
659      e.preventDefault();
660      var d = formData(form);
661      ensurePlayer().then(function (p) {
662        if (!p) return;
663        api("/api/player/handle", d).then(function (r) {
664          if (r.error) return toast("Couldn't rename", r.error);
665          form.reset(); toast("Handle updated", d.handle); loadMe();
666        });
667      });
668    });
669  });
670  $$("[data-copy-invite]").forEach(function (b) {
671    b.addEventListener("click", function () {
672      var t = $("[data-invite-link]").textContent;
673      if (navigator.clipboard) navigator.clipboard.writeText(t).then(function () { toast("Invite link copied", "Friends who finish a round earn you +100 XP."); });
674    });
675  });
676
677  function loadHistory() {
678    var list = $("[data-history]");
679    if (!list || !state.token) return;
680    api("/api/me/history").then(function (r) {
681      if (!r.rounds || !r.rounds.length) return;
682      list.textContent = "";
683      r.rounds.forEach(function (x) {
684        list.appendChild(h("li", null, h("span", { text: x.domain + " · " + x.day + (x.mode === "practice" ? " · practice" : "") }), h("b", { text: x.correct + "/" + x.total }), h("b", { text: fmt(x.score) + " pts" })));
685      });
686    });
687  }
688
689  // ---------- network pass: carry the profile to another arena in one click ----------
690  document.addEventListener("click", function (e) {
691    var a = e.target.closest && e.target.closest("a[data-arena]");
692    if (!a || !state.token || BOOT.preview || e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
693    e.preventDefault();
694    var href = a.href;
695    api("/api/pass", {}).then(function (r) {
696      location.href = r.pass ? href.split("#")[0] + "#icpass=" + encodeURIComponent(r.pass) : href;
697    });
698  });
699  function redeemPassFromHash() {
700    var m = location.hash.match(/icpass=([^&]+)/);
701    if (!m) return Promise.resolve();
702    history.replaceState(null, "", location.pathname + location.search);
703    return api("/api/pass/redeem", { pass: decodeURIComponent(m[1]) }).then(function (r) {
704      if (r.token) { state.token = r.token; LS.set(TOKEN_KEY, r.token); state.player = r.player; toast("Welcome, " + r.player.handle, "Your level and streak came with you."); }
705      else if (r.kept_current) toast("You're already playing here", "Save your email on both arenas to combine progress.");
706    });
707  }
708
709  // ---------- analytics beacon (JS-executing visitors only) ----------
710  function beacon() {
711    var key = "ic_seen_" + new Date().toISOString().slice(0, 10);
712    var first = !LS.get(key);
713    LS.set(key, "1");
714    var payload = JSON.stringify({ u: first ? 1 : 0, p: location.pathname, r: document.referrer ? document.referrer.slice(0, 300) : "" });
715    try {
716      fetch("/api/beacon", { method: "POST", keepalive: true, headers: { "content-type": "application/json", "x-arena-host": BOOT.host }, body: payload });
717    } catch (e) {}
718  }
719
720  // ---------- boot ----------
721  var ref = new URLSearchParams(location.search).get("ref");
722  if (ref && /^[A-Za-z0-9]{6,10}$/.test(ref)) LS.set("ic_ref", ref.toUpperCase());
723  $$("[data-start]").forEach(function (b) { b.addEventListener("click", function () { start(b.getAttribute("data-start")); }); });
724  if (BOOT.preview) $$("a[href^='/']").forEach(function (a) { if (a.getAttribute("href").indexOf("arena=") < 0 && a.getAttribute("href").indexOf("/.well-known") < 0 && a.getAttribute("href").indexOf("/sitemap") < 0) a.setAttribute("href", withArena(a.getAttribute("href"))); });
725  redeemPassFromHash().then(loadMe).then(function () {
726    refreshToday(); loadLeaderboard(); loadChallenges(); loadGallery(); loadHistory();
727    if (location.hash === "#play" && roundEl) roundEl.scrollIntoView({ block: "nearest" });
728  });
729  beacon();
730})();

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.