File size: 28,810 Bytes
a78d80c
 
 
 
 
 
 
 
 
 
 
 
 
49fa989
a78d80c
 
49fa989
f38ba17
49fa989
988fd81
49fa989
 
 
 
485f41b
49fa989
a78d80c
 
 
 
49fa989
 
a78d80c
49fa989
 
7d6398e
 
 
 
49fa989
a78d80c
 
 
 
 
 
 
 
49fa989
988fd81
 
 
 
 
 
7d6398e
988fd81
49fa989
 
988fd81
7d6398e
49fa989
 
 
988fd81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a78d80c
988fd81
 
 
 
49fa989
a78d80c
49fa989
 
 
 
 
a78d80c
 
 
 
 
 
7d6398e
a78d80c
 
 
 
 
 
 
49fa989
 
 
 
 
 
 
 
a78d80c
 
 
 
49fa989
 
 
 
 
7d6398e
49fa989
 
 
 
 
 
 
a78d80c
 
49fa989
 
 
 
 
 
 
 
 
 
 
 
 
a78d80c
 
 
49fa989
 
 
 
7d6398e
a78d80c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49fa989
 
a78d80c
 
7d6398e
 
a78d80c
7d6398e
 
a78d80c
7d6398e
 
a78d80c
7d6398e
a78d80c
 
49fa989
 
a78d80c
7d6398e
a78d80c
49fa989
 
 
f38ba17
49fa989
 
a78d80c
49fa989
 
 
 
 
 
 
 
 
 
f38ba17
 
 
7d6398e
 
 
 
 
 
 
 
 
 
 
 
 
f38ba17
 
7d6398e
a78d80c
f38ba17
 
 
a78d80c
 
7d6398e
a78d80c
 
 
 
7d6398e
f38ba17
a78d80c
f38ba17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d6398e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a78d80c
 
49fa989
 
988fd81
49fa989
a78d80c
 
 
 
 
49fa989
 
 
988fd81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
// CyberMax games checkout: the per-game "full game" unlock + the Fun Pass (every game), copied into each game's web/ folder.
// Model (games lane GM, 9 Oct 2026; evidence docs/ceo/games-monetization-2026-10-09.md):
//   - Free demo: every game's daily challenge stays free for everyone, plus the first levels (OFFER.demo once the per-game
//     unlock is live; levels 1-20 until then). A level the player has opened is never locked again.
//   - Full game: ONE payment per game (OFFER.price, Stripe pack revenue/packs/stripe/<game>-full) unlocks every level,
//     mode, the daily archive and themes in that game, for good. Shown only when OFFER.live is true (owner-approved).
//   - Fun Pass = the all-games bundle: Monthly $2.99 · Yearly $19 · Lifetime $29.99 (pack revenue/packs/stripe/fun-pass).
//   - Portal builds (scripts/games/portals.py) never show a checkout; where the portal SDK offers rewarded ads, the player
//     may watch one to open the full game for an hour (shared/portal-adapter/cmx-portal.js CMXPortal.rewarded()).
// Buy: counted redirect https://data.cybermaxtools.com/buy/st-<pack>-<plan>?s=game-<id> -> Stripe checkout.
// After checkout the data Worker (/unlock/fun-pass or /unlock/<game>-full) checks the Checkout Session and returns with
// #cmx-pro=<code> (the full-game return adds ?unlock=full). The code on the receipt page unlocks any other device.
// No account, no cookies, nothing personal stored. (c) 2026 CyberMax. All rights reserved.
import { count, store, toast } from './cmx.js';
import { OFFER } from './offer.js'; // written by scripts/games/sync_kit.py from products/fun-pass/games.json
export { OFFER };

const DATA = 'https://data.cybermaxtools.com';
const KEY = 'cmx-pro:fun-pass';
const SID = /^(cmxk1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+|cs_(live|test)_[A-Za-z0-9]{10,200}|[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8})$/i; // Stripe order code or Gumroad licence key
const RECHECK = 3 * 864e5; // subscriptions are re-checked every 3 days; a cancelled pass turns off by itself
export const PLANS = [
  { id: 'monthly', label: 'Monthly', price: '$2.99', per: '/month', note: 'Cancel any time' },
  { id: 'yearly', label: 'Yearly', price: '$19', per: '/year', note: 'Save 47%', best: true },
  { id: 'unlock', label: 'Lifetime', price: '$29.99', per: ' once', note: 'Pay once, keep forever' },
];
export const COMMON_PERKS = ['Every level and mode in every CyberMax game', 'The full archive of past dailies', 'Custom colour themes', 'New games included as they launch'];
const FULL_KEY = `cmx-full:${OFFER.game}`;
const REWARD_KEY = `cmx-reward:${OFFER.game}`;
const REWARD_MS = 60 * 60 * 1000; // one rewarded ad on a portal build opens the full game for an hour

let state = store(KEY);
let full = store(FULL_KEY);
const listeners = [];
let cfg = { game: 'games', name: 'CyberMax games', perks: [] };
// Per-game offer naming (17 Oct 2026 conversion fix): games whose one-time unlock is not a "full game" (no levels, every
// mode free) pass init({ title: 'Stoopshake Crew Pass', exact: true, free: '...' }): the sheet then names the offer exactly
// as the Stripe product, lists only its own perks and states what stays free, instead of "every level / bonus mode".
const fullTitle = () => cfg.title || `Full ${OFFER.name}`;

const passOn = () => !!(state && state.code);
export const ownsGame = () => !!(full && full.code);
const rewardOn = () => { const r = store(REWARD_KEY); return !!(r && Date.now() - r.at < REWARD_MS); };
/** True when this player may use everything in this game: Fun Pass, this game's full unlock, or a portal reward hour. */
export const isPro = () => passOn() || ownsGame() || rewardOn();
/** Free levels in this game's demo: OFFER.demo once the per-game unlock is live, else the game's current number. */
export const freeLevels = (current = 20) => (OFFER.live && OFFER.demo ? Math.min(current, OFFER.demo) : current);
const portal = () => (typeof window !== 'undefined' && window.CMXPortal && window.CMXPortal.portal) || '';
export function onChange(fn) { listeners.push(fn); fn(isPro()); }
// The header pass button names the real price (convert lane, 15 Oct 2026): "Full game $2.99" where this game sells its
// own unlock, else the Fun Pass monthly price. Pass holders see the button's own label.
function priceButton() {
  const btn = typeof document !== 'undefined' && document.getElementById('pass-btn');
  if (!btn) return;
  if (!btn.dataset.label) btn.dataset.label = btn.textContent;
  btn.textContent = isPro() || portal() ? btn.dataset.label : sellsGame() ? `✨ ${cfg.title ? cfg.short || cfg.title : 'Full game'} ${OFFER.price}` : `✨ Fun Pass ${PLANS[0].price}/mo`;
}
function emit() {
  document.documentElement.classList.toggle('cmx-pro', isPro());
  priceButton();
  paintShop();
  listeners.forEach((fn) => { try { fn(isPro()); } catch { /* game hook */ } });
}

// Offline Pro keys (17 Oct 2026): "cmxk1.<payload>.<Ed25519 signature>" from data.cybermaxtools.com/unlock is verified here
// with WebCrypto and this public key, so an unlock needs no Worker call. Expired, foreign-signed or unverifiable keys fall
// back to the online check of the order code inside them, as before.
const CMX_PUB = 'u0FDxbu0d9B9Sz5-yLT6AuL5IagyTlFcbBgEDqZxWtk';
const b64d = (s) => { s = s.replace(/-/g, '+').replace(/_/g, '/'); while (s.length % 4) s += '='; return Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); };
async function cmxKeyOk(k, app, online) {
  const [tag, body, sig] = k.split('.');
  let p;
  try { p = JSON.parse(new TextDecoder().decode(b64d(body))); } catch { return false; }
  if (!p || p.a !== app) return false;
  try {
    const key = await crypto.subtle.importKey('raw', b64d(CMX_PUB), { name: 'Ed25519' }, false, ['verify']);
    if (await crypto.subtle.verify({ name: 'Ed25519' }, key, b64d(sig), new TextEncoder().encode(tag + '.' + body)) && (!p.x || p.x * 1000 > Date.now())) return true;
  } catch { /* no Ed25519 here: online check below */ }
  return p.c ? online(p.c) : false;
}
async function check(code, what = 'fun-pass') {
  if (/^cmxk1\./.test(code)) return cmxKeyOk(code, what, (c) => checkOnline(c, what));
  return checkOnline(code, what);
}
async function checkOnline(code, what = 'fun-pass') {
  try {
    const r = await fetch(`${DATA}/unlock/${what}?json=1&session_id=${encodeURIComponent(code)}`);
    const j = await r.json();
    return j && typeof j.pro === 'boolean' ? j.pro : null;
  } catch { return null; }
}

/** Turn on a full-game code for this game (receipt code of the <game>-full Payment Link). */
export async function unlockGame(code) {
  const ok = await check(code, `${OFFER.game}-full`);
  if (ok) {
    full = store(FULL_KEY, { code, at: new Date().toISOString() });
    count(cfg.game, 'pro', 'full-unlocked');
    toast(cfg.title ? `${cfg.title} is on. Thank you for supporting CyberMax games!` : `${OFFER.name}: the full game is on. Thank you for supporting CyberMax games!`, 3500);
    emit(); render();
  } else if (ok === false) toast('That code is not a full-game order for this game.', 3500);
  else toast('Could not reach the unlock check. Try again in a minute.', 3500);
  return ok;
}

/** Turn on a pasted or returned code: a Fun Pass code, or (where this game sells its own unlock) a full-game code. */
export async function unlock(code) {
  const ok = await check(code);
  if (ok) {
    state = store(KEY, { code, at: new Date().toISOString(), checked: Date.now() });
    count(cfg.game, 'pro', 'unlocked');
    toast('Fun Pass is on. Thank you for supporting CyberMax games!', 3500);
    emit();
    render();
    return ok;
  }
  if (sellsGame() && (await check(code, `${OFFER.game}-full`))) return unlockGame(code);
  if (ok === false) toast('That code is not an active Fun Pass.', 3500);
  else toast('Could not reach the pass check. Try again in a minute.', 3500);
  return ok;
}

async function recheck() {
  if (!passOn() || Date.now() - (state.checked || 0) < RECHECK) return; // only a Fun Pass is re-checked (a full-game or Founder owner has no pass state)
  const ok = await check(state.code);
  if (ok === false) { state = null; try { localStorage.removeItem(KEY); } catch { /* */ } emit(); render(); toast('Your Fun Pass has ended. Everything free is still here.', 4000); }
  else if (ok) state = store(KEY, { ...state, checked: Date.now() });
}

const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const buyUrl = (plan) => `${DATA}/buy/st-fun-pass-${plan}?s=game-${cfg.game}`;
const gameUrl = (from = '') => `${DATA}/buy/st-${OFFER.game}-full-unlock?s=game-${cfg.game}${from ? `-${from}` : ''}`;
const sellsGame = () => !!(OFFER.live && OFFER.price && OFFER.game !== 'games');

function dialog() {
  let d = document.getElementById('funpass');
  if (!d) {
    d = document.createElement('dialog');
    d.id = 'funpass';
    d.className = 'funpass';
    document.body.appendChild(d);
    d.addEventListener('click', (e) => { if (e.target === d || e.target.closest('[data-close]')) d.close(); });
  }
  return d;
}

const planHtml = () => PLANS.map((p) => `<a class="fp-plan${p.best ? ' best' : ''}" href="${buyUrl(p.id)}" rel="nofollow" data-plan="${p.id}">${p.best ? '<i>Best value</i>' : ''}<b>${p.label}</b><span class="fp-price">${p.price}<small>${p.per}</small></span><span class="fp-note">${p.note}</span></a>`).join('');
const codeForm = `<details><summary>Already bought?</summary><form class="fp-form"><input aria-label="Order code" autocomplete="off" placeholder="Receipt code (cs_live_…) or Gumroad licence key"><button class="btn sm" type="submit">Turn on</button></form></details>`;

function render() {
  const d = dialog();
  const perks = [...(cfg.perks || []), ...COMMON_PERKS.filter((p) => !(cfg.perks || []).some((q) => q.split(' ')[0] === p.split(' ')[0]))].slice(0, 6);
  if (isPro()) {
    const what = passOn() ? 'The Fun Pass is on in this browser for every CyberMax game' : ownsGame() ? (cfg.title ? `You own the ${esc(cfg.title)}` : `You own the full ${esc(OFFER.name)}`) : 'The full game is open for the next hour (rewarded ad)';
    const code = passOn() ? state.code : ownsGame() ? full.code : '';
    d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${passOn() ? 'CyberMax Fun Pass' : esc(OFFER.name)}</p><h2>Everything is open. Thank you!</h2>
      <p>${what}: ${perks.map(esc).join(' · ')}.</p>
      ${code ? `<p class="fp-small">Your code (paste it on another device to turn it on there):</p><code class="fp-code">${esc(code)}</code>` : ''}
      ${passOn() ? '<p class="fp-small">Manage or cancel a subscription any time from the link in your Stripe receipt email.</p>' : ''}</div>`;
    return;
  }
  if (portal()) { // portal build: no outside checkout; a rewarded ad (when the portal offers one) opens the game for an hour
    d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name || 'Full game')}</p>
      <h2>Open the full game for an hour</h2><ul class="fp-perks">${perks.slice(0, 4).map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
      <p><button class="btn" type="button" data-reward>▶ Watch a short ad</button></p>
      <p class="fp-small">Optional. The daily challenge and the free levels stay free without ads.</p></div>`;
    d.querySelector('[data-reward]').addEventListener('click', async () => {
      count(cfg.game, 'cta', 'reward-ad');
      const ok = await window.CMXPortal.rewarded().catch(() => false);
      if (ok) { store(REWARD_KEY, { at: Date.now() }); count(cfg.game, 'pro', 'reward-hour'); toast('Everything is open for the next hour. Have fun!', 3000); emit(); d.close(); }
      else toast('No ad is available right now. Try again later.', 3000);
    });
    return;
  }
  const allGames = `${OFFER.games || 'every'} CyberMax games`;
  const own = cfg.perks || [];
  const gamePerks = cfg.exact ? own.slice(0, 6) : [...own, ...(own.some((p) => /archive/i.test(p)) ? [] : ['The full archive of past dailies']), 'Every bonus mode in this game'].slice(0, 6);
  const freeLine = cfg.free || `The daily challenge and ${OFFER.levels === false ? 'the free modes stay' : `the first ${OFFER.demo || ''} levels stay`} free for everyone.`;
  d.innerHTML = sellsGame()
    ? `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name)} · ${cfg.title ? 'one payment' : 'full game'}</p>
    <h2>${cfg.title ? `Get the ${esc(cfg.title)}` : `Unlock the full ${esc(OFFER.name)}`}</h2>
    <ul class="fp-perks">${gamePerks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
    <a class="fp-plan fp-game" href="${gameUrl()}" rel="nofollow" data-game><i>This game only</i><b>${esc(fullTitle())}</b><span class="fp-price">${esc(OFFER.price)}<small> once</small></span><span class="fp-note">Pay once, keep it forever. No subscription.</span></a>
    <p class="fp-or"><b>Or get all ${esc(allGames)}</b> with the Fun Pass (a separate pass for every game):</p>
    <div class="fp-plans">${planHtml()}</div>
    <p class="fp-small">Secure checkout by Stripe (shown as Link) · turns on as soon as you pay · your code works on any device · 14-day fix-or-refund if it doesn't work as described (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). ${esc(freeLine)} Prices in USD; tax for your country is added at checkout.</p>
    ${codeForm}</div>`
    : `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">CyberMax Fun Pass</p>
    <h2>One pass. Every game. More to play.</h2>
    <ul class="fp-perks">${perks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
    <div class="fp-plans">${planHtml()}</div>
    <p class="fp-small">Secure checkout by Stripe (shown as Link; card, Apple Pay, Google Pay) · turns on as soon as you pay · cancel any time from your receipt email · 14-day fix-or-refund if it doesn't work as described (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). Prices in USD, + tax where applicable. Works in every CyberMax game. The daily challenges and the free levels stay free for everyone.</p>
    ${codeForm}</div>`;
  d.querySelector('.fp-form').addEventListener('submit', (e) => {
    e.preventDefault();
    const c = e.target.querySelector('input').value.trim();
    if (SID.test(c)) unlock(c); else toast('Paste the code from your receipt page (cs_live_…) or your Gumroad licence key.', 3500);
  });
  d.querySelectorAll('[data-plan]').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', `pass-${a.dataset.plan}`)));
  d.querySelector('[data-game]')?.addEventListener('click', () => count(cfg.game, 'cta', 'full-game'));
}

/** Open the pass sheet; `from` names the button that opened it (counted as cta). */
export function open(from = 'button') {
  render();
  const d = dialog();
  if (d.showModal) d.showModal(); else d.setAttribute('open', '');
  count(cfg.game, 'cta', `pass-open-${from}`);
}

/** After a finished round (the value moment): one small inline card under the result, never a pop-up. Shows the
 * pass price, the honest trust line and an optional "email me" weekly reminder (double opt-in).
 * `host` is the result panel; the card is added once and removed for pass holders. */
// One quiet line under the result: CyberMax's free Roblox games (24 Oct 2026). Not on game portals (their rules ban
// outside links), never a pop-up; links carry the game id so clicks can be told apart.
const ROBLOX = [['Skyline Flight Circuit', 'https://www.roblox.com/games/89786604994377'], ['Hatchlume', 'https://www.roblox.com/games/80122923062327'],
  ['Sled Volt', 'https://www.roblox.com/games/134881648422275']];
function robloxRow(host) {
  if (portal() || host.querySelector('.fp-rbx')) return;
  const p = document.createElement('p'); p.className = 'fp-rbx';
  const tag = `?utm_source=cybermax&utm_medium=game&utm_campaign=${encodeURIComponent(cfg.game || 'games')}-end`;
  p.innerHTML = 'Play our Roblox games: ' + ROBLOX.map(([n, u]) => `<a href="${u}${tag}" target="_blank" rel="noopener">${esc(n)}</a>`).join(' · ');
  p.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', 'roblox-end')));
  host.appendChild(p);
}

export function afterWin(host, from = 'win') {
  if (!host) return;
  robloxRow(host);
  if (isPro() || portal()) { host.querySelector('.fp-after')?.remove(); return; }
  let el = host.querySelector('.fp-after');
  if (!el) { el = document.createElement('div'); el.className = 'fp-after'; host.appendChild(el); }
  const best = PLANS.find((p) => p.best) || PLANS[0];
  const game = sellsGame();
  const href = game ? gameUrl('after') : `${buyUrl(best.id)}-after`;
  const label = game ? (cfg.title ? `Get the ${cfg.title}: ${OFFER.price} once` : `Get the full ${OFFER.name}: ${OFFER.price} once`) : `Get the Fun Pass: ${best.price}${best.per}`;
  const line = game ? (cfg.extra || `The full game opens every level, mode, the daily archive and themes in ${OFFER.name}.`)
    : (cfg.extra || 'The Fun Pass opens every level, mode, the full archive and themes in every CyberMax game.');
  el.innerHTML = `<p><b>Want more today?</b> ${esc(line)}</p>
    <p class="fp-row"><a class="btn sm" href="${href}" rel="nofollow" data-after>${esc(label)}</a> <button class="btn sm ghost" type="button" data-plans>${game ? 'All games pass' : 'See all plans'}</button></p>
    <p class="fp-small">Secure checkout by Stripe (shown as Link) · turns on as soon as you pay · ${game ? 'one payment, no subscription' : 'cancel any time from your receipt email'} · 14-day fix-or-refund if it doesn't work as described (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). Prices in USD, + tax where applicable. Today's daily challenge stays free.</p>
    <div class="fp-cap"></div>`;
  el.querySelector('[data-after]').addEventListener('click', () => { count(cfg.game, 'cta', game ? 'full-game-after' : `pass-${best.id}-after`); kit((K) => K.event('upgrade-click', cfg.game)); });
  el.querySelector('[data-plans]').addEventListener('click', () => open(`after-${from}`));
  kit((K) => {
    K.event('result', cfg.game); K.event('upgrade-view', cfg.game);
    K.capture(el.querySelector('.fp-cap'), { list: `weekly-${cfg.game}`, src: `game-${cfg.game}`, label: cfg.remind || 'Email me a weekly reminder with new puzzles (free)', button: 'Remind me' });
  });
}

// Convert kit (funnel beacons + email capture) from the CyberMax site; optional, the game works without it.
let kitQ = null;
function kit(fn) {
  if (window.CMXConvert) return fn(window.CMXConvert);
  if (kitQ) return kitQ.push(fn);
  kitQ = [fn];
  const sc = document.createElement('script');
  sc.src = 'https://cybermaxtools.com/store/convert.js'; sc.async = true;
  sc.onload = () => kitQ.splice(0).forEach((f) => { try { f(window.CMXConvert); } catch { /* optional */ } });
  document.head.appendChild(sc);
}

// ------------------------------------------------------------------ the Shop (owner, 29 Oct 2026: "there's nothing to buy")
// One clear Shop button on the title/menu screen and one quiet button on the results screen open ONE sheet that lists
// everything this game sells, cheapest first (OFFER.shop, written by scripts/games/sync_kit.py from products/fun-pass/shop.json;
// prices come from the Stripe packs): coin packs, Skin/Look/Founder Packs, the full game, the Fun Pass and the Arcade
// Lifetime. Buttons are plain markup with data-cmx-shop="<where>" (menu, title, results); this file labels them, opens
// the sheet and hides them on portal and Telegram builds (no outside checkout there). Never shown during play: the
// buttons live on menu/results screens or above the game, so the play HUD is unchanged. Buy links carry
// s=game-<id>-shop-<where>; cmx.js adds the first-touch tag on click. Nothing pops up by itself.
const SHOP = Array.isArray(OFFER.shop) ? OFFER.shop : [];
const TG = typeof location !== 'undefined' && (/[?&]tg(=|&|$)/.test(location.search) || /tgWebAppData=/.test(location.hash));
/** True where no web checkout may show: portal builds and the Telegram Mini App (Stars only). */
export const noShop = () => !!portal() || TG || !SHOP.length;
const slug = (s) => String(s || 'shop').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 24) || 'shop';
const shopHref = (offer, from) => `${DATA}/buy/${offer}?s=game-${OFFER.game}-shop-${slug(from)}`;
const ownedKind = (it) => {
  if (it.kind === 'pass') return passOn();
  if (it.kind === 'full') return ownsGame();
  if (it.kind === 'skins') return !!store(`cmx-skins:${OFFER.game}`) || !!store(`cmx-founder:${OFFER.game}`);
  if (it.kind === 'founder') return !!store(`cmx-founder:${OFFER.game}`);
  if (it.kind === 'collector') return !!store(`cmx-collector:${OFFER.game}`); // looks Collector Packs (skins.js v2)
  return false; // coin packs, looks packs and the Arcade Lifetime can always be bought
};
/** The Shop items, cheapest first, with an owned flag. */
export const shopItems = () => SHOP.slice().sort((a, b) => a.cents - b.cents).map((it) => ({ ...it, owned: ownedKind(it) }));
/** Label for a Shop button: the menu/title one names the lowest price, the results one the most relevant offer. */
export function shopLabel(where = 'menu') {
  if (where === 'results') return esc(OFFER.shopResults || '🛍️ See the shop →');
  return `🛍️ Shop${OFFER.shopFrom ? ` <small>from ${esc(OFFER.shopFrom)}</small>` : ''}`;
}
/** Markup for a Shop button in screens a game draws itself ('' where no checkout may show). */
export function shopButtonHtml(where = 'menu', cls = '') {
  if (noShop()) return '';
  const c = where === 'results' ? 'btn ghost sm cmx-shopres' : 'btn cmx-shopbtn';
  return `<button class="${c}${cls ? ` ${cls}` : ''}" type="button" data-cmx-shop="${esc(where)}">${shopLabel(where)}</button>`;
}
function paintShop() {
  if (typeof document === 'undefined') return;
  const off = noShop();
  document.documentElement.classList.toggle('cmx-noshop', off);
  for (const b of document.querySelectorAll('[data-cmx-shop]')) {
    if (off) { if (!b.hasAttribute('data-cmx-keep')) b.hidden = true; continue; } // data-cmx-keep: the game's own button, its own handler
    const where = b.dataset.cmxShop || 'menu';
    const html = shopLabel(where === 'results' ? 'results' : 'menu');
    if (b.dataset.cmxLabel !== html) { b.innerHTML = html; b.dataset.cmxLabel = html; }
    if (b.hasAttribute('data-cmx-hidden')) continue; // the game shows/hides this one itself
    b.hidden = false;
  }
}
const swatch = (it) => `background:${it.sw && it.sw.length > 1 ? `linear-gradient(135deg,${it.sw.map(esc).join(',')})` : esc((it.sw || ['#FF6A3D'])[0])}`;
function shopDialog() {
  let d = document.getElementById('cmxshop');
  if (!d) {
    d = document.createElement('dialog');
    d.id = 'cmxshop';
    d.className = 'funpass cmxshop';
    d.setAttribute('aria-label', `${OFFER.name} shop`);
    document.body.appendChild(d);
    d.addEventListener('click', (e) => { if (e.target === d || e.target.closest('[data-close]')) d.close(); });
  }
  return d;
}
function renderShop(from) {
  const d = shopDialog();
  const rows = shopItems().map((it) => {
    const img = it.img ? `<img src="${esc(it.img)}" alt="" loading="lazy" onerror="this.remove()">` : '';
    const buy = it.owned ? '<span class="sh-own">Owned ✓</span>'
      : `<a class="btn sm pass sh-buy" href="${shopHref(it.offer, from)}" rel="nofollow" data-offer="${esc(it.offer)}">${esc(it.price)}<small>${esc(it.per)}</small></a>`;
    const alt = !it.owned && it.alt ? `<span class="sh-alt">or ${it.alt.map((a) => `<a href="${shopHref(a.offer, from)}" rel="nofollow" data-offer="${esc(a.offer)}">${esc(a.label)}</a>`).join(' · ')}</span>` : '';
    return `<li class="sh-i sh-${esc(it.kind)}"><span class="sh-pv" style="${swatch(it)}" aria-hidden="true"><i>${esc(it.icon || '🛍️')}</i>${img}</span>`
      + `<span class="sh-t"><b>${esc(it.name)}</b><span class="sh-w"><em>What you get:</em> ${esc(it.what)}</span>${alt}</span>${buy}</li>`;
  }).join('');
  const store = OFFER.shopStore ? ` · <a href="${esc(OFFER.shopStore)}" target="_blank" rel="noopener">details in the store</a>` : '';
  d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name)} shop</p>
    <h2>Looks, packs and more</h2><ul class="sh-list">${rows}</ul>
    <p class="fp-small">One-time prices unless marked /month or /year. Secure checkout by Stripe (shown as Link) · turns on as soon as you pay · your receipt code works on any device · 14-day fix-or-refund if it doesn't work as described (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). Prices in USD, + tax where applicable. Looks never change scores or the daily board; the daily challenge stays free for everyone${store}.</p>
    <p class="fp-small"><button class="sh-code" type="button" data-code>Already bought? Turn on your code</button></p></div>`;
  d.querySelectorAll('a[data-offer]').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', `shop-buy-${a.dataset.offer.replace(/^st-/, '')}`.slice(0, 40))));
  d.querySelector('[data-code]').addEventListener('click', () => { d.close(); open('shop-code'); });
}
/** Open the Shop sheet; `from` names the button that opened it (counted, and in the buy tag). */
export function openShop(from = 'button') {
  if (noShop()) { if (portal()) open(from); return; } // a portal build keeps its rewarded-ad sheet
  renderShop(from);
  const d = shopDialog();
  if (d.showModal) { if (!d.open) d.showModal(); } else d.setAttribute('open', '');
  count(cfg.game, 'cta', `shop-open-${slug(from)}`);
}
if (typeof document !== 'undefined' && !window.__cmxShop) {
  window.__cmxShop = 1;
  // capture phase on the document: runs before the game's own handlers on that button, and stops them
  document.addEventListener('click', (e) => {
    const b = e.target && e.target.closest && e.target.closest('[data-cmx-shop]');
    if (!b || noShop()) return; // portal / Telegram: the game's own handler (if any) runs as before
    e.preventDefault(); e.stopPropagation();
    openShop(b.dataset.cmxShop || 'menu');
  }, true);
  for (const ev of ['pointerdown', 'mousedown', 'touchstart']) { // tap-to-retry overlays never see a Shop tap
    document.addEventListener(ev, (e) => { if (!noShop() && e.target && e.target.closest && e.target.closest('[data-cmx-shop]')) e.stopPropagation(); }, true);
  }
  paintShop();
}

/** Call once per page: game id, display name and the game's own perks (listed first). Reads #cmx-pro=<code> after
 * checkout (?unlock=full marks this game's full-game return). Skin-pack codes are taken first by skins.js capture(). */
export function init(options) {
  cfg = { ...cfg, ...options };
  const m = /[#&]cmx-key=([^&]+)/.exec(location.hash || '') || /[#&]cmx-pro=([^&]+)/.exec(location.hash || '');
  if (m && SID.test(decodeURIComponent(m[1]))) {
    const q = new URLSearchParams(location.search);
    const fullReturn = q.get('unlock') === 'full';
    q.delete('unlock');
    history.replaceState(null, '', location.pathname + (q.toString() ? `?${q}` : ''));
    if (fullReturn) unlockGame(decodeURIComponent(m[1])); else unlock(decodeURIComponent(m[1]));
  } else recheck();
  emit();
}

// Direct checkout (17 Oct 2026, Workers cap fix): a click on a data.cybermaxtools.com/buy/<offer> link goes straight to the
// Stripe/Polar checkout (static map https://cybermaxtools.com/store/buy-direct.json); the click is counted by a beacon.
(() => {
  if (typeof document === 'undefined' || window.__cmxBd) return;
  window.__cmxBd = 1;
  let MAP = null;
  try { MAP = JSON.parse(sessionStorage.getItem('cmx-bd') || 'null'); } catch { /* none */ }
  if (!MAP) fetch('https://cybermaxtools.com/store/buy-direct.json').then((r) => r.json()).then((j) => { MAP = j; try { sessionStorage.setItem('cmx-bd', JSON.stringify(j)); } catch { /* full */ } }).catch(() => {});
  document.addEventListener('click', (e) => {
    const a = e.target && e.target.closest && e.target.closest('a[href*="data.cybermaxtools.com/buy/"]');
    if (!a || !MAP || !MAP.offers) return;
    try {
      const u = new URL(a.href);
      const k = /^\/buy\/([a-z0-9-]+)\/?$/.exec(u.pathname);
      const t = k && MAP.offers[k[1]];
      if (!t) return;
      const s = String(u.searchParams.get('s') || 'unknown').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 80) || 'unknown';
      a.href = t.split(MAP.ph).join(s);
      if (!navigator.webdriver) navigator.sendBeacon(u.href + (u.search ? '&' : '?') + 'b=1');
    } catch { /* keep the counted link */ }
  }, true);
})();