CyberMax-tools commited on
Commit
8a273a6
·
verified ·
1 Parent(s): 2882ebf

Deploy hushfox

Browse files
Files changed (10) hide show
  1. cmx.js +69 -1
  2. fun.css +45 -12
  3. funpass.js +188 -14
  4. game.js +3 -2
  5. index.html +9 -7
  6. looks.js +35 -0
  7. offer.js +1 -1
  8. skins.js +208 -0
  9. style.css +1 -0
  10. sw.js +2 -2
cmx.js CHANGED
@@ -172,10 +172,78 @@ export function themeToggle(btn) {
172
  if (!btn) return;
173
  btn.addEventListener('click', () => {
174
  const r = document.documentElement;
175
- const c = r.dataset.theme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
176
  const n = c === 'dark' ? 'light' : 'dark';
177
  r.dataset.theme = n;
178
  store('cm-theme-v', n);
179
  try { localStorage.setItem('cm-theme', n); } catch { /* private mode */ }
180
  });
181
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  if (!btn) return;
173
  btn.addEventListener('click', () => {
174
  const r = document.documentElement;
175
+ const c = r.dataset.theme || 'dark'; // games default to the dark arcade look (fun.css)
176
  const n = c === 'dark' ? 'light' : 'dark';
177
  r.dataset.theme = n;
178
  store('cm-theme-v', n);
179
  try { localStorage.setItem('cm-theme', n); } catch { /* private mode */ }
180
  });
181
  }
182
+
183
+ // ------------------------------------------------------------------ first-touch attribution on buy links
184
+ // First-sale review (29 Oct 2026, docs/ceo/first-sale-2026-10-27.md): the first paid order could be traced to the page
185
+ // that sold it but not to where the buyer came from. So the first time this browser opens a CyberMax page, one short tag
186
+ // is kept in localStorage (no cookie, no id, never sent on its own): where it came from (utm_source + utm_campaign, s=,
187
+ // the referrer's host or "direct") and the landing page, e.g. "ft-youtube-brimbloom-at-games-brimbloom". A Buy click
188
+ // carries it: s=<source>--<tag> on data.cybermaxtools.com/buy links (the Worker copies s into client_reference_id
189
+ // "data_<s>"), client_reference_id=<id>--<tag> on buy.stripe.com links. No extra request, page view or Worker call.
190
+ // The same code is in shared/convert-kit/convert.js (site pages), so the tag is shared across cybermaxtools.com.
191
+ const FT_KEY = 'cmx-ft';
192
+ const FT_DAYS = 90;
193
+ const ftSlug = (x, n) => String(x || '').toLowerCase().replace(/^www\./, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+/, '').slice(0, n).replace(/-+$/, '');
194
+ // words collect.py reads as "our own test" (TEST_MARK) never appear in a buyer's tag
195
+ const ftClean = (t) => t.replace(/(^|-)(test|e2e|check|probe|audit|lane\d*|internal)(?=-|$)/g, '$1x');
196
+ export function ftCompute(loc, referrer) {
197
+ const q = new URLSearchParams(loc.search || '');
198
+ const us = q.get('utm_source'), uc = q.get('utm_campaign'), s = q.get('s');
199
+ let ch;
200
+ if (us) ch = ftSlug(us, 14) + (uc ? `-${ftSlug(String(uc).replace(/^\d{4}-\d{2}-\d{2}-(\d+-)?/, ''), 18)}` : ''); // post ids start with a date
201
+ else if (s) ch = `s-${ftSlug(s, 24)}`;
202
+ else {
203
+ let host = '';
204
+ try { host = referrer ? new URL(referrer).hostname.toLowerCase() : ''; } catch { host = 'other'; }
205
+ ch = !host ? 'direct' : /(^|\.)cybermaxtools\.com$|cybermax-tools/.test(host) ? `site-${ftSlug(host.split('.')[0], 12)}` : `r-${ftSlug(host, 24)}`;
206
+ }
207
+ const segs = String(loc.pathname || '/').toLowerCase().split('/').filter((x) => x && !/^index\.html?$/.test(x));
208
+ const land = ftSlug(segs.slice(-2).join('-'), 18) || 'home';
209
+ return ftClean(`ft-${ch || 'direct'}-at-${land}`).slice(0, 48).replace(/-+$/, '');
210
+ }
211
+ /** The first-touch tag of this browser ('' when storage is blocked). Recorded once, kept FT_DAYS days. */
212
+ export function firstTouch() {
213
+ try {
214
+ const old = JSON.parse(localStorage.getItem(FT_KEY) || 'null');
215
+ if (old && old.t && Date.now() - old.at < FT_DAYS * 864e5) return old.t;
216
+ const t = ftCompute(location, document.referrer);
217
+ localStorage.setItem(FT_KEY, JSON.stringify({ t, at: Date.now() }));
218
+ return t;
219
+ } catch { return ''; }
220
+ }
221
+ /** href with the first-touch tag added (data /buy links: s; Stripe links: client_reference_id). Pure, for tests too. */
222
+ export function ftTagUrl(href, t) {
223
+ if (!t) return href;
224
+ try {
225
+ const u = new URL(href);
226
+ if (u.hostname === 'data.cybermaxtools.com' && u.pathname.startsWith('/buy/')) {
227
+ const s = (u.searchParams.get('s') || 'unknown').toLowerCase().replace(/[^a-z0-9-]/g, '');
228
+ if (s.includes('--ft-')) return href;
229
+ u.searchParams.set('s', `${s}--${t}`.slice(0, 80).replace(/-+$/, '')); // the Worker keeps s to 80 chars of [a-z0-9-]
230
+ return u.toString();
231
+ }
232
+ if (u.hostname === 'buy.stripe.com') {
233
+ const c = u.searchParams.get('client_reference_id') || 'data_unknown';
234
+ if (c.includes('--ft-')) return href;
235
+ u.searchParams.set('client_reference_id', `${c}--${t}`.slice(0, 200)); // Stripe: 200 chars of letters, digits, - and _
236
+ return u.toString();
237
+ }
238
+ } catch { /* not a URL */ }
239
+ return href;
240
+ }
241
+ if (typeof document !== 'undefined' && typeof location !== 'undefined' && !window.__cmxFt) {
242
+ window.__cmxFt = 1;
243
+ firstTouch();
244
+ // capture phase, registered before funpass.js/skins.js add their direct-checkout handler (they import this module)
245
+ document.addEventListener('click', (e) => {
246
+ const a = e.target && e.target.closest && e.target.closest('a[href*="data.cybermaxtools.com/buy/"],a[href*="buy.stripe.com/"]');
247
+ if (a) { const t = firstTouch(); if (t) a.href = ftTagUrl(a.href, t); }
248
+ }, true);
249
+ }
fun.css CHANGED
@@ -1,9 +1,10 @@
1
- /* CyberMax Fun kit: shared look for CyberMax games (copied into each game). Mobile first, light + dark. (c) 2026 CyberMax. */
2
- :root{--bg:#F6F3EC;--card:#fff;--ink:#11141B;--muted:#565C6B;--line:#E2DCD0;--chip:#ECE6DA;--acc:#FF6A3D;--acc-ink:#11141B;--ok:#0B7F5E;--bad:#C2361B;--shadow:0 1px 2px rgba(17,20,27,.06),0 10px 30px rgba(17,20,27,.08);color-scheme:light}
3
- @media (prefers-color-scheme:dark){:root:not([data-theme=light]){--bg:#0F1218;--card:#181C25;--ink:#EDEAE3;--muted:#A6ACB9;--line:#2A303C;--chip:#222734;--ok:#2FD3A0;--bad:#FF7A5C;--shadow:0 1px 2px rgba(0,0,0,.3),0 10px 30px rgba(0,0,0,.3);color-scheme:dark}}
4
- :root[data-theme=dark]{--bg:#0F1218;--card:#181C25;--ink:#EDEAE3;--muted:#A6ACB9;--line:#2A303C;--chip:#222734;--ok:#2FD3A0;--bad:#FF7A5C;--shadow:0 1px 2px rgba(0,0,0,.3),0 10px 30px rgba(0,0,0,.3);color-scheme:dark}
5
  *{box-sizing:border-box}[hidden]{display:none!important}html{-webkit-text-size-adjust:100%}
6
- body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;overflow-x:hidden}
 
7
  a{color:inherit}img,canvas{max-width:100%}
8
  .wrap{max-width:760px;margin:0 auto;padding:0 16px}
9
  .top{display:flex;align-items:center;gap:6px;padding:12px 0}
@@ -11,19 +12,19 @@ a{color:inherit}img,canvas{max-width:100%}
11
  .top nav a,.top .icon{color:var(--muted);text-decoration:none;padding:8px 10px;border-radius:10px;font-size:15px;background:none;border:0;font-family:inherit;cursor:pointer;min-height:40px}
12
  .top nav a:hover,.top .icon:hover{color:var(--ink);background:var(--chip)}
13
  .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:48px;padding:10px 18px;border-radius:12px;border:0;background:var(--acc);color:var(--acc-ink);font:inherit;font-weight:800;font-size:16px;cursor:pointer;text-decoration:none;touch-action:manipulation}
14
- .btn:hover{filter:brightness(1.06)}.btn.ghost{background:transparent;color:var(--ink);border:1.5px solid var(--line)}.btn.sm{min-height:40px;padding:6px 14px;font-size:15px}
15
  .btn.pass{background:linear-gradient(135deg,#FFB547,#FF6A3D);color:#11141B}
16
  .row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
17
  .chips{display:flex;gap:6px;flex-wrap:wrap}
18
- .chip{border:1.5px solid var(--line);background:var(--card);color:var(--ink);border-radius:999px;padding:6px 12px;font:inherit;font-size:14px;font-weight:700;cursor:pointer;min-height:36px}
19
  .chip[aria-pressed=true]{background:var(--ink);color:var(--bg);border-color:var(--ink)}.chip .lock{opacity:.7;font-size:12px}
20
- .panel{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:16px;box-shadow:var(--shadow)}
21
  .muted{color:var(--muted)}.small{font-size:14px}
22
- h1{font-size:clamp(26px,6vw,40px);line-height:1.08;letter-spacing:-.02em;margin:0 0 6px;text-wrap:balance}
23
  h2{font-size:21px;letter-spacing:-.01em;margin:30px 0 8px}
24
  .seo h3{font-size:17px;margin:18px 0 4px}.seo p,.seo li{color:var(--muted)}
25
  .more{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin:12px 0}
26
- .more a{display:block;padding:12px 14px;border:1px solid var(--line);border-radius:14px;background:var(--card);text-decoration:none}.more a b{display:block}.more a span{color:var(--muted);font-size:14px}
27
  footer{color:var(--muted);font-size:14px;padding:26px 0 40px;border-top:1px solid var(--line);margin-top:36px}
28
  #toast{position:fixed;left:50%;bottom:22px;transform:translate(-50%,40px);opacity:0;background:var(--ink);color:var(--bg);padding:12px 16px;border-radius:12px;font-weight:700;transition:.25s;z-index:99;max-width:calc(100% - 32px);text-align:center;pointer-events:none}
29
  #toast.on{transform:translate(-50%,0);opacity:1}
@@ -31,14 +32,14 @@ footer{color:var(--muted);font-size:14px;padding:26px 0 40px;border-top:1px soli
31
  html.cmx-pro .nudge{display:none}
32
  .pro-badge{display:none;font-size:12px;font-weight:800;background:linear-gradient(135deg,#FFB547,#FF6A3D);color:#11141B;border-radius:999px;padding:2px 8px;vertical-align:middle}
33
  html.cmx-pro .pro-badge{display:inline-block}
34
- dialog.funpass{border:0;padding:0;border-radius:20px;max-width:min(520px,calc(100% - 24px));width:100%;background:var(--card);color:var(--ink);box-shadow:0 20px 60px rgba(0,0,0,.35)}
35
  dialog.funpass::backdrop{background:rgba(10,12,18,.55)}
36
  .fp{padding:22px 20px 18px;position:relative}.fp h2{margin:2px 0 10px;font-size:24px}
37
  .fp-x{position:absolute;right:10px;top:8px;border:0;background:none;color:var(--muted);font-size:28px;cursor:pointer;width:44px;height:44px}
38
  .fp-kick{margin:0;color:var(--acc);font-weight:800;font-size:13px;letter-spacing:.06em;text-transform:uppercase}
39
  .fp-perks{margin:0 0 14px;padding-left:20px}.fp-perks li{margin:2px 0}
40
  .fp-plans{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
41
- .fp-plan{position:relative;display:flex;flex-direction:column;gap:2px;padding:14px 10px 12px;border:1.5px solid var(--line);border-radius:14px;text-decoration:none;text-align:center;background:var(--bg)}
42
  .fp-plan:hover{border-color:var(--acc)}.fp-plan.best{border-color:var(--acc);box-shadow:0 0 0 2px var(--acc) inset}
43
  .fp-plan i{position:absolute;top:-10px;left:50%;transform:translateX(-50%);background:var(--acc);color:#11141B;font-style:normal;font-size:11px;font-weight:800;padding:2px 8px;border-radius:999px;white-space:nowrap}
44
  .fp-plan b{font-size:14px}.fp-price{font-size:22px;font-weight:900}.fp-price small{font-size:12px;font-weight:600;color:var(--muted)}.fp-note{font-size:12px;color:var(--muted)}
@@ -52,3 +53,35 @@ dialog.funpass::backdrop{background:rgba(10,12,18,.55)}
52
  .fp-after{margin:14px 0 0;padding:12px 14px;border:1px solid var(--line, rgba(127,127,127,.35));border-radius:14px;text-align:left}.fp-after p{margin:0 0 8px}.fp-after .fp-row{display:flex;gap:8px;flex-wrap:wrap}
53
  .fp-game{border-color:var(--acc);display:grid;grid-template-columns:1fr auto;align-items:center;text-align:left;gap:2px 12px;padding:14px 14px 12px;margin:0 0 6px}.fp-game b{font-size:16px}.fp-game .fp-price{grid-row:span 2}.fp-game .fp-note{grid-column:1}
54
  .fp-or{margin:12px 0 8px;font-size:14px}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* CyberMax Fun kit: shared look for CyberMax games (copied into each game). Mobile first. 2026 look: dark arcade night by
2
+ default (glass panels, a glow in each game's own colours behind the stage), light on request. (c) 2026 CyberMax. */
3
+ :root{--bg:#07080F;--bg2:#0E1020;--card:rgba(22,25,42,.72);--card-solid:#151829;--ink:#F1EFF8;--muted:#A3A8BE;--line:rgba(255,255,255,.10);--chip:rgba(255,255,255,.06);--acc:#FF6A3D;--acc-ink:#11141B;--ok:#2FD3A0;--bad:#FF7A5C;--game:#5CF2FF;--game2:#FF4D9A;--shadow:0 1px 0 rgba(255,255,255,.04) inset,0 18px 50px rgba(0,0,0,.45);color-scheme:dark}
4
+ :root[data-theme=light]{--bg:#F6F3EC;--bg2:#EFEADF;--card:#fff;--card-solid:#fff;--ink:#11141B;--muted:#565C6B;--line:#E2DCD0;--chip:#ECE6DA;--ok:#0B7F5E;--bad:#C2361B;--shadow:0 1px 2px rgba(17,20,27,.06),0 10px 30px rgba(17,20,27,.08);color-scheme:light}
5
  *{box-sizing:border-box}[hidden]{display:none!important}html{-webkit-text-size-adjust:100%}
6
+ body{margin:0;background:radial-gradient(1100px 620px at 50% -12%,color-mix(in srgb,var(--game) 20%,transparent),transparent 70%),radial-gradient(800px 520px at 105% 38%,color-mix(in srgb,var(--game2) 13%,transparent),transparent 70%),radial-gradient(700px 500px at -8% 70%,color-mix(in srgb,var(--game) 9%,transparent),transparent 70%),linear-gradient(180deg,var(--bg),var(--bg2));background-attachment:fixed;color:var(--ink);font:16px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;overflow-x:hidden;min-height:100vh}
7
+ :root[data-theme=light] body{background:var(--bg)}
8
  a{color:inherit}img,canvas{max-width:100%}
9
  .wrap{max-width:760px;margin:0 auto;padding:0 16px}
10
  .top{display:flex;align-items:center;gap:6px;padding:12px 0}
 
12
  .top nav a,.top .icon{color:var(--muted);text-decoration:none;padding:8px 10px;border-radius:10px;font-size:15px;background:none;border:0;font-family:inherit;cursor:pointer;min-height:40px}
13
  .top nav a:hover,.top .icon:hover{color:var(--ink);background:var(--chip)}
14
  .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:48px;padding:10px 18px;border-radius:12px;border:0;background:var(--acc);color:var(--acc-ink);font:inherit;font-weight:800;font-size:16px;cursor:pointer;text-decoration:none;touch-action:manipulation}
15
+ .btn:hover{filter:brightness(1.08)}.btn:not(.ghost){box-shadow:0 6px 22px color-mix(in srgb,var(--acc) 35%,transparent)}.btn.ghost{background:transparent;color:var(--ink);border:1.5px solid var(--line)}.btn.sm{min-height:40px;padding:6px 14px;font-size:15px}
16
  .btn.pass{background:linear-gradient(135deg,#FFB547,#FF6A3D);color:#11141B}
17
  .row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
18
  .chips{display:flex;gap:6px;flex-wrap:wrap}
19
+ .chip{border:1.5px solid var(--line);background:var(--chip);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:var(--ink);border-radius:999px;padding:6px 12px;font:inherit;font-size:14px;font-weight:700;cursor:pointer;min-height:36px}
20
  .chip[aria-pressed=true]{background:var(--ink);color:var(--bg);border-color:var(--ink)}.chip .lock{opacity:.7;font-size:12px}
21
+ .panel{background:var(--card);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);border:1px solid var(--line);border-radius:20px;padding:16px;box-shadow:var(--shadow)}
22
  .muted{color:var(--muted)}.small{font-size:14px}
23
+ h1{font-size:clamp(28px,6.4vw,46px);font-weight:900;line-height:1.05;letter-spacing:-.025em;margin:0 0 6px;text-wrap:balance}
24
  h2{font-size:21px;letter-spacing:-.01em;margin:30px 0 8px}
25
  .seo h3{font-size:17px;margin:18px 0 4px}.seo p,.seo li{color:var(--muted)}
26
  .more{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin:12px 0}
27
+ .more a{display:block;padding:12px 14px;border:1px solid var(--line);border-radius:14px;background:var(--card);text-decoration:none;transition:transform .15s,border-color .15s}.more a:hover{transform:translateY(-2px);border-color:color-mix(in srgb,var(--game) 50%,var(--line))}.more a b{display:block}.more a span{color:var(--muted);font-size:14px}
28
  footer{color:var(--muted);font-size:14px;padding:26px 0 40px;border-top:1px solid var(--line);margin-top:36px}
29
  #toast{position:fixed;left:50%;bottom:22px;transform:translate(-50%,40px);opacity:0;background:var(--ink);color:var(--bg);padding:12px 16px;border-radius:12px;font-weight:700;transition:.25s;z-index:99;max-width:calc(100% - 32px);text-align:center;pointer-events:none}
30
  #toast.on{transform:translate(-50%,0);opacity:1}
 
32
  html.cmx-pro .nudge{display:none}
33
  .pro-badge{display:none;font-size:12px;font-weight:800;background:linear-gradient(135deg,#FFB547,#FF6A3D);color:#11141B;border-radius:999px;padding:2px 8px;vertical-align:middle}
34
  html.cmx-pro .pro-badge{display:inline-block}
35
+ dialog.funpass{border:0;padding:0;border-radius:20px;max-width:min(520px,calc(100% - 24px));width:100%;background:var(--card-solid);color:var(--ink);box-shadow:0 20px 60px rgba(0,0,0,.35)}
36
  dialog.funpass::backdrop{background:rgba(10,12,18,.55)}
37
  .fp{padding:22px 20px 18px;position:relative}.fp h2{margin:2px 0 10px;font-size:24px}
38
  .fp-x{position:absolute;right:10px;top:8px;border:0;background:none;color:var(--muted);font-size:28px;cursor:pointer;width:44px;height:44px}
39
  .fp-kick{margin:0;color:var(--acc);font-weight:800;font-size:13px;letter-spacing:.06em;text-transform:uppercase}
40
  .fp-perks{margin:0 0 14px;padding-left:20px}.fp-perks li{margin:2px 0}
41
  .fp-plans{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
42
+ .fp-plan{position:relative;display:flex;flex-direction:column;gap:2px;padding:14px 10px 12px;border:1.5px solid var(--line);border-radius:14px;text-decoration:none;text-align:center;background:var(--bg2)}
43
  .fp-plan:hover{border-color:var(--acc)}.fp-plan.best{border-color:var(--acc);box-shadow:0 0 0 2px var(--acc) inset}
44
  .fp-plan i{position:absolute;top:-10px;left:50%;transform:translateX(-50%);background:var(--acc);color:#11141B;font-style:normal;font-size:11px;font-weight:800;padding:2px 8px;border-radius:999px;white-space:nowrap}
45
  .fp-plan b{font-size:14px}.fp-price{font-size:22px;font-weight:900}.fp-price small{font-size:12px;font-weight:600;color:var(--muted)}.fp-note{font-size:12px;color:var(--muted)}
 
53
  .fp-after{margin:14px 0 0;padding:12px 14px;border:1px solid var(--line, rgba(127,127,127,.35));border-radius:14px;text-align:left}.fp-after p{margin:0 0 8px}.fp-after .fp-row{display:flex;gap:8px;flex-wrap:wrap}
54
  .fp-game{border-color:var(--acc);display:grid;grid-template-columns:1fr auto;align-items:center;text-align:left;gap:2px 12px;padding:14px 14px 12px;margin:0 0 6px}.fp-game b{font-size:16px}.fp-game .fp-price{grid-row:span 2}.fp-game .fp-note{grid-column:1}
55
  .fp-or{margin:12px 0 8px;font-size:14px}
56
+
57
+ .top{position:relative;z-index:2}.top .brand{letter-spacing:-.01em}
58
+ /* 3D games on wide screens: a bigger stage, centred header (2026 upgrade) */
59
+ @media (min-width:1100px){.wrap:has(.cmx3d-wrap){max-width:1080px}.wrap:has(.cmx3d-wrap) .seo,.wrap:has(.cmx3d-wrap) footer{max-width:760px;margin-left:auto;margin-right:auto}
60
+ .wrap:has(.cmx3d-wrap) section.game>.headline{text-align:center}.wrap:has(.cmx3d-wrap) section.game>.headline .tag{margin-left:auto;margin-right:auto;max-width:640px}
61
+ .wrap:has(.cmx3d-wrap) section.game>.chips{justify-content:center}.wrap:has(.cmx3d-wrap) section.game>.lv-goal,.wrap:has(.cmx3d-wrap) section.game>#goal{text-align:center;justify-content:center}
62
+ .wrap:has(.cmx3d-wrap) section.game>.panel{max-width:760px;margin-left:auto;margin-right:auto}}
63
+ /* living background: two slow glows in the game's colours drift behind the page (dark theme; off for reduced motion) */
64
+ body::before,body::after{content:"";position:fixed;z-index:-1;width:60vmax;height:60vmax;border-radius:50%;filter:blur(70px);opacity:.16;pointer-events:none;animation:cmx-drift 26s ease-in-out infinite alternate}
65
+ body::before{left:-20vmax;top:-25vmax;background:var(--game)}body::after{right:-25vmax;bottom:-30vmax;background:var(--game2);animation-duration:34s;animation-direction:alternate-reverse}
66
+ :root[data-theme=light] body::before,:root[data-theme=light] body::after{display:none}
67
+ @keyframes cmx-drift{from{transform:translate(0,0) scale(1)}to{transform:translate(12vmax,8vmax) scale(1.15)}}
68
+ .panel{transition:border-color .2s,box-shadow .2s}.panel:hover{border-color:color-mix(in srgb,var(--game) 28%,var(--line))}
69
+ .fp-rbx{font-size:13px;color:var(--muted);margin:10px 0 0;text-align:center}.fp-rbx a{color:inherit;font-weight:700}
70
+ /* Shop (29 Oct 2026): one Shop button on the title/menu screen, one quiet one on results, one sheet with every item */
71
+ .cmx-shopbtn{background:linear-gradient(135deg,#FFD15C,#FF8A3D)!important;color:#11141B!important;font-weight:900;letter-spacing:.01em;gap:8px;border:0}
72
+ .cmx-shopbtn small{font-size:.78em;font-weight:700;opacity:.8;white-space:nowrap}.cmx-shopres small{font-size:.85em;opacity:.8}
73
+ html.cmx-noshop [data-cmx-shop]:not([data-cmx-keep]){display:none!important}
74
+ .cmxshop .sh-list{list-style:none;margin:10px 0 6px;padding:0;display:grid;gap:8px}
75
+ .cmxshop .sh-i{display:grid;grid-template-columns:56px 1fr auto;gap:12px;align-items:center;padding:10px;border:1.5px solid var(--line);border-radius:14px;background:var(--bg2)}
76
+ .cmxshop .sh-own{font-weight:800;color:var(--ok);font-size:14px;white-space:nowrap}
77
+ .cmxshop .sh-pv{position:relative;width:56px;height:56px;border-radius:12px;overflow:hidden;display:grid;place-items:center;font-size:26px}
78
+ .cmxshop .sh-pv i{font-style:normal}.cmxshop .sh-pv img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
79
+ .cmxshop .sh-t{min-width:0;display:flex;flex-direction:column;gap:2px}.cmxshop .sh-t b{font-size:16px}
80
+ .cmxshop .sh-w{font-size:13px;color:var(--muted);line-height:1.35}.cmxshop .sh-w em{font-style:normal;font-weight:700;color:var(--ink)}
81
+ .cmxshop .sh-alt{font-size:13px;color:var(--muted)}.cmxshop .sh-alt a{color:var(--ink);font-weight:700}
82
+ .cmxshop .sh-buy{min-width:84px;white-space:nowrap}.cmxshop .sh-buy small{font-size:11px;font-weight:600}
83
+ .cmxshop .sh-code{border:0;background:none;color:var(--ink);font:inherit;font-weight:700;text-decoration:underline;cursor:pointer;padding:0}
84
+ @media (max-width:460px){.cmxshop .sh-i{grid-template-columns:44px 1fr;gap:8px 10px}.cmxshop .sh-pv{width:44px;height:44px;font-size:20px}.cmxshop .sh-buy,.cmxshop .sh-own{grid-column:2;justify-self:start}}
85
+ dialog.cmxshop{max-height:calc(100dvh - 16px);overflow:auto}
86
+ .cmx-shop-top{min-height:38px;padding:5px 12px;font-size:14px;border-radius:999px;margin:0 4px;white-space:nowrap;flex:0 0 auto;gap:6px}
87
+ @media (max-width:430px){header.top:has(.cmx-shop-top:not([hidden])) nav{display:none}header.top:has(.cmx-shop-top:not([hidden])) .brand{font-size:0;gap:0}.cmx-shop-top{padding:5px 10px;font-size:13px;margin:0 2px}.cmx-shop-top small{font-size:11px}}
funpass.js CHANGED
@@ -17,7 +17,7 @@ export { OFFER };
17
 
18
  const DATA = 'https://data.cybermaxtools.com';
19
  const KEY = 'cmx-pro:fun-pass';
20
- const SID = /^(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
21
  const RECHECK = 3 * 864e5; // subscriptions are re-checked every 3 days; a cancelled pass turns off by itself
22
  export const PLANS = [
23
  { id: 'monthly', label: 'Monthly', price: '$2.99', per: '/month', note: 'Cancel any time' },
@@ -33,6 +33,10 @@ let state = store(KEY);
33
  let full = store(FULL_KEY);
34
  const listeners = [];
35
  let cfg = { game: 'games', name: 'CyberMax games', perks: [] };
 
 
 
 
36
 
37
  const passOn = () => !!(state && state.code);
38
  export const ownsGame = () => !!(full && full.code);
@@ -43,12 +47,42 @@ export const isPro = () => passOn() || ownsGame() || rewardOn();
43
  export const freeLevels = (current = 20) => (OFFER.live && OFFER.demo ? Math.min(current, OFFER.demo) : current);
44
  const portal = () => (typeof window !== 'undefined' && window.CMXPortal && window.CMXPortal.portal) || '';
45
  export function onChange(fn) { listeners.push(fn); fn(isPro()); }
 
 
 
 
 
 
 
 
46
  function emit() {
47
  document.documentElement.classList.toggle('cmx-pro', isPro());
 
 
48
  listeners.forEach((fn) => { try { fn(isPro()); } catch { /* game hook */ } });
49
  }
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  async function check(code, what = 'fun-pass') {
 
 
 
 
52
  try {
53
  const r = await fetch(`${DATA}/unlock/${what}?json=1&session_id=${encodeURIComponent(code)}`);
54
  const j = await r.json();
@@ -62,7 +96,7 @@ export async function unlockGame(code) {
62
  if (ok) {
63
  full = store(FULL_KEY, { code, at: new Date().toISOString() });
64
  count(cfg.game, 'pro', 'full-unlocked');
65
- toast(`${OFFER.name}: the full game is on. Thank you for supporting CyberMax games!`, 3500);
66
  emit(); render();
67
  } else if (ok === false) toast('That code is not a full-game order for this game.', 3500);
68
  else toast('Could not reach the unlock check. Try again in a minute.', 3500);
@@ -87,7 +121,7 @@ export async function unlock(code) {
87
  }
88
 
89
  async function recheck() {
90
- if (!isPro() || Date.now() - (state.checked || 0) < RECHECK) return;
91
  const ok = await check(state.code);
92
  if (ok === false) { state = null; try { localStorage.removeItem(KEY); } catch { /* */ } emit(); render(); toast('Your Fun Pass has ended. Everything free is still here.', 4000); }
93
  else if (ok) state = store(KEY, { ...state, checked: Date.now() });
@@ -117,7 +151,7 @@ function render() {
117
  const d = dialog();
118
  const perks = [...(cfg.perks || []), ...COMMON_PERKS.filter((p) => !(cfg.perks || []).some((q) => q.split(' ')[0] === p.split(' ')[0]))].slice(0, 6);
119
  if (isPro()) {
120
- const what = passOn() ? 'The Fun Pass is on in this browser for every CyberMax game' : ownsGame() ? `You own the full ${esc(OFFER.name)}` : 'The full game is open for the next hour (rewarded ad)';
121
  const code = passOn() ? state.code : ownsGame() ? full.code : '';
122
  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>
123
  <p>${what}: ${perks.map(esc).join(' · ')}.</p>
@@ -140,21 +174,22 @@ function render() {
140
  }
141
  const allGames = `${OFFER.games || 'every'} CyberMax games`;
142
  const own = cfg.perks || [];
143
- const gamePerks = [...own, ...(own.some((p) => /archive/i.test(p)) ? [] : ['The full archive of past dailies']), 'Every bonus mode in this game'].slice(0, 6);
 
144
  d.innerHTML = sellsGame()
145
- ? `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name)} · full game</p>
146
- <h2>Unlock the full ${esc(OFFER.name)}</h2>
147
  <ul class="fp-perks">${gamePerks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
148
- <a class="fp-plan fp-game" href="${gameUrl()}" rel="nofollow" data-game><i>This game</i><b>Full ${esc(OFFER.name)}</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>
149
- <p class="fp-or"><b>Or get all ${esc(allGames)}</b> with the Fun Pass:</p>
150
  <div class="fp-plans">${planHtml()}</div>
151
- <p class="fp-small">Secure Stripe checkout · turns on as soon as you pay · your code works on any device · 14-day promise: if it doesn't work as described, we fix it or refund you. The daily challenge and the first ${OFFER.demo || ''} levels stay free for everyone.</p>
152
  ${codeForm}</div>`
153
  : `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">CyberMax Fun Pass</p>
154
  <h2>One pass. Every game. More to play.</h2>
155
  <ul class="fp-perks">${perks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
156
  <div class="fp-plans">${planHtml()}</div>
157
- <p class="fp-small">Secure Stripe checkout. Works in every CyberMax game. The daily challenges and the free levels stay free for everyone.</p>
158
  ${codeForm}</div>`;
159
  d.querySelector('.fp-form').addEventListener('submit', (e) => {
160
  e.preventDefault();
@@ -176,20 +211,34 @@ export function open(from = 'button') {
176
  /** After a finished round (the value moment): one small inline card under the result, never a pop-up. Shows the
177
  * pass price, the honest trust line and an optional "email me" weekly reminder (double opt-in).
178
  * `host` is the result panel; the card is added once and removed for pass holders. */
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  export function afterWin(host, from = 'win') {
180
  if (!host) return;
 
181
  if (isPro() || portal()) { host.querySelector('.fp-after')?.remove(); return; }
182
  let el = host.querySelector('.fp-after');
183
  if (!el) { el = document.createElement('div'); el.className = 'fp-after'; host.appendChild(el); }
184
  const best = PLANS.find((p) => p.best) || PLANS[0];
185
  const game = sellsGame();
186
  const href = game ? gameUrl('after') : `${buyUrl(best.id)}-after`;
187
- const label = game ? `Get the full ${OFFER.name}: ${OFFER.price} once` : `Get the Fun Pass: ${best.price}${best.per}`;
188
  const line = game ? (cfg.extra || `The full game opens every level, mode, the daily archive and themes in ${OFFER.name}.`)
189
  : (cfg.extra || 'The Fun Pass opens every level, mode, the full archive and themes in every CyberMax game.');
190
  el.innerHTML = `<p><b>Want more today?</b> ${esc(line)}</p>
191
  <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>
192
- <p class="fp-small">Secure Stripe checkout · turns on as soon as you pay · ${game ? 'one payment, no subscription' : 'cancel any time from your receipt email'} · 14-day promise: if it doesn't work as described, we fix it or refund you (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). Today's daily challenge stays free.</p>
193
  <div class="fp-cap"></div>`;
194
  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)); });
195
  el.querySelector('[data-plans]').addEventListener('click', () => open(`after-${from}`));
@@ -211,11 +260,113 @@ function kit(fn) {
211
  document.head.appendChild(sc);
212
  }
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  /** Call once per page: game id, display name and the game's own perks (listed first). Reads #cmx-pro=<code> after
215
  * checkout (?unlock=full marks this game's full-game return). Skin-pack codes are taken first by skins.js capture(). */
216
  export function init(options) {
217
  cfg = { ...cfg, ...options };
218
- const m = /[#&]cmx-pro=([^&]+)/.exec(location.hash || '');
219
  if (m && SID.test(decodeURIComponent(m[1]))) {
220
  const q = new URLSearchParams(location.search);
221
  const fullReturn = q.get('unlock') === 'full';
@@ -225,3 +376,26 @@ export function init(options) {
225
  } else recheck();
226
  emit();
227
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  const DATA = 'https://data.cybermaxtools.com';
19
  const KEY = 'cmx-pro:fun-pass';
20
+ 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
21
  const RECHECK = 3 * 864e5; // subscriptions are re-checked every 3 days; a cancelled pass turns off by itself
22
  export const PLANS = [
23
  { id: 'monthly', label: 'Monthly', price: '$2.99', per: '/month', note: 'Cancel any time' },
 
33
  let full = store(FULL_KEY);
34
  const listeners = [];
35
  let cfg = { game: 'games', name: 'CyberMax games', perks: [] };
36
+ // Per-game offer naming (17 Oct 2026 conversion fix): games whose one-time unlock is not a "full game" (no levels, every
37
+ // mode free) pass init({ title: 'Stoopshake Crew Pass', exact: true, free: '...' }): the sheet then names the offer exactly
38
+ // as the Stripe product, lists only its own perks and states what stays free, instead of "every level / bonus mode".
39
+ const fullTitle = () => cfg.title || `Full ${OFFER.name}`;
40
 
41
  const passOn = () => !!(state && state.code);
42
  export const ownsGame = () => !!(full && full.code);
 
47
  export const freeLevels = (current = 20) => (OFFER.live && OFFER.demo ? Math.min(current, OFFER.demo) : current);
48
  const portal = () => (typeof window !== 'undefined' && window.CMXPortal && window.CMXPortal.portal) || '';
49
  export function onChange(fn) { listeners.push(fn); fn(isPro()); }
50
+ // The header pass button names the real price (convert lane, 15 Oct 2026): "Full game $2.99" where this game sells its
51
+ // own unlock, else the Fun Pass monthly price. Pass holders see the button's own label.
52
+ function priceButton() {
53
+ const btn = typeof document !== 'undefined' && document.getElementById('pass-btn');
54
+ if (!btn) return;
55
+ if (!btn.dataset.label) btn.dataset.label = btn.textContent;
56
+ btn.textContent = isPro() || portal() ? btn.dataset.label : sellsGame() ? `✨ ${cfg.title ? cfg.short || cfg.title : 'Full game'} ${OFFER.price}` : `✨ Fun Pass ${PLANS[0].price}/mo`;
57
+ }
58
  function emit() {
59
  document.documentElement.classList.toggle('cmx-pro', isPro());
60
+ priceButton();
61
+ paintShop();
62
  listeners.forEach((fn) => { try { fn(isPro()); } catch { /* game hook */ } });
63
  }
64
 
65
+ // Offline Pro keys (17 Oct 2026): "cmxk1.<payload>.<Ed25519 signature>" from data.cybermaxtools.com/unlock is verified here
66
+ // with WebCrypto and this public key, so an unlock needs no Worker call. Expired, foreign-signed or unverifiable keys fall
67
+ // back to the online check of the order code inside them, as before.
68
+ const CMX_PUB = 'u0FDxbu0d9B9Sz5-yLT6AuL5IagyTlFcbBgEDqZxWtk';
69
+ const b64d = (s) => { s = s.replace(/-/g, '+').replace(/_/g, '/'); while (s.length % 4) s += '='; return Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); };
70
+ async function cmxKeyOk(k, app, online) {
71
+ const [tag, body, sig] = k.split('.');
72
+ let p;
73
+ try { p = JSON.parse(new TextDecoder().decode(b64d(body))); } catch { return false; }
74
+ if (!p || p.a !== app) return false;
75
+ try {
76
+ const key = await crypto.subtle.importKey('raw', b64d(CMX_PUB), { name: 'Ed25519' }, false, ['verify']);
77
+ if (await crypto.subtle.verify({ name: 'Ed25519' }, key, b64d(sig), new TextEncoder().encode(tag + '.' + body)) && (!p.x || p.x * 1000 > Date.now())) return true;
78
+ } catch { /* no Ed25519 here: online check below */ }
79
+ return p.c ? online(p.c) : false;
80
+ }
81
  async function check(code, what = 'fun-pass') {
82
+ if (/^cmxk1\./.test(code)) return cmxKeyOk(code, what, (c) => checkOnline(c, what));
83
+ return checkOnline(code, what);
84
+ }
85
+ async function checkOnline(code, what = 'fun-pass') {
86
  try {
87
  const r = await fetch(`${DATA}/unlock/${what}?json=1&session_id=${encodeURIComponent(code)}`);
88
  const j = await r.json();
 
96
  if (ok) {
97
  full = store(FULL_KEY, { code, at: new Date().toISOString() });
98
  count(cfg.game, 'pro', 'full-unlocked');
99
+ 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);
100
  emit(); render();
101
  } else if (ok === false) toast('That code is not a full-game order for this game.', 3500);
102
  else toast('Could not reach the unlock check. Try again in a minute.', 3500);
 
121
  }
122
 
123
  async function recheck() {
124
+ 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)
125
  const ok = await check(state.code);
126
  if (ok === false) { state = null; try { localStorage.removeItem(KEY); } catch { /* */ } emit(); render(); toast('Your Fun Pass has ended. Everything free is still here.', 4000); }
127
  else if (ok) state = store(KEY, { ...state, checked: Date.now() });
 
151
  const d = dialog();
152
  const perks = [...(cfg.perks || []), ...COMMON_PERKS.filter((p) => !(cfg.perks || []).some((q) => q.split(' ')[0] === p.split(' ')[0]))].slice(0, 6);
153
  if (isPro()) {
154
+ 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)';
155
  const code = passOn() ? state.code : ownsGame() ? full.code : '';
156
  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>
157
  <p>${what}: ${perks.map(esc).join(' · ')}.</p>
 
174
  }
175
  const allGames = `${OFFER.games || 'every'} CyberMax games`;
176
  const own = cfg.perks || [];
177
+ 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);
178
+ const freeLine = cfg.free || `The daily challenge and ${OFFER.levels === false ? 'the free modes stay' : `the first ${OFFER.demo || ''} levels stay`} free for everyone.`;
179
  d.innerHTML = sellsGame()
180
+ ? `<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>
181
+ <h2>${cfg.title ? `Get the ${esc(cfg.title)}` : `Unlock the full ${esc(OFFER.name)}`}</h2>
182
  <ul class="fp-perks">${gamePerks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
183
+ <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>
184
+ <p class="fp-or"><b>Or get all ${esc(allGames)}</b> with the Fun Pass (a separate pass for every game):</p>
185
  <div class="fp-plans">${planHtml()}</div>
186
+ <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>
187
  ${codeForm}</div>`
188
  : `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">CyberMax Fun Pass</p>
189
  <h2>One pass. Every game. More to play.</h2>
190
  <ul class="fp-perks">${perks.map((p) => `<li>${esc(p)}</li>`).join('')}</ul>
191
  <div class="fp-plans">${planHtml()}</div>
192
+ <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>
193
  ${codeForm}</div>`;
194
  d.querySelector('.fp-form').addEventListener('submit', (e) => {
195
  e.preventDefault();
 
211
  /** After a finished round (the value moment): one small inline card under the result, never a pop-up. Shows the
212
  * pass price, the honest trust line and an optional "email me" weekly reminder (double opt-in).
213
  * `host` is the result panel; the card is added once and removed for pass holders. */
214
+ // One quiet line under the result: CyberMax's free Roblox games (24 Oct 2026). Not on game portals (their rules ban
215
+ // outside links), never a pop-up; links carry the game id so clicks can be told apart.
216
+ const ROBLOX = [['Skyline Flight Circuit', 'https://www.roblox.com/games/89786604994377'], ['Hatchlume', 'https://www.roblox.com/games/80122923062327'],
217
+ ['Sled Volt', 'https://www.roblox.com/games/134881648422275']];
218
+ function robloxRow(host) {
219
+ if (portal() || host.querySelector('.fp-rbx')) return;
220
+ const p = document.createElement('p'); p.className = 'fp-rbx';
221
+ const tag = `?utm_source=cybermax&utm_medium=game&utm_campaign=${encodeURIComponent(cfg.game || 'games')}-end`;
222
+ p.innerHTML = 'Play our Roblox games: ' + ROBLOX.map(([n, u]) => `<a href="${u}${tag}" target="_blank" rel="noopener">${esc(n)}</a>`).join(' · ');
223
+ p.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', 'roblox-end')));
224
+ host.appendChild(p);
225
+ }
226
+
227
  export function afterWin(host, from = 'win') {
228
  if (!host) return;
229
+ robloxRow(host);
230
  if (isPro() || portal()) { host.querySelector('.fp-after')?.remove(); return; }
231
  let el = host.querySelector('.fp-after');
232
  if (!el) { el = document.createElement('div'); el.className = 'fp-after'; host.appendChild(el); }
233
  const best = PLANS.find((p) => p.best) || PLANS[0];
234
  const game = sellsGame();
235
  const href = game ? gameUrl('after') : `${buyUrl(best.id)}-after`;
236
+ 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}`;
237
  const line = game ? (cfg.extra || `The full game opens every level, mode, the daily archive and themes in ${OFFER.name}.`)
238
  : (cfg.extra || 'The Fun Pass opens every level, mode, the full archive and themes in every CyberMax game.');
239
  el.innerHTML = `<p><b>Want more today?</b> ${esc(line)}</p>
240
  <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>
241
+ <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>
242
  <div class="fp-cap"></div>`;
243
  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)); });
244
  el.querySelector('[data-plans]').addEventListener('click', () => open(`after-${from}`));
 
260
  document.head.appendChild(sc);
261
  }
262
 
263
+ // ------------------------------------------------------------------ the Shop (owner, 29 Oct 2026: "there's nothing to buy")
264
+ // One clear Shop button on the title/menu screen and one quiet button on the results screen open ONE sheet that lists
265
+ // everything this game sells, cheapest first (OFFER.shop, written by scripts/games/sync_kit.py from products/fun-pass/shop.json;
266
+ // prices come from the Stripe packs): coin packs, Skin/Look/Founder Packs, the full game, the Fun Pass and the Arcade
267
+ // Lifetime. Buttons are plain markup with data-cmx-shop="<where>" (menu, title, results); this file labels them, opens
268
+ // the sheet and hides them on portal and Telegram builds (no outside checkout there). Never shown during play: the
269
+ // buttons live on menu/results screens or above the game, so the play HUD is unchanged. Buy links carry
270
+ // s=game-<id>-shop-<where>; cmx.js adds the first-touch tag on click. Nothing pops up by itself.
271
+ const SHOP = Array.isArray(OFFER.shop) ? OFFER.shop : [];
272
+ const TG = typeof location !== 'undefined' && (/[?&]tg(=|&|$)/.test(location.search) || /tgWebAppData=/.test(location.hash));
273
+ /** True where no web checkout may show: portal builds and the Telegram Mini App (Stars only). */
274
+ export const noShop = () => !!portal() || TG || !SHOP.length;
275
+ const slug = (s) => String(s || 'shop').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 24) || 'shop';
276
+ const shopHref = (offer, from) => `${DATA}/buy/${offer}?s=game-${OFFER.game}-shop-${slug(from)}`;
277
+ const ownedKind = (it) => {
278
+ if (it.kind === 'pass') return passOn();
279
+ if (it.kind === 'full') return ownsGame();
280
+ if (it.kind === 'skins') return !!store(`cmx-skins:${OFFER.game}`) || !!store(`cmx-founder:${OFFER.game}`);
281
+ if (it.kind === 'founder') return !!store(`cmx-founder:${OFFER.game}`);
282
+ if (it.kind === 'collector') return !!store(`cmx-collector:${OFFER.game}`); // looks Collector Packs (skins.js v2)
283
+ return false; // coin packs, looks packs and the Arcade Lifetime can always be bought
284
+ };
285
+ /** The Shop items, cheapest first, with an owned flag. */
286
+ export const shopItems = () => SHOP.slice().sort((a, b) => a.cents - b.cents).map((it) => ({ ...it, owned: ownedKind(it) }));
287
+ /** Label for a Shop button: the menu/title one names the lowest price, the results one the most relevant offer. */
288
+ export function shopLabel(where = 'menu') {
289
+ if (where === 'results') return esc(OFFER.shopResults || '🛍️ See the shop →');
290
+ return `🛍️ Shop${OFFER.shopFrom ? ` <small>from ${esc(OFFER.shopFrom)}</small>` : ''}`;
291
+ }
292
+ /** Markup for a Shop button in screens a game draws itself ('' where no checkout may show). */
293
+ export function shopButtonHtml(where = 'menu', cls = '') {
294
+ if (noShop()) return '';
295
+ const c = where === 'results' ? 'btn ghost sm cmx-shopres' : 'btn cmx-shopbtn';
296
+ return `<button class="${c}${cls ? ` ${cls}` : ''}" type="button" data-cmx-shop="${esc(where)}">${shopLabel(where)}</button>`;
297
+ }
298
+ function paintShop() {
299
+ if (typeof document === 'undefined') return;
300
+ const off = noShop();
301
+ document.documentElement.classList.toggle('cmx-noshop', off);
302
+ for (const b of document.querySelectorAll('[data-cmx-shop]')) {
303
+ if (off) { if (!b.hasAttribute('data-cmx-keep')) b.hidden = true; continue; } // data-cmx-keep: the game's own button, its own handler
304
+ const where = b.dataset.cmxShop || 'menu';
305
+ const html = shopLabel(where === 'results' ? 'results' : 'menu');
306
+ if (b.dataset.cmxLabel !== html) { b.innerHTML = html; b.dataset.cmxLabel = html; }
307
+ if (b.hasAttribute('data-cmx-hidden')) continue; // the game shows/hides this one itself
308
+ b.hidden = false;
309
+ }
310
+ }
311
+ const swatch = (it) => `background:${it.sw && it.sw.length > 1 ? `linear-gradient(135deg,${it.sw.map(esc).join(',')})` : esc((it.sw || ['#FF6A3D'])[0])}`;
312
+ function shopDialog() {
313
+ let d = document.getElementById('cmxshop');
314
+ if (!d) {
315
+ d = document.createElement('dialog');
316
+ d.id = 'cmxshop';
317
+ d.className = 'funpass cmxshop';
318
+ d.setAttribute('aria-label', `${OFFER.name} shop`);
319
+ document.body.appendChild(d);
320
+ d.addEventListener('click', (e) => { if (e.target === d || e.target.closest('[data-close]')) d.close(); });
321
+ }
322
+ return d;
323
+ }
324
+ function renderShop(from) {
325
+ const d = shopDialog();
326
+ const rows = shopItems().map((it) => {
327
+ const img = it.img ? `<img src="${esc(it.img)}" alt="" loading="lazy" onerror="this.remove()">` : '';
328
+ const buy = it.owned ? '<span class="sh-own">Owned ✓</span>'
329
+ : `<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>`;
330
+ 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>` : '';
331
+ 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>`
332
+ + `<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>`;
333
+ }).join('');
334
+ const store = OFFER.shopStore ? ` · <a href="${esc(OFFER.shopStore)}" target="_blank" rel="noopener">details in the store</a>` : '';
335
+ d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${esc(OFFER.name)} shop</p>
336
+ <h2>Looks, packs and more</h2><ul class="sh-list">${rows}</ul>
337
+ <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>
338
+ <p class="fp-small"><button class="sh-code" type="button" data-code>Already bought? Turn on your code</button></p></div>`;
339
+ d.querySelectorAll('a[data-offer]').forEach((a) => a.addEventListener('click', () => count(cfg.game, 'cta', `shop-buy-${a.dataset.offer.replace(/^st-/, '')}`.slice(0, 40))));
340
+ d.querySelector('[data-code]').addEventListener('click', () => { d.close(); open('shop-code'); });
341
+ }
342
+ /** Open the Shop sheet; `from` names the button that opened it (counted, and in the buy tag). */
343
+ export function openShop(from = 'button') {
344
+ if (noShop()) { if (portal()) open(from); return; } // a portal build keeps its rewarded-ad sheet
345
+ renderShop(from);
346
+ const d = shopDialog();
347
+ if (d.showModal) { if (!d.open) d.showModal(); } else d.setAttribute('open', '');
348
+ count(cfg.game, 'cta', `shop-open-${slug(from)}`);
349
+ }
350
+ if (typeof document !== 'undefined' && !window.__cmxShop) {
351
+ window.__cmxShop = 1;
352
+ // capture phase on the document: runs before the game's own handlers on that button, and stops them
353
+ document.addEventListener('click', (e) => {
354
+ const b = e.target && e.target.closest && e.target.closest('[data-cmx-shop]');
355
+ if (!b || noShop()) return; // portal / Telegram: the game's own handler (if any) runs as before
356
+ e.preventDefault(); e.stopPropagation();
357
+ openShop(b.dataset.cmxShop || 'menu');
358
+ }, true);
359
+ for (const ev of ['pointerdown', 'mousedown', 'touchstart']) { // tap-to-retry overlays never see a Shop tap
360
+ document.addEventListener(ev, (e) => { if (!noShop() && e.target && e.target.closest && e.target.closest('[data-cmx-shop]')) e.stopPropagation(); }, true);
361
+ }
362
+ paintShop();
363
+ }
364
+
365
  /** Call once per page: game id, display name and the game's own perks (listed first). Reads #cmx-pro=<code> after
366
  * checkout (?unlock=full marks this game's full-game return). Skin-pack codes are taken first by skins.js capture(). */
367
  export function init(options) {
368
  cfg = { ...cfg, ...options };
369
+ const m = /[#&]cmx-key=([^&]+)/.exec(location.hash || '') || /[#&]cmx-pro=([^&]+)/.exec(location.hash || '');
370
  if (m && SID.test(decodeURIComponent(m[1]))) {
371
  const q = new URLSearchParams(location.search);
372
  const fullReturn = q.get('unlock') === 'full';
 
376
  } else recheck();
377
  emit();
378
  }
379
+
380
+ // Direct checkout (17 Oct 2026, Workers cap fix): a click on a data.cybermaxtools.com/buy/<offer> link goes straight to the
381
+ // Stripe/Polar checkout (static map https://cybermaxtools.com/store/buy-direct.json); the click is counted by a beacon.
382
+ (() => {
383
+ if (typeof document === 'undefined' || window.__cmxBd) return;
384
+ window.__cmxBd = 1;
385
+ let MAP = null;
386
+ try { MAP = JSON.parse(sessionStorage.getItem('cmx-bd') || 'null'); } catch { /* none */ }
387
+ 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(() => {});
388
+ document.addEventListener('click', (e) => {
389
+ const a = e.target && e.target.closest && e.target.closest('a[href*="data.cybermaxtools.com/buy/"]');
390
+ if (!a || !MAP || !MAP.offers) return;
391
+ try {
392
+ const u = new URL(a.href);
393
+ const k = /^\/buy\/([a-z0-9-]+)\/?$/.exec(u.pathname);
394
+ const t = k && MAP.offers[k[1]];
395
+ if (!t) return;
396
+ const s = String(u.searchParams.get('s') || 'unknown').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 80) || 'unknown';
397
+ a.href = t.split(MAP.ph).join(s);
398
+ if (!navigator.webdriver) navigator.sendBeacon(u.href + (u.search ? '&' : '?') + 'b=1');
399
+ } catch { /* keep the counted link */ }
400
+ }, true);
401
+ })();
game.js CHANGED
@@ -6,6 +6,7 @@ import { PACKS, deck, packById } from './packs.js';
6
  import { count, store, toast, share, pwa, themeToggle, canvasBlob, download, today, dayNumber, SITE } from './cmx.js';
7
  import { progress, daily, readChallenge, challengeLink, shareChallenge, sfx, burstAt, shake, renderMap, scoreCard, starsFor, starsHtml, css as lvCss } from './levels.js';
8
  import * as pass from './funpass.js';
 
9
  import { apiBase, createRoom, roomInfo, connect } from './net.js';
10
 
11
  const GAME = 'hushfox';
@@ -325,7 +326,7 @@ function finalScreen({ players, history, winners, daily: dly }, { rematch, edit,
325
  <ul class="board">${[...players].sort((a, b) => b.score - a.score).map((p) => `<li class="${winners.includes(p.id) ? 'win' : ''}">${winners.includes(p.id) ? '👑 ' : ''}${esc(p.name)}<b>${p.score}</b></li>`).join('')}</ul>
326
  ${host ? '<button class="btn go" id="again" type="button">↻ Rematch</button>' : `<p class="lead">${esc(waitingNote || 'Waiting for the host to start a rematch…')}</p>`}
327
  <div class="row" style="justify-content:center"><button class="btn sm" id="share" type="button">📤 Share result</button><button class="btn ghost sm" id="card" type="button">🖼 Save card</button>${edit ? '<button class="btn ghost sm" id="edit" type="button">👥 Players</button>' : ''}<button class="btn ghost sm" id="menu" type="button">Menu</button></div>
328
- <div id="fp"></div>`);
329
  snd.win(); [0, 1, 2].forEach((i) => setTimeout(() => burstAt(stage.querySelector('.out'), 0.2 + i * 0.3, 0.5), i * 180));
330
  const text = `Hushfox 🦊 ${history.length} rounds${dly ? ` · Daily deck #${dayNumber(today())}` : ''}\n${line}\n${names.length ? `Winner: ${names.join(' & ')}` : 'The Fox won the night'}`;
331
  if (host) on('again', () => { count(GAME, 'play', 'rematch'); rematch(); });
@@ -593,7 +594,7 @@ function huntEnd() {
593
  <p class="code-big" style="font-size:clamp(46px,14vw,72px);letter-spacing:0">${score}</p><p class="emoji">${grid}</p>
594
  ${vs ? `<p class="banner">${esc(vs)}</p>` : ''}${H.isDaily ? `<p class="small muted2">🔥 streak ${rec.streak} · best today ${rec.best}</p>` : ''}
595
  <div class="row" style="justify-content:center">${canNext ? '<button class="btn" id="nx" type="button">Next level ▶</button>' : ''}<button class="btn ${canNext ? 'ghost' : ''}" id="re" type="button">↻ Again</button></div>
596
- <div class="row" style="justify-content:center"><button class="btn sm" id="beat" type="button">🎯 Challenge a friend</button><button class="btn ghost sm" id="hcard" type="button">🖼 Card</button><button class="btn ghost sm" id="hm" type="button">Fox Hunt</button></div><div id="fp"></div>`);
597
  if (correct >= 4) snd.win(); else snd.lose();
598
  if (stars) [0, 1, 2].slice(0, stars).forEach((i) => setTimeout(() => snd.star(i), 200 + i * 140));
599
  const text = `Hushfox Fox Hunt ${H.isDaily ? `#${dly.num}` : `level ${H.level}`}: ${score}\n${grid}`;
 
6
  import { count, store, toast, share, pwa, themeToggle, canvasBlob, download, today, dayNumber, SITE } from './cmx.js';
7
  import { progress, daily, readChallenge, challengeLink, shareChallenge, sfx, burstAt, shake, renderMap, scoreCard, starsFor, starsHtml, css as lvCss } from './levels.js';
8
  import * as pass from './funpass.js';
9
+ import './looks.js'; // Theme Pack + Collector Pack (themes picked in the pack sheet), $5 Store lane 30 Oct 2026
10
  import { apiBase, createRoom, roomInfo, connect } from './net.js';
11
 
12
  const GAME = 'hushfox';
 
326
  <ul class="board">${[...players].sort((a, b) => b.score - a.score).map((p) => `<li class="${winners.includes(p.id) ? 'win' : ''}">${winners.includes(p.id) ? '👑 ' : ''}${esc(p.name)}<b>${p.score}</b></li>`).join('')}</ul>
327
  ${host ? '<button class="btn go" id="again" type="button">↻ Rematch</button>' : `<p class="lead">${esc(waitingNote || 'Waiting for the host to start a rematch…')}</p>`}
328
  <div class="row" style="justify-content:center"><button class="btn sm" id="share" type="button">📤 Share result</button><button class="btn ghost sm" id="card" type="button">🖼 Save card</button>${edit ? '<button class="btn ghost sm" id="edit" type="button">👥 Players</button>' : ''}<button class="btn ghost sm" id="menu" type="button">Menu</button></div>
329
+ <p class="row" style="justify-content:center">${pass.shopButtonHtml('results')}</p><div id="fp"></div>`);
330
  snd.win(); [0, 1, 2].forEach((i) => setTimeout(() => burstAt(stage.querySelector('.out'), 0.2 + i * 0.3, 0.5), i * 180));
331
  const text = `Hushfox 🦊 ${history.length} rounds${dly ? ` · Daily deck #${dayNumber(today())}` : ''}\n${line}\n${names.length ? `Winner: ${names.join(' & ')}` : 'The Fox won the night'}`;
332
  if (host) on('again', () => { count(GAME, 'play', 'rematch'); rematch(); });
 
594
  <p class="code-big" style="font-size:clamp(46px,14vw,72px);letter-spacing:0">${score}</p><p class="emoji">${grid}</p>
595
  ${vs ? `<p class="banner">${esc(vs)}</p>` : ''}${H.isDaily ? `<p class="small muted2">🔥 streak ${rec.streak} · best today ${rec.best}</p>` : ''}
596
  <div class="row" style="justify-content:center">${canNext ? '<button class="btn" id="nx" type="button">Next level ▶</button>' : ''}<button class="btn ${canNext ? 'ghost' : ''}" id="re" type="button">↻ Again</button></div>
597
+ <div class="row" style="justify-content:center"><button class="btn sm" id="beat" type="button">🎯 Challenge a friend</button><button class="btn ghost sm" id="hcard" type="button">🖼 Card</button><button class="btn ghost sm" id="hm" type="button">Fox Hunt</button></div><p class="row" style="justify-content:center">${pass.shopButtonHtml('results')}</p><div id="fp"></div>`);
598
  if (correct >= 4) snd.win(); else snd.lose();
599
  if (stars) [0, 1, 2].slice(0, stars).forEach((i) => setTimeout(() => snd.star(i), 200 + i * 140));
600
  const text = `Hushfox Fox Hunt ${H.isDaily ? `#${dly.num}` : `level ${H.level}`}: ${score}\n${grid}`;
index.html CHANGED
@@ -19,20 +19,21 @@
19
  <meta name="twitter:description" content="Hushfox: free party game for 3-12 players. Everyone gets the word but the Fox. Pass one phone or join by room code. 30-second rounds, no sign-up, works offline.">
20
  <meta name="twitter:image" content="https://cybermaxtools.com/games/hushfox/og.png">
21
  <style>.h1kw{display:block;font-size:.42em;font-weight:600;opacity:.75;letter-spacing:.01em;margin-top:.15em}.seo ol,.seo ul{padding-left:1.2em}.seo li{margin:.25em 0}</style>
22
- <script type="application/ld+json">{"@context": "https://schema.org", "@graph": [{"@type": "VideoGame", "@id": "https://cybermaxtools.com/games/hushfox/#game", "name": "Hushfox", "alternateName": "Party game online free", "url": "https://cybermaxtools.com/games/hushfox/", "image": "https://cybermaxtools.com/games/hushfox/og.png", "description": "Hushfox is a free party game for 3 to 12 players in the same room. Everyone secretly sees the same word, except one player: the Fox, who only sees the category. Go round giving one clue each, and at the 15-second mark a Twist card flips (mime it, one-syllable clues, opposites, a sound instead of a word) for a second lap. When the 30 seconds are up, point at the Fox. A caught Fox can still steal the round by picking the word from six. Play on one phone passed around the table (works offline) or let everyone join on their own phone with a 4-letter room code. Over 3,400 hand-written family-safe words in the free deck, a daily deck that is the same for everyone, and a solo Fox Hunt with 30 levels.", "genre": ["Party game", "Word game", "Social deduction", "Hidden role"], "gamePlatform": ["Web browser", "Android", "iOS", "Windows", "macOS", "ChromeOS"], "applicationCategory": "Game", "operatingSystem": "Any (web browser)", "playMode": "SinglePlayer", "inLanguage": "en", "isAccessibleForFree": true, "offers": [{"@type": "Offer", "name": "Free demo (daily challenge + levels 1-20)", "price": "0", "priceCurrency": "USD"}], "author": {"@type": "Organization", "name": "CyberMax", "url": "https://cybermaxtools.com/"}, "publisher": {"@type": "Organization", "name": "CyberMax"}, "datePublished": "2026-10-09", "keywords": "Party game online free, free party word game for groups, unblocked, no download, browser game", "trailer": {"@id": "https://cybermaxtools.com/games/hushfox/#video"}}, {"@type": "FAQPage", "@id": "https://cybermaxtools.com/games/hushfox/#faq", "mainEntity": [{"@type": "Question", "name": "Is Hushfox free?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Pass-the-phone, room codes, the Classic deck, the daily deck and Fox Hunt levels 1-20 are free with no ads. The optional CyberMax Fun Pass adds 8 extra packs and levels 21-30."}}, {"@type": "Question", "name": "How many players do I need?", "acceptedAnswer": {"@type": "Answer", "text": "3 to 12 players in the same room. It is best with 4-8."}}, {"@type": "Question", "name": "Does everyone need a phone?", "acceptedAnswer": {"@type": "Answer", "text": "No. Pass one phone around the table, or use a room code if everyone has their own phone."}}, {"@type": "Question", "name": "Does it work offline?", "acceptedAnswer": {"@type": "Answer", "text": "Pass-the-phone and Fox Hunt work offline after your first visit. Room codes need an internet connection."}}, {"@type": "Question", "name": "Is it family friendly?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Every word is hand-written and checked against a blocklist. The optional AI surprise category is filtered the same way. The Grown-ups night pack is about adult life (bills, dinner parties), never anything rude."}}, {"@type": "Question", "name": "Is it the same as other imposter games?", "acceptedAnswer": {"@type": "Answer", "text": "Hushfox has its own rules: a Twist card flips halfway through every round, and a caught Fox can steal the round by picking the word from six."}}, {"@type": "Question", "name": "Do you store anything about players?", "acceptedAnswer": {"@type": "Answer", "text": "No accounts or emails. A room keeps nicknames and scores only while it is open and deletes itself after 3 hours."}}]}, {"@type": "VideoObject", "name": "Hushfox demo: Find the Fox in 30 seconds", "description": "A 17-second screen recording of the live Hushfox page: Everyone sees the word, except the Fox; One clue each, then a Twist card flips; Caught? The Fox can still steal the word. Real result, no voice.", "thumbnailUrl": ["https://cybermax.cybermax-tools.workers.dev/media/demos/hushfox/poster.jpg"], "uploadDate": "2026-10-09T01:28:19+00:00", "duration": "PT17S", "contentUrl": "https://cybermax.cybermax-tools.workers.dev/media/demos/hushfox/demo-16x9.mp4", "embedUrl": "https://cybermaxtools.com/games/hushfox/", "inLanguage": "en", "publisher": {"@type": "Organization", "name": "CyberMax", "logo": {"@type": "ImageObject", "url": "https://cybermax.cybermax-tools.workers.dev/store/img/favicon.webp"}}, "@id": "https://cybermaxtools.com/games/hushfox/#video"}]}</script>
23
  </head><body>
24
  <div class="wrap">
25
  <header class="top"><a class="brand" href="https://cybermaxtools.com/games/"><svg viewBox="0 0 512 512" aria-hidden="true"><clipPath id="lg"><rect width="512" height="512" rx="116"/></clipPath><g clip-path="url(#lg)"><rect width="512" height="512" fill="#FF6A3D"/><path d="M512 250V512H250Z" fill="#D9481F"/></g><g transform="translate(86 86) scale(3.4)"><path d="M66 27 A27 27 0 1 0 66 73" fill="none" stroke="#11141B" stroke-width="17" stroke-linecap="round"/><rect x="66" y="41" width="20" height="18" rx="4" fill="#11141B"/></g></svg>CyberMax</a>
26
- <nav><a href="https://cybermaxtools.com/games/">Games</a></nav><button class="icon" id="mute" type="button" aria-label="Sound on or off">🔊</button><button class="icon" id="theme" type="button" aria-label="Switch light or dark">◐</button></header>
27
  <main>
28
  <section class="game">
29
- <div class="headline"><h1>Hushfox <span id="num" class="num"></span> <small class="h1kw">Free party word game for groups</small></h1>
30
  <p class="muted tag">Everyone gets the secret word except the Fox. One clue each, a Twist at 15 seconds, then point at the Fox. 3-12 players, one phone or a room code.</p></div>
31
  <div class="stage" id="stage" aria-live="polite">
32
  <div class="scr"><p class="logo">HUSH<i>FOX</i></p><p class="lead">Loading the party…</p><noscript><p class="lead">Hushfox needs JavaScript turned on.</p></noscript></div>
33
  </div>
34
  <div class="row under"><span class="muted small">Fox Hunt daily best <b id="best">-</b> · 🔥 streak <b id="streak">0</b></span><button class="btn ghost sm" id="install" type="button" hidden>📲 Add to Home Screen</button><button class="btn pass sm" id="pass-btn" type="button">✨ Fun Pass</button></div>
35
  </section>
 
36
  <section class="seo" id="guide">
37
  <h2>Hushfox: party game online free</h2>
38
  <p>Hushfox is a free party game for 3 to 12 players in the same room. Everyone secretly sees the same word, except one player: the Fox, who only sees the category. Go round giving one clue each, and at the 15-second mark a Twist card flips (mime it, one-syllable clues, opposites, a sound instead of a word) for a second lap. When the 30 seconds are up, point at the Fox. A caught Fox can still steal the round by picking the word from six. Play on one phone passed around the table (works offline) or let everyone join on their own phone with a 4-letter room code. Over 3,400 hand-written family-safe words in the free deck, a daily deck that is the same for everyone, and a solo Fox Hunt with 30 levels.</p>
@@ -44,16 +45,17 @@
44
  <h2>Hushfox tips and strategy</h2>
45
  <ul><li>As a word holder, give a clue only someone who knows the word would understand, not the obvious one.</li><li>As the Fox, listen to the first clues and stay vague: a colour, a feeling or a place works for many words.</li><li>Watch who hesitates when the Twist card flips: the Fox has to improvise twice.</li><li>A split vote lets the Fox escape, so talk before you point.</li><li>Play the daily deck with friends in other cities and compare your result lines.</li></ul>
46
  <h2>Game modes</h2>
47
- <ul><li>Pass one phone (free, offline): 3-12 players, 3, 5 or 8 rounds, the free Classic deck of 145 categories.</li><li>Room code (free): everyone joins on their own phone; the host picks packs and rounds.</li><li>Daily deck: the same 5 words for everyone today.</li><li>Fox Hunt (solo): spot the word that doesn&#x27;t belong, a daily puzzle and 30 levels with stars.</li><li>Fun Pass: 8 extra packs (Grown-ups night, Movie night, Office hours, Holidays, Español, Português, Deutsch, Français) and Fox Hunt levels 21-30.</li></ul>
48
- <p>The daily challenge and levels 1-20 are free to play, with no ads and no sign-up. Later levels, bonus modes, the daily archive and colour themes come with the optional <a href="https://cybermaxtools.com/games/#pass">CyberMax Fun Pass</a> ($2.99/month, $19/year or $29.99 for life), which opens everything in every CyberMax game.</p>
49
  <h2>Hushfox FAQ</h2>
50
- <h3>Is Hushfox free?</h3><p>Yes. Pass-the-phone, room codes, the Classic deck, the daily deck and Fox Hunt levels 1-20 are free with no ads. The optional CyberMax Fun Pass adds 8 extra packs and levels 21-30.</p>
51
  <h3>How many players do I need?</h3><p>3 to 12 players in the same room. It is best with 4-8.</p>
52
  <h3>Does everyone need a phone?</h3><p>No. Pass one phone around the table, or use a room code if everyone has their own phone.</p>
53
  <h3>Does it work offline?</h3><p>Pass-the-phone and Fox Hunt work offline after your first visit. Room codes need an internet connection.</p>
54
  <h3>Is it family friendly?</h3><p>Yes. Every word is hand-written and checked against a blocklist. The optional AI surprise category is filtered the same way. The Grown-ups night pack is about adult life (bills, dinner parties), never anything rude.</p>
55
  <h3>Is it the same as other imposter games?</h3><p>Hushfox has its own rules: a Twist card flips halfway through every round, and a caught Fox can steal the round by picking the word from six.</p>
56
  <h3>Do you store anything about players?</h3><p>No accounts or emails. A room keeps nicknames and scores only while it is open and deletes itself after 3 hours.</p>
 
57
  <h2>Similar free games</h2>
58
  <div class="more"><a href="https://cybermaxtools.com/games/kenwren/"><b>Kenwren</b><span>How well do your friends know you?</span></a><a href="https://cybermaxtools.com/games/townduel/"><b>Townduel</b><span>Which US town costs more? 10 daily duels</span></a><a href="https://cybermaxtools.com/games/pawfoil/"><b>Pawfoil</b><span>Make your pet a holo trading card</span></a><a href="https://cybermaxtools.com/games/"><b>All CyberMax games</b><span>Free browser games, no download</span></a></div>
59
  </section>
@@ -61,4 +63,4 @@
61
  <footer>© 2026 CyberMax · <a href="https://cybermaxtools.com/games/">Games</a> · <a href="https://cybermaxtools.com/toys/">Toys</a> · <a href="https://cybermaxtools.com/store/">Store</a> · <a href="https://cybermaxtools.com/privacy">Privacy</a> · <a href="https://cybermaxtools.com/terms">Terms</a></footer>
62
  </div>
63
  <script type="module" src="game.js"></script>
64
- <!-- cybermax-count:start --><script>(function(){try{var D="https://data.cybermaxtools.com",P=location.host+location.pathname,H=navigator.webdriver||!(navigator.languages&&navigator.languages.length)||!screen.width?"&h=1":"",Z=function(x){navigator.sendBeacon?navigator.sendBeacon(x):(new Image).src=x},V=null,K="cmx-visit";if(navigator.doNotTrack!=="1")try{var o=JSON.parse(sessionStorage.getItem(K)||"null"),t=Date.now();if(!o||t-o.t>18e5)o={e:P,n:0};o.n++;o.t=t;sessionStorage.setItem(K,JSON.stringify(o));V=o}catch(e){}var u=D+"/v?p="+encodeURIComponent(P)+"&r="+encodeURIComponent((function(r){var h=r.split("/")[2]||"";return h===location.host?r.split(/[?#]/)[0]:h})(document.referrer||""))+H+(function(){var q=new URLSearchParams(location.search),s=q.get("utm_source"),t=(q.get("s")||"").toLowerCase().replace(/[^a-z0-9-]/g,"-").slice(0,80);return s?"&us="+encodeURIComponent(s)+"&uc="+encodeURIComponent(q.get("utm_campaign")||""):t?"&us=s&uc="+encodeURIComponent(t):""})()+(V?"&vn="+V.n+(V.n>1?"&ve="+encodeURIComponent(V.e):""):"");Z(u);if(!V)return;var B="a[data-count],a[href*=\"buy.stripe.com\"],a[href*=\"data.cybermaxtools.com/buy/\"],a[href*=\".workers.dev/buy/\"],a[href*=\"buy.polar.sh\"],a[href*=\"polar.sh/checkout\"],[data-cmx-buy]",E=function(k){try{var o=JSON.parse(sessionStorage.getItem(K)||"null");if(!o||o[k])return;o[k]=1;sessionStorage.setItem(K,JSON.stringify(o));Z(D+"/v?p="+encodeURIComponent(P)+"&ev="+k+"&ve="+encodeURIComponent(o.e)+H)}catch(e){}};document.addEventListener("click",function(x){var a=x.target&&x.target.closest&&x.target.closest(B);if(a){E("seen");E("click")}},true);var S=V.seen,C=function(){if(S)return;var l=document.querySelectorAll(B),w=innerWidth,h=innerHeight;for(var i=0;i<l.length;i++){var r=l[i].getBoundingClientRect();if(r.width>0&&r.height>0&&r.bottom>0&&r.right>0&&r.top<h&&r.left<w){S=1;E("seen");return}}},T0=Date.now(),I=setInterval(function(){C();if(S||Date.now()-T0>6e5)clearInterval(I)},1500),R=0;addEventListener("scroll",function(){if(!R&&!S){R=1;setTimeout(function(){R=0;C()},250)}},{passive:true});document.readyState=="loading"?document.addEventListener("DOMContentLoaded",C):C()}catch(e){}})()</script><!-- cybermax-count:end --></body></html>
 
19
  <meta name="twitter:description" content="Hushfox: free party game for 3-12 players. Everyone gets the word but the Fox. Pass one phone or join by room code. 30-second rounds, no sign-up, works offline.">
20
  <meta name="twitter:image" content="https://cybermaxtools.com/games/hushfox/og.png">
21
  <style>.h1kw{display:block;font-size:.42em;font-weight:600;opacity:.75;letter-spacing:.01em;margin-top:.15em}.seo ol,.seo ul{padding-left:1.2em}.seo li{margin:.25em 0}</style>
22
+ <script type="application/ld+json">{"@context": "https://schema.org", "@graph": [{"@type": "VideoGame", "@id": "https://cybermaxtools.com/games/hushfox/#game", "name": "Hushfox", "alternateName": "Party game online free", "url": "https://cybermaxtools.com/games/hushfox/", "image": "https://cybermaxtools.com/games/hushfox/og.png", "description": "Hushfox is a free party game for 3 to 12 players in the same room. Everyone secretly sees the same word, except one player: the Fox, who only sees the category. Go round giving one clue each, and at the 15-second mark a Twist card flips (mime it, one-syllable clues, opposites, a sound instead of a word) for a second lap. When the 30 seconds are up, point at the Fox. A caught Fox can still steal the round by picking the word from six. Play on one phone passed around the table (works offline) or let everyone join on their own phone with a 4-letter room code. Over 3,400 hand-written family-safe words in the free deck, a daily deck that is the same for everyone, and a solo Fox Hunt with 30 levels.", "genre": ["Party game", "Word game", "Social deduction", "Hidden role"], "gamePlatform": ["Web browser", "Android", "iOS", "Windows", "macOS", "ChromeOS"], "applicationCategory": "Game", "operatingSystem": "Any (web browser)", "playMode": "SinglePlayer", "inLanguage": "en", "isAccessibleForFree": true, "offers": [{"@type": "Offer", "name": "Free demo (daily challenge + levels 1-20)", "price": "0", "priceCurrency": "USD"}, {"@type": "Offer", "name": "Full game (one-time unlock)", "price": "3.99", "priceCurrency": "USD"}], "author": {"@type": "Organization", "name": "CyberMax", "url": "https://cybermaxtools.com/"}, "publisher": {"@type": "Organization", "name": "CyberMax"}, "datePublished": "2026-10-09", "keywords": "Party game online free, free party word game for groups, unblocked, no download, browser game", "trailer": {"@id": "https://cybermaxtools.com/games/hushfox/#video"}}, {"@type": "FAQPage", "@id": "https://cybermaxtools.com/games/hushfox/#faq", "mainEntity": [{"@type": "Question", "name": "Is Hushfox free?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Pass-the-phone, room codes, the Classic deck, the daily deck and Fox Hunt levels 1-20 are free with no ads. The full game ($3.99 once) or the Fun Pass adds 8 extra packs and levels 21-30."}}, {"@type": "Question", "name": "How many players do I need?", "acceptedAnswer": {"@type": "Answer", "text": "3 to 12 players in the same room. It is best with 4-8."}}, {"@type": "Question", "name": "Does everyone need a phone?", "acceptedAnswer": {"@type": "Answer", "text": "No. Pass one phone around the table, or use a room code if everyone has their own phone."}}, {"@type": "Question", "name": "Does it work offline?", "acceptedAnswer": {"@type": "Answer", "text": "Pass-the-phone and Fox Hunt work offline after your first visit. Room codes need an internet connection."}}, {"@type": "Question", "name": "Is it family friendly?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. Every word is hand-written and checked against a blocklist. The optional AI surprise category is filtered the same way. The Grown-ups night pack is about adult life (bills, dinner parties), never anything rude."}}, {"@type": "Question", "name": "Is it the same as other imposter games?", "acceptedAnswer": {"@type": "Answer", "text": "Hushfox has its own rules: a Twist card flips halfway through every round, and a caught Fox can steal the round by picking the word from six."}}, {"@type": "Question", "name": "Do you store anything about players?", "acceptedAnswer": {"@type": "Answer", "text": "No accounts or emails. A room keeps nicknames and scores only while it is open and deletes itself after 3 hours."}}, {"@type": "Question", "name": "How much is the full Hushfox?", "acceptedAnswer": {"@type": "Answer", "text": "$3.99 once, no subscription: it opens every level, mode, the daily archive and themes in Hushfox for good, and your receipt code works on any device. The daily challenge and levels 1-20 stay free. The CyberMax Fun Pass opens every CyberMax game instead."}}]}, {"@type": "VideoObject", "name": "Hushfox demo: Find the Fox in 30 seconds", "description": "A 17-second screen recording of the live Hushfox page: Everyone sees the word, except the Fox; One clue each, then a Twist card flips; Caught? The Fox can still steal the word. Real result, no voice.", "thumbnailUrl": ["https://cybermax.cybermax-tools.workers.dev/media/demos/hushfox/poster.jpg"], "uploadDate": "2026-10-09T01:28:19+00:00", "duration": "PT17S", "contentUrl": "https://cybermax.cybermax-tools.workers.dev/media/demos/hushfox/demo-16x9.mp4", "embedUrl": "https://cybermaxtools.com/games/hushfox/", "inLanguage": "en", "publisher": {"@type": "Organization", "name": "CyberMax", "logo": {"@type": "ImageObject", "url": "https://cybermax.cybermax-tools.workers.dev/store/img/favicon.webp"}}, "@id": "https://cybermaxtools.com/games/hushfox/#video"}]}</script>
23
  </head><body>
24
  <div class="wrap">
25
  <header class="top"><a class="brand" href="https://cybermaxtools.com/games/"><svg viewBox="0 0 512 512" aria-hidden="true"><clipPath id="lg"><rect width="512" height="512" rx="116"/></clipPath><g clip-path="url(#lg)"><rect width="512" height="512" fill="#FF6A3D"/><path d="M512 250V512H250Z" fill="#D9481F"/></g><g transform="translate(86 86) scale(3.4)"><path d="M66 27 A27 27 0 1 0 66 73" fill="none" stroke="#11141B" stroke-width="17" stroke-linecap="round"/><rect x="66" y="41" width="20" height="18" rx="4" fill="#11141B"/></g></svg>CyberMax</a>
26
+ <nav><a href="https://cybermaxtools.com/games/">Games</a></nav><button class="btn sm cmx-shopbtn cmx-shop-top" type="button" data-cmx-shop="title">🛍️ Shop</button><button class="icon" id="mute" type="button" aria-label="Sound on or off">🔊</button><button class="icon" id="theme" type="button" aria-label="Switch light or dark">◐</button></header>
27
  <main>
28
  <section class="game">
29
+ <div class="headline"><h1>Hushfox <span id="num" class="num"></span> <small class="h1kw">Free party word game for groups</small></h1>
30
  <p class="muted tag">Everyone gets the secret word except the Fox. One clue each, a Twist at 15 seconds, then point at the Fox. 3-12 players, one phone or a room code.</p></div>
31
  <div class="stage" id="stage" aria-live="polite">
32
  <div class="scr"><p class="logo">HUSH<i>FOX</i></p><p class="lead">Loading the party…</p><noscript><p class="lead">Hushfox needs JavaScript turned on.</p></noscript></div>
33
  </div>
34
  <div class="row under"><span class="muted small">Fox Hunt daily best <b id="best">-</b> · 🔥 streak <b id="streak">0</b></span><button class="btn ghost sm" id="install" type="button" hidden>📲 Add to Home Screen</button><button class="btn pass sm" id="pass-btn" type="button">✨ Fun Pass</button></div>
35
  </section>
36
+ <!-- cmx:getmore --><aside class="cmx-gm" aria-label="Get more for Hushfox"><style>.cmx-gm{margin:14px auto 6px;max-width:960px;padding:12px 14px;border:1px solid var(--line,rgba(255,255,255,.14));border-radius:16px;background:var(--card,rgba(255,255,255,.05));color:var(--ink,inherit);font:15px/1.4 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:left}.cmx-gm .gm-h{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:4px 12px;margin:0 0 8px}.cmx-gm .gm-h b{font-size:16px}.cmx-gm .gm-h a{font-size:14px;font-weight:700;color:inherit;opacity:.85}.cmx-gm .gm-l{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px}.cmx-gm .gm-i{display:grid;grid-template-columns:40px 1fr auto;align-items:center;gap:10px;padding:8px 10px;border-radius:12px;border:1px solid var(--line,rgba(255,255,255,.12));text-decoration:none;color:inherit;background:rgba(127,127,127,.06)}.cmx-gm .gm-i:hover{border-color:#FF9A3D}.cmx-gm .gm-s{width:40px;height:40px;border-radius:10px;display:grid;place-items:center;font-size:20px}.cmx-gm .gm-t{min-width:0}.cmx-gm .gm-t b{display:block;font-size:14px}.cmx-gm .gm-t small{display:block;font-size:12px;opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cmx-gm .gm-b{font-weight:900;font-size:14px;padding:7px 12px;border-radius:999px;background:linear-gradient(135deg,#FFB547,#FF6A3D);color:#11141B;white-space:nowrap}html.cmx-noshop .cmx-gm{display:none}</style><p class="gm-h"><b>🛍️ Get more for Hushfox</b><a href="https://cybermaxtools.com/store/hushfox-theme-pack/">All packs and details in the store →</a></p><div class="gm-l"><a class="gm-i" href="https://data.cybermaxtools.com/buy/st-hushfox-skins-pack?s=game-hushfox-page" rel="nofollow" data-gm="st-hushfox-skins-pack"><span class="gm-s" style="background:linear-gradient(135deg,#2FD3A0,#FF5C8A,#3AB8FF)" aria-hidden="true">🦊</span><span class="gm-t"><b>Theme Pack</b><small>6 themes: Mint, Rose, Ocean, Gold, Noir and Sunset. Looks only.</small></span><span class="gm-b">$1.99</span></a><a class="gm-i" href="https://data.cybermaxtools.com/buy/st-hushfox-full-unlock?s=game-hushfox-page" rel="nofollow" data-gm="st-hushfox-full-unlock"><span class="gm-s" style="background:linear-gradient(135deg,#1C1530,#1C1530)" aria-hidden="true">🔓</span><span class="gm-t"><b>Full game</b><small>8 extra word packs (Grown-ups night, Movie night, 4 languages and more) and Fox Hunt levels 21-30. One payment, no subscription.</small></span><span class="gm-b">$3.99</span></a><a class="gm-i" href="https://data.cybermaxtools.com/buy/st-hushfox-collector-pack?s=game-hushfox-page" rel="nofollow" data-gm="st-hushfox-collector-pack"><span class="gm-s" style="background:linear-gradient(135deg,#45E0B4,#FF5E3A,#FF4FD8)" aria-hidden="true">💎</span><span class="gm-t"><b>Collector Pack</b><small>All 10 themes: the 6 in the Theme Pack plus Aurora Night, Lava Lamp, Disco and Galaxy (animated), only in this pack. Looks only.</small></span><span class="gm-b">$4.99</span></a></div></aside><!-- /cmx:getmore -->
37
  <section class="seo" id="guide">
38
  <h2>Hushfox: party game online free</h2>
39
  <p>Hushfox is a free party game for 3 to 12 players in the same room. Everyone secretly sees the same word, except one player: the Fox, who only sees the category. Go round giving one clue each, and at the 15-second mark a Twist card flips (mime it, one-syllable clues, opposites, a sound instead of a word) for a second lap. When the 30 seconds are up, point at the Fox. A caught Fox can still steal the round by picking the word from six. Play on one phone passed around the table (works offline) or let everyone join on their own phone with a 4-letter room code. Over 3,400 hand-written family-safe words in the free deck, a daily deck that is the same for everyone, and a solo Fox Hunt with 30 levels.</p>
 
45
  <h2>Hushfox tips and strategy</h2>
46
  <ul><li>As a word holder, give a clue only someone who knows the word would understand, not the obvious one.</li><li>As the Fox, listen to the first clues and stay vague: a colour, a feeling or a place works for many words.</li><li>Watch who hesitates when the Twist card flips: the Fox has to improvise twice.</li><li>A split vote lets the Fox escape, so talk before you point.</li><li>Play the daily deck with friends in other cities and compare your result lines.</li></ul>
47
  <h2>Game modes</h2>
48
+ <ul><li>Pass one phone (free, offline): 3-12 players, 3, 5 or 8 rounds, the free Classic deck of 145 categories.</li><li>Room code (free): everyone joins on their own phone; the host picks packs and rounds.</li><li>Daily deck: the same 5 words for everyone today.</li><li>Fox Hunt (solo): spot the word that doesn&#x27;t belong, a daily puzzle and 30 levels with stars.</li><li>Full game ($3.99 once) or Fun Pass: 8 extra packs (Grown-ups night, Movie night, Office hours, Holidays, Español, Português, Deutsch, Français) and Fox Hunt levels 21-30.</li></ul>
49
+ <p>The daily challenge and levels 1-20 are free to play, with no ads and no sign-up. Buy the full game once for $3.99 to open every level, mode, the daily archive and themes in Hushfox, or get the optional <a href="https://cybermaxtools.com/games/#pass">CyberMax Fun Pass</a> ($2.99/month, $19/year or $29.99 for life), which opens everything in every CyberMax game.</p>
50
  <h2>Hushfox FAQ</h2>
51
+ <h3>Is Hushfox free?</h3><p>Yes. Pass-the-phone, room codes, the Classic deck, the daily deck and Fox Hunt levels 1-20 are free with no ads. The full game ($3.99 once) or the Fun Pass adds 8 extra packs and levels 21-30.</p>
52
  <h3>How many players do I need?</h3><p>3 to 12 players in the same room. It is best with 4-8.</p>
53
  <h3>Does everyone need a phone?</h3><p>No. Pass one phone around the table, or use a room code if everyone has their own phone.</p>
54
  <h3>Does it work offline?</h3><p>Pass-the-phone and Fox Hunt work offline after your first visit. Room codes need an internet connection.</p>
55
  <h3>Is it family friendly?</h3><p>Yes. Every word is hand-written and checked against a blocklist. The optional AI surprise category is filtered the same way. The Grown-ups night pack is about adult life (bills, dinner parties), never anything rude.</p>
56
  <h3>Is it the same as other imposter games?</h3><p>Hushfox has its own rules: a Twist card flips halfway through every round, and a caught Fox can steal the round by picking the word from six.</p>
57
  <h3>Do you store anything about players?</h3><p>No accounts or emails. A room keeps nicknames and scores only while it is open and deletes itself after 3 hours.</p>
58
+ <h3>How much is the full Hushfox?</h3><p>$3.99 once, no subscription: it opens every level, mode, the daily archive and themes in Hushfox for good, and your receipt code works on any device. The daily challenge and levels 1-20 stay free. The CyberMax Fun Pass opens every CyberMax game instead.</p>
59
  <h2>Similar free games</h2>
60
  <div class="more"><a href="https://cybermaxtools.com/games/kenwren/"><b>Kenwren</b><span>How well do your friends know you?</span></a><a href="https://cybermaxtools.com/games/townduel/"><b>Townduel</b><span>Which US town costs more? 10 daily duels</span></a><a href="https://cybermaxtools.com/games/pawfoil/"><b>Pawfoil</b><span>Make your pet a holo trading card</span></a><a href="https://cybermaxtools.com/games/"><b>All CyberMax games</b><span>Free browser games, no download</span></a></div>
61
  </section>
 
63
  <footer>© 2026 CyberMax · <a href="https://cybermaxtools.com/games/">Games</a> · <a href="https://cybermaxtools.com/toys/">Toys</a> · <a href="https://cybermaxtools.com/store/">Store</a> · <a href="https://cybermaxtools.com/privacy">Privacy</a> · <a href="https://cybermaxtools.com/terms">Terms</a></footer>
64
  </div>
65
  <script type="module" src="game.js"></script>
66
+ </body></html>
looks.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Hushfox Theme Pack ($1.99) + Collector Pack ($4.99): extra themes for the free game, one payment each, cosmetic only
2
+ // ($5 Store lane, 30 Oct 2026). Data + unlock only. Hushfox has no theme picker, so the pack sheet itself lists the owned themes (cfg.picker); a theme recolours the party screens and cards.
3
+ // Checkout + unlock: ./skins.js (data Worker /unlock/hushfox-skins and /unlock/hushfox-collector; a CyberMax Arcade Lifetime
4
+ // code opens both). A shop sheet opens the offer with looks.open('shop') or window.cmxSkinPack.open(), and can list
5
+ // window.cmxSkinPack.ladder (pack -> Collector Pack -> Arcade Lifetime). (c) 2026 CyberMax. All rights reserved.
6
+ import * as skins from './skins.js';
7
+
8
+ export const GAME = 'hushfox';
9
+ /** 6 pack themes (the Theme Pack) */
10
+ export const PACK_LOOKS = {"mint": {"name": "Mint", "fox": "#2FD3A0", "night": "#0F2422", "cream": "#F0FFF8", "swatch": "linear-gradient(135deg, #0F2422 50%, #2FD3A0 50%)"}, "rose": {"name": "Rose", "fox": "#FF5C8A", "night": "#2A1020", "cream": "#FFF0F5", "swatch": "linear-gradient(135deg, #2A1020 50%, #FF5C8A 50%)"}, "ocean": {"name": "Ocean", "fox": "#3AB8FF", "night": "#0A1A33", "cream": "#EEF7FF", "swatch": "linear-gradient(135deg, #0A1A33 50%, #3AB8FF 50%)"}, "gold": {"name": "Gold", "fox": "#FFC845", "night": "#1A1508", "cream": "#FFF8E1", "swatch": "linear-gradient(135deg, #1A1508 50%, #FFC845 50%)"}, "noir": {"name": "Noir", "fox": "#F2F2F2", "night": "#0B0B0D", "cream": "#FFFFFF", "swatch": "linear-gradient(135deg, #0B0B0D 50%, #F2F2F2 50%)"}, "sunset": {"name": "Sunset", "fox": "#FF8A3D", "night": "#2B0F2E", "cream": "#FFF1E8", "swatch": "linear-gradient(135deg, #2B0F2E 50%, #FF8A3D 50%)"}};
11
+ /** 4 collector-only themes (the Collector Pack also includes every pack look) */
12
+ export const COLLECTOR_LOOKS = {"aurora": {"name": "Aurora Night", "fox": "#45E0B4", "night": "#061A26", "cream": "#E8FFFA", "swatch": "linear-gradient(135deg, #061A26 50%, #45E0B4 50%)"}, "lava": {"name": "Lava Lamp", "fox": "#FF5E3A", "night": "#240A06", "cream": "#FFF0E8", "swatch": "linear-gradient(135deg, #240A06 50%, #FF5E3A 50%)"}, "disco": {"name": "Disco", "fox": "#FF4FD8", "night": "#14002E", "cream": "#FFF0FD", "swatch": "linear-gradient(135deg, #14002E 50%, #FF4FD8 50%)"}, "galaxy": {"name": "Galaxy", "fox": "#B8C6FF", "night": "#05051A", "cream": "#F2F4FF", "swatch": "linear-gradient(135deg, #05051A 50%, #B8C6FF 50%)"}};
13
+ const CSS = ":root[data-hx=mint]{--fox:#2FD3A0;--acc:#2FD3A0;--night:#0F2422;--night2:#163A35;--cream:#F0FFF8}:root[data-hx=mint] .stage{background:radial-gradient(120% 90% at 50% 0%,#1F5249 0%,var(--night) 62%)}:root[data-hx=rose]{--fox:#FF5C8A;--acc:#FF5C8A;--night:#2A1020;--night2:#3E1830;--cream:#FFF0F5}:root[data-hx=rose] .stage{background:radial-gradient(120% 90% at 50% 0%,#5A2346 0%,var(--night) 62%)}:root[data-hx=ocean]{--fox:#3AB8FF;--acc:#3AB8FF;--night:#0A1A33;--night2:#12294D;--cream:#EEF7FF}:root[data-hx=ocean] .stage{background:radial-gradient(120% 90% at 50% 0%,#1E3F73 0%,var(--night) 62%)}:root[data-hx=gold]{--fox:#FFC845;--acc:#FFC845;--night:#1A1508;--night2:#2B2310;--cream:#FFF8E1}:root[data-hx=gold] .stage{background:radial-gradient(120% 90% at 50% 0%,#4A3A12 0%,var(--night) 62%)}:root[data-hx=noir]{--fox:#F2F2F2;--acc:#F2F2F2;--night:#0B0B0D;--night2:#18181C;--cream:#FFFFFF}:root[data-hx=noir] .stage{background:radial-gradient(120% 90% at 50% 0%,#2A2A30 0%,var(--night) 62%)}:root[data-hx=sunset]{--fox:#FF8A3D;--acc:#FF8A3D;--night:#2B0F2E;--night2:#41163F;--cream:#FFF1E8}:root[data-hx=sunset] .stage{background:radial-gradient(120% 90% at 50% 0%,#6B2350 0%,var(--night) 62%)}:root[data-hx=aurora]{--fox:#45E0B4;--acc:#45E0B4;--night:#061A26;--night2:#0E3040;--cream:#E8FFFA}:root[data-hx=aurora] .stage{background:radial-gradient(120% 90% at 50% 0%,#1C6B5E 0%,var(--night) 62%)}@media (prefers-reduced-motion:no-preference){:root[data-hx=aurora] .stage{background:linear-gradient(120deg,#061A26,#1C6B5E,#3B1C6E,#0E5C7A,#061A26);background-size:300% 300%;animation:cmx-lk 14s ease-in-out infinite}}:root[data-hx=lava]{--fox:#FF5E3A;--acc:#FF5E3A;--night:#240A06;--night2:#3D140C;--cream:#FFF0E8}:root[data-hx=lava] .stage{background:radial-gradient(120% 90% at 50% 0%,#8C2A12 0%,var(--night) 62%)}@media (prefers-reduced-motion:no-preference){:root[data-hx=lava] .stage{background:linear-gradient(120deg,#240A06,#8C2A12,#C2541C,#5A1A30,#240A06);background-size:300% 300%;animation:cmx-lk 14s ease-in-out infinite}}:root[data-hx=disco]{--fox:#FF4FD8;--acc:#FF4FD8;--night:#14002E;--night2:#24104A;--cream:#FFF0FD}:root[data-hx=disco] .stage{background:radial-gradient(120% 90% at 50% 0%,#5B1A8C 0%,var(--night) 62%)}@media (prefers-reduced-motion:no-preference){:root[data-hx=disco] .stage{background:linear-gradient(120deg,#14002E,#5B1A8C,#00A3A3,#B0457A,#14002E);background-size:300% 300%;animation:cmx-lk 14s ease-in-out infinite}}:root[data-hx=galaxy]{--fox:#B8C6FF;--acc:#B8C6FF;--night:#05051A;--night2:#10103A;--cream:#F2F4FF}:root[data-hx=galaxy] .stage{background:radial-gradient(120% 90% at 50% 0%,#2A1C6E 0%,var(--night) 62%)}@media (prefers-reduced-motion:no-preference){:root[data-hx=galaxy] .stage{background:linear-gradient(120deg,#05051A,#2A1C6E,#3B0E5A,#05051A);background-size:300% 300%;animation:cmx-lk 14s ease-in-out infinite}}.sp-row{display:flex;gap:6px;margin:0 0 10px;flex-wrap:wrap}.sp-dot{width:26px;height:26px;border-radius:50%;display:inline-block;border:2px solid rgba(255,255,255,.35);vertical-align:middle}.sp-pick{display:flex;flex-wrap:wrap;gap:8px}.sp-pick .sp-dot{width:18px;height:18px}.sp-pick [aria-pressed=true]{outline:2px solid currentColor}@keyframes cmx-lk{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}";
14
+ if (typeof document !== 'undefined') { const st = document.createElement('style'); st.textContent = CSS; document.head.appendChild(st); }
15
+
16
+ skins.capture({ game: GAME, name: 'Hushfox', label: 'Theme Pack', unit: 'themes', skins: 6, price: '$1.99', where: '', picker: { label: 'Pick a theme', items: () => [{ id: 'classic', name: 'Classic', swatch: 'linear-gradient(135deg, #1C1530 50%, #FF7A2F 50%)' }, ...Object.entries(LOOKS).map(([id, x]) => ({ id, ...x }))], current: () => cur(), pick: (id) => pick(id) },
17
+ collector: { price: '$4.99', extra: 4, names: "Aurora Night, Lava Lamp, Disco, Galaxy" },
18
+ preview: Object.values(PACK_LOOKS).map((x) => x.swatch),
19
+ perks: ["6 themes: Mint, Rose, Ocean, Gold, Noir, Sunset", "Recolours the party screens and word cards, on every phone you pass round", "Cosmetic only: same words, same rules"] });
20
+ const had = [skins.owned(), skins.collector()];
21
+ const tag = (o, extra) => Object.fromEntries(Object.entries(o).map(([k, v]) => [k, { ...v, ...extra }]));
22
+ /** The looks this player owns (empty until bought, so nothing unsellable shows in the game's own picker). */
23
+ export const LOOKS = { ...(had[0] ? tag(PACK_LOOKS, { pack: true, free: true }) : {}), ...(had[1] ? tag(COLLECTOR_LOOKS, { pack: true, collector: true, free: true }) : {}) };
24
+ export const owned = skins.owned;
25
+ export const collector = skins.collector;
26
+ export const open = skins.open;
27
+ // bought just now (back from checkout, or a pasted order code): reload once so the new looks join the game
28
+ skins.onChange(() => { if (skins.owned() !== had[0] || skins.collector() !== had[1]) { try { sessionStorage.setItem('cmx-looks-open', '1'); } catch { /* */ } setTimeout(() => location.reload(), 1800); } });
29
+ // after that reload, show the pack sheet once: it lists the new looks and where to pick them
30
+ try { if (sessionStorage.getItem('cmx-looks-open')) { sessionStorage.removeItem('cmx-looks-open'); setTimeout(() => skins.open('bought'), 400); } } catch { /* */ }
31
+
32
+ // apply the chosen look (stored as hushfox:look) and keep it to looks the player owns
33
+ const pick = (id) => { try { localStorage.setItem('hushfox:look', JSON.stringify(id)); } catch { /* private mode */ } document.documentElement.dataset.hx = id === 'classic' ? '' : id; };
34
+ const cur = () => { try { return JSON.parse(localStorage.getItem('hushfox:look') || '"classic"'); } catch { return 'classic'; } };
35
+ if (typeof document !== 'undefined') document.documentElement.dataset.hx = LOOKS[cur()] ? cur() : '';
offer.js CHANGED
@@ -1,2 +1,2 @@
1
  // Written by scripts/games/sync_kit.py from products/fun-pass/games.json; do not edit by hand. (c) 2026 CyberMax.
2
- export const OFFER = {"game": "hushfox", "name": "Hushfox", "price": "$3.99", "live": false, "demo": 20, "games": 10};
 
1
  // Written by scripts/games/sync_kit.py from products/fun-pass/games.json; do not edit by hand. (c) 2026 CyberMax.
2
+ export const OFFER = {"game": "hushfox", "name": "Hushfox", "price": "$3.99", "live": true, "demo": 20, "games": 20, "levels": true, "shop": [{"offer": "st-hushfox-skins-pack", "kind": "skins", "name": "Theme Pack", "what": "6 themes: Mint, Rose, Ocean, Gold, Noir and Sunset. Looks only.", "cents": 199, "price": "$1.99", "per": "", "icon": "🦊", "sw": ["#2FD3A0", "#FF5C8A", "#3AB8FF"], "store": "https://cybermaxtools.com/store/hushfox-theme-pack/", "img": "https://cybermaxtools.com/store/img/hushfox-theme-pack-icon.webp"}, {"offer": "st-fun-pass-monthly", "kind": "pass", "name": "Fun Pass", "what": "Every paid level, mode, past daily and theme in every CyberMax game. Cancel any time.", "cents": 299, "price": "$2.99", "per": "/month", "icon": "✨", "sw": ["#FFB547", "#FF6A3D"], "store": "https://cybermaxtools.com/store/fun-pass/", "img": "https://cybermaxtools.com/store/img/fun-pass-icon.webp", "alt": [{"offer": "st-fun-pass-yearly", "label": "$19/year"}, {"offer": "st-fun-pass-unlock", "label": "$29.99 for life"}]}, {"offer": "st-hushfox-full-unlock", "kind": "full", "name": "Full game", "what": "8 extra word packs (Grown-ups night, Movie night, 4 languages and more) and Fox Hunt levels 21-30. One payment, no subscription.", "cents": 399, "price": "$3.99", "per": "", "icon": "🔓", "sw": ["#1C1530", "#1C1530"], "store": "https://cybermaxtools.com/store/hushfox-full-game/", "img": "https://cybermaxtools.com/store/img/hushfox-full-game-icon.webp"}, {"offer": "st-hushfox-collector-pack", "kind": "collector", "name": "Collector Pack", "what": "All 10 themes: the 6 in the Theme Pack plus Aurora Night, Lava Lamp, Disco and Galaxy (animated), only in this pack. Looks only.", "cents": 499, "price": "$4.99", "per": "", "icon": "💎", "sw": ["#45E0B4", "#FF5E3A", "#FF4FD8"], "store": "https://cybermaxtools.com/store/hushfox-collector-pack/", "img": "https://cybermaxtools.com/store/img/hushfox-collector-pack-icon.webp"}, {"offer": "st-arcade-lifetime-lifetime", "kind": "arcade", "name": "Arcade Lifetime", "what": "One payment for every game pack, now and later: the Fun Pass for life plus every Skin, Look and Founder Pack and Campaign+. Coin and crown packs not included.", "cents": 3999, "price": "$39.99", "per": "", "icon": "🕹️", "sw": ["#5CF2FF", "#B26BFF"], "store": "https://cybermaxtools.com/store/arcade-lifetime/", "img": "https://cybermaxtools.com/store/img/arcade-lifetime-icon.webp"}], "shopFrom": "$1.99", "shopResults": "🎨 New looks →", "shopStore": "https://cybermaxtools.com/store/hushfox-theme-pack/", "shopPass": true};
skins.js ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // CyberMax game Skin Pack: a one-time cosmetic unlock for ONE game (separate from the Fun Pass, which covers every
2
+ // game's archive + themes). Buy: counted redirect data.cybermaxtools.com/buy/st-<game>-skins-pack?s=game-<game>
3
+ // -> Stripe Payment Link; or the Gumroad listing (licence key). After checkout data.cybermaxtools.com/unlock/<game>-skins
4
+ // checks the order (or the Gumroad licence key) and returns to the game with ?unlock=skins#cmx-pro=<code>.
5
+ // capture() must run BEFORE funpass.init() so the Fun Pass never mistakes a skin-pack code for a pass code.
6
+ // Founder Pack (price ladder, 29 Oct 2026; optional, capture({ founder: { price, look } })): one payment for the full game,
7
+ // every Skin Pack look and one Founder-only look. The data Worker returns with ?unlock=founder#cmx-pro=<code>; the code
8
+ // is checked at /unlock/<game>-founder (a CyberMax Arcade Lifetime code passes too) and also turns on the full game.
9
+ // Cosmetic only: skins never change the game, scores or the daily course.
10
+ // v2 (30 Oct 2026, $5 Store lane): optional label ('Theme Pack', 'Looks Pack'), unit ('themes', 'looks'), an in-dialog
11
+ // picker for games with no look picker of their own (cfg.picker), and window.cmxSkinPack = { open, owned, label, price }
12
+ // so a game's shop sheet can open this sheet without importing the module. Same checkout, unlock and codes as v1.
13
+ // Collector tier (optional, capture({ collector: { price, extra, names } })): every pack look plus collector-only looks,
14
+ // looks only (no game content). Buy /buy/st-<game>-collector-pack; the data Worker returns with ?unlock=collector#cmx-pro=<code>
15
+ // after checking /unlock/<game>-collector (a CyberMax Arcade Lifetime code passes too). (c) 2026 CyberMax.
16
+ import { count, store, toast } from './cmx.js';
17
+
18
+ const DATA = 'https://data.cybermaxtools.com';
19
+ const CODE = /^(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;
20
+ let cfg = { game: '', name: '', price: '$1.99', gumroad: '', skins: 0, label: 'Skin Pack', unit: 'skins', picker: null };
21
+ let state = null;
22
+ let fstate = null;
23
+ let cstate = null;
24
+ const subs = [];
25
+ const key = () => `cmx-skins:${cfg.game}`;
26
+ const fkey = () => `cmx-founder:${cfg.game}`;
27
+ const ckey = () => `cmx-collector:${cfg.game}`;
28
+
29
+ export const owned = () => !!(state && state.code);
30
+ /** True when this player owns the Founder Pack (or the CyberMax Arcade Lifetime): the Founder-only look is theirs. */
31
+ export const founder = () => !!(fstate && fstate.code);
32
+ /** True when this player owns the Collector Pack (or the CyberMax Arcade Lifetime): the collector-only looks are theirs. */
33
+ export const collector = () => !!(cstate && cstate.code);
34
+ export function onChange(fn) { subs.push(fn); }
35
+ const emit = () => subs.forEach((f) => { try { f(owned()); } catch { /* game hook */ } });
36
+
37
+ // Offline Pro keys (17 Oct 2026): "cmxk1.<payload>.<Ed25519 signature>" from data.cybermaxtools.com/unlock is verified here
38
+ // with WebCrypto and this public key, so an unlock needs no Worker call. Expired, foreign-signed or unverifiable keys fall
39
+ // back to the online check of the order code inside them, as before.
40
+ const CMX_PUB = 'u0FDxbu0d9B9Sz5-yLT6AuL5IagyTlFcbBgEDqZxWtk';
41
+ const b64d = (s) => { s = s.replace(/-/g, '+').replace(/_/g, '/'); while (s.length % 4) s += '='; return Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); };
42
+ async function cmxKeyOk(k, app, online) {
43
+ const [tag, body, sig] = k.split('.');
44
+ let p;
45
+ try { p = JSON.parse(new TextDecoder().decode(b64d(body))); } catch { return false; }
46
+ if (!p || p.a !== app) return false;
47
+ try {
48
+ const key = await crypto.subtle.importKey('raw', b64d(CMX_PUB), { name: 'Ed25519' }, false, ['verify']);
49
+ if (await crypto.subtle.verify({ name: 'Ed25519' }, key, b64d(sig), new TextEncoder().encode(tag + '.' + body)) && (!p.x || p.x * 1000 > Date.now())) return true;
50
+ } catch { /* no Ed25519 here: online check below */ }
51
+ return p.c ? online(p.c) : false;
52
+ }
53
+ async function check(code, what = 'skins') {
54
+ if (/^cmxk1\./.test(code)) return cmxKeyOk(code, `${cfg.game}-${what}`, (c) => checkOnline(c, what));
55
+ return checkOnline(code, what);
56
+ }
57
+ async function checkOnline(code, what = 'skins') {
58
+ try {
59
+ const r = await fetch(`${DATA}/unlock/${cfg.game}-${what}?json=1&session_id=${encodeURIComponent(code)}`);
60
+ const j = await r.json();
61
+ return j && typeof j.pro === 'boolean' ? j.pro : null;
62
+ } catch { return null; }
63
+ }
64
+ export async function unlock(code) {
65
+ const ok = await check(code);
66
+ if (ok) {
67
+ state = store(key(), { code, at: new Date().toISOString() });
68
+ count(cfg.game, 'pro', 'skins-unlocked');
69
+ toast(`${cfg.name} ${cfg.label} is on. Enjoy the new looks!`, 3500);
70
+ emit(); render();
71
+ if (cfg.founder && !founder() && (await check(code, 'founder'))) setFounder(code); // a Founder code pasted in the Skin Pack box
72
+ if (cfg.collector && !collector() && (await check(code, 'collector'))) setCollector(code); // a Collector or Arcade code
73
+ } else if (ok === false) toast(`That code is not a ${cfg.label} order for this game.`, 3500);
74
+ else toast('Could not reach the unlock check. Try again in a minute.', 3500);
75
+ return ok;
76
+ }
77
+
78
+ function setFounder(code) {
79
+ const at = new Date().toISOString();
80
+ fstate = store(fkey(), { code, at });
81
+ state = state || store(key(), { code, at });
82
+ const hadFull = !!store(`cmx-full:${cfg.game}`);
83
+ if (!hadFull) store(`cmx-full:${cfg.game}`, { code, at }); // the full game (read by funpass.js on load)
84
+ count(cfg.game, 'pro', 'founder-unlocked');
85
+ toast(`${cfg.name} Founder Pack is on: the full game, every look and the ${cfg.founder.look} look. Thank you!`, 3500);
86
+ emit(); render();
87
+ if (!hadFull) setTimeout(() => location.reload(), 1600); // funpass.js reads the full game at start
88
+ }
89
+ function setCollector(code) {
90
+ const at = new Date().toISOString();
91
+ cstate = store(ckey(), { code, at });
92
+ state = state || store(key(), { code, at });
93
+ count(cfg.game, 'pro', 'collector-unlocked');
94
+ toast(`${cfg.name} Collector Pack is on: all ${cfg.skins + cfg.collector.extra} ${cfg.unit}. Thank you!`, 3500);
95
+ emit(); render();
96
+ }
97
+ /** Turn on a Collector Pack (or CyberMax Arcade Lifetime) code; a plain pack code falls back to the pack. */
98
+ export async function unlockCollector(code) {
99
+ const ok = await check(code, 'collector');
100
+ if (ok) setCollector(code);
101
+ else if (ok === false) { if (await check(code)) await unlock(code); else toast('That code is not a Collector Pack order for this game.', 3500); }
102
+ else toast('Could not reach the unlock check. Try again in a minute.', 3500);
103
+ return ok;
104
+ }
105
+ /** Turn on a Founder Pack (or CyberMax Arcade Lifetime) code. */
106
+ export async function unlockFounder(code) {
107
+ const ok = await check(code, 'founder');
108
+ if (ok) setFounder(code);
109
+ else if (ok === false) { if (await check(code)) await unlock(code); else toast('That code is not a Founder Pack order for this game.', 3500); }
110
+ else toast('Could not reach the unlock check. Try again in a minute.', 3500);
111
+ return ok;
112
+ }
113
+
114
+ /** Call first: game id, display name, skin count, Gumroad URL. Reads ?unlock=skins|founder#cmx-pro=<code> after checkout. */
115
+ export function capture(options) {
116
+ cfg = { ...cfg, ...options };
117
+ state = store(key());
118
+ fstate = cfg.founder ? store(fkey()) : null;
119
+ cstate = cfg.collector ? store(ckey()) : null;
120
+ const q = new URLSearchParams(location.search);
121
+ const m = /[#&]cmx-key=([^&]+)/.exec(location.hash || '') || /[#&]cmx-pro=([^&]+)/.exec(location.hash || '');
122
+ const u = q.get('unlock');
123
+ if ((u === 'skins' || (u === 'founder' && cfg.founder) || (u === 'collector' && cfg.collector)) && m) {
124
+ const code = decodeURIComponent(m[1]);
125
+ q.delete('unlock');
126
+ history.replaceState(null, '', location.pathname + (q.toString() ? `?${q}` : ''));
127
+ if (CODE.test(code)) (u === 'founder' ? unlockFounder : u === 'collector' ? unlockCollector : unlock)(code);
128
+ }
129
+ if (typeof window !== 'undefined') window.cmxSkinPack = { open: (from) => open(from || 'shop'), owned, collector, label: cfg.label, price: cfg.price, game: cfg.game,
130
+ // the price ladder a shop sheet can list (cheap -> mid -> premium); the URLs are counted /buy/ links
131
+ ladder: [{ id: 'pack', name: `${cfg.name} ${cfg.label}`, price: cfg.price, buy: buyUrl('shop') }].concat(cfg.collector ? [{ id: 'collector', name: `${cfg.name} Collector Pack`, price: cfg.collector.price, buy: collectorUrl() }] : [], cfg.founder ? [{ id: 'founder', name: `${cfg.name} Founder Pack`, price: cfg.founder.price, buy: founderUrl() }] : [], [{ id: 'arcade', name: 'CyberMax Arcade Lifetime', price: '$39.99', buy: arcadeUrl() }]) };
132
+ }
133
+
134
+ const buyUrl = (from) => `${DATA}/buy/st-${cfg.game}-skins-pack?s=game-${cfg.game}${from ? `-${from}` : ''}`;
135
+ const founderUrl = () => `${DATA}/buy/st-${cfg.game}-founder-pack?s=game-${cfg.game}-founder`;
136
+ const collectorUrl = () => `${DATA}/buy/st-${cfg.game}-collector-pack?s=game-${cfg.game}-collector`;
137
+ const arcadeUrl = () => `${DATA}/buy/st-arcade-lifetime-lifetime?s=game-${cfg.game}-arcade`;
138
+ // price ladder, one quiet line each: pack -> Collector Pack -> CyberMax Arcade Lifetime (every pack in every game)
139
+ const collectorLine = () => (cfg.collector && !collector() ? `<p class="fp-small">Want more? The <b>Collector Pack</b>, ${cfg.collector.price} once: all ${cfg.skins} ${cfg.unit} plus ${cfg.collector.extra} collector-only ${cfg.unit}${cfg.collector.names ? ` (${cfg.collector.names})` : ''}. <a href="${collectorUrl()}" rel="nofollow" data-collector>Get the Collector Pack</a></p>` : '');
140
+ const arcadeLine = () => (cfg.collector && !collector() ? `<p class="fp-small">Or every pack in every CyberMax game, now and later: <b>Arcade Lifetime</b>, $39.99 once. <a href="${arcadeUrl()}" rel="nofollow" data-arcade>See Arcade Lifetime</a></p>` : '');
141
+ // one quiet line under the Skin Pack offer (never a second popup)
142
+ const founderLine = () => (cfg.founder && !founder() ? `<p class="fp-small">Or the <b>Founder Pack</b>, ${cfg.founder.price} once: the full game, every look here, the Founder-only ${cfg.founder.look} look and phone + desktop wallpapers. <a href="${founderUrl()}" rel="nofollow" data-founder>Get the Founder Pack</a></p>` : '');
143
+ function dialog() {
144
+ let d = document.getElementById('skinpack');
145
+ if (!d) {
146
+ d = document.createElement('dialog'); d.id = 'skinpack'; d.className = 'funpass';
147
+ document.body.appendChild(d);
148
+ d.addEventListener('click', (e) => { if (e.target === d || e.target.closest('[data-close]')) d.close(); });
149
+ }
150
+ return d;
151
+ }
152
+ function render() {
153
+ const d = dialog();
154
+ const prev = (cfg.preview || []).map((s) => `<span class="sp-dot" style="background:${s}"></span>`).join('');
155
+ if (owned()) {
156
+ d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${cfg.name} ${cfg.label}</p><h2>All ${cfg.skins + (collector() ? cfg.collector.extra : 0)} ${cfg.unit} are yours. Thank you!</h2>
157
+ ${cfg.picker ? `<p>${cfg.picker.label || 'Pick a look'}:</p><p class="fp-row sp-pick">${(typeof cfg.picker.items === 'function' ? cfg.picker.items() : cfg.picker.items).map((x) => `<button type="button" class="btn ghost sm" data-look="${x.id}" aria-pressed="${String(cfg.picker.current() === x.id)}"><span class="sp-dot" style="background:${x.swatch}"></span> ${x.name}</button>`).join('')}</p>` : `<p>${cfg.where || 'Pick one from the colour dots under the game.'}</p>`}${founderLine()}${collectorLine()}${arcadeLine()}<p class="fp-small">Your code (paste it on another device):</p><code class="fp-code">${state.code}</code></div>`;
158
+ d.querySelector('[data-collector]')?.addEventListener('click', () => count(cfg.game, 'cta', 'collector-buy'));
159
+ d.querySelector('[data-arcade]')?.addEventListener('click', () => count(cfg.game, 'cta', 'arcade-buy'));
160
+ d.querySelectorAll('[data-look]').forEach((b) => b.addEventListener('click', () => { cfg.picker.pick(b.dataset.look); count(cfg.game, 'pro', 'look-picked'); render(); }));
161
+ return;
162
+ }
163
+ d.innerHTML = `<div class="fp"><button class="fp-x" data-close aria-label="Close">×</button><p class="fp-kick">${cfg.name} ${cfg.label}</p>
164
+ <h2>${cfg.skins} extra ${cfg.unit}, ${cfg.price} once</h2><p class="sp-row">${prev}</p>
165
+ <ul class="fp-perks">${(cfg.perks || []).map((p) => `<li>${p}</li>`).join('')}<li>Cosmetic only: same course, same scores for everyone</li><li>Pay once, keep forever, on any device with your code</li></ul>
166
+ <p class="fp-row"><a class="btn pass" href="${buyUrl('')}" rel="nofollow" data-buy>Get the ${cfg.label}: ${cfg.price}</a>${cfg.gumroad ? ` <a class="btn ghost sm" href="${cfg.gumroad}" rel="nofollow noopener" target="_blank" data-gum>or on Gumroad</a>` : ''}</p>
167
+ ${founderLine()}${collectorLine()}${arcadeLine()}<p class="fp-small">Secure checkout by Stripe (shown as Link) · the ${cfg.unit} turn on as soon as you pay · prices in USD, + tax where applicable · 14-day promise: if it doesn't work as described, we fix it or refund you (<a href="https://cybermaxtools.com/terms#payments" target="_blank" rel="noopener">terms</a>). The <b>Fun Pass</b> is a separate pass for every game's archive and themes.</p>
168
+ <details><summary>Already bought it?</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></div>`;
169
+ d.querySelector('.fp-form').addEventListener('submit', (e) => {
170
+ e.preventDefault();
171
+ const c = e.target.querySelector('input').value.trim();
172
+ if (CODE.test(c)) (cfg.founder ? unlockFounder : cfg.collector ? unlockCollector : unlock)(c); else toast('Paste the code from your receipt page (cs_live_…) or your Gumroad licence key.', 3500);
173
+ });
174
+ d.querySelector('[data-buy]').addEventListener('click', () => count(cfg.game, 'cta', 'skins-buy'));
175
+ d.querySelector('[data-gum]')?.addEventListener('click', () => count(cfg.game, 'cta', 'skins-gumroad'));
176
+ d.querySelector('[data-founder]')?.addEventListener('click', () => count(cfg.game, 'cta', 'founder-buy'));
177
+ d.querySelector('[data-collector]')?.addEventListener('click', () => count(cfg.game, 'cta', 'collector-buy'));
178
+ d.querySelector('[data-arcade]')?.addEventListener('click', () => count(cfg.game, 'cta', 'arcade-buy'));
179
+ }
180
+ export function open(from = 'button') {
181
+ render();
182
+ const d = dialog();
183
+ if (d.showModal) d.showModal(); else d.setAttribute('open', '');
184
+ count(cfg.game, 'cta', `skins-open-${from}`);
185
+ }
186
+
187
+ // Direct checkout (17 Oct 2026, Workers cap fix): a click on a data.cybermaxtools.com/buy/<offer> link goes straight to the
188
+ // Stripe/Polar checkout (static map https://cybermaxtools.com/store/buy-direct.json); the click is counted by a beacon.
189
+ (() => {
190
+ if (typeof document === 'undefined' || window.__cmxBd) return;
191
+ window.__cmxBd = 1;
192
+ let MAP = null;
193
+ try { MAP = JSON.parse(sessionStorage.getItem('cmx-bd') || 'null'); } catch { /* none */ }
194
+ 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(() => {});
195
+ document.addEventListener('click', (e) => {
196
+ const a = e.target && e.target.closest && e.target.closest('a[href*="data.cybermaxtools.com/buy/"]');
197
+ if (!a || !MAP || !MAP.offers) return;
198
+ try {
199
+ const u = new URL(a.href);
200
+ const k = /^\/buy\/([a-z0-9-]+)\/?$/.exec(u.pathname);
201
+ const t = k && MAP.offers[k[1]];
202
+ if (!t) return;
203
+ const s = String(u.searchParams.get('s') || 'unknown').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 80) || 'unknown';
204
+ a.href = t.split(MAP.ph).join(s);
205
+ if (!navigator.webdriver) navigator.sendBeacon(u.href + (u.search ? '&' : '?') + 'b=1');
206
+ } catch { /* keep the counted link */ }
207
+ }, true);
208
+ })();
style.css CHANGED
@@ -1,4 +1,5 @@
1
  /* Hushfox layout: big, readable party screens for a phone passed around a room. Colours: plum night + fox orange.
 
2
  (c) 2026 CyberMax. All rights reserved. */
3
  :root{--acc:#FF7A2F;--acc-ink:#1C1530;--fox:#FF7A2F;--night:#1C1530;--night2:#2B2048;--cream:#FFF3E6}
4
  .headline h1 .num{color:var(--muted);font-weight:700;font-size:.62em}
 
1
  /* Hushfox layout: big, readable party screens for a phone passed around a room. Colours: plum night + fox orange.
2
+ :root{--game:#FF6A3D;--game2:#8E6CFF}
3
  (c) 2026 CyberMax. All rights reserved. */
4
  :root{--acc:#FF7A2F;--acc-ink:#1C1530;--fox:#FF7A2F;--night:#1C1530;--night2:#2B2048;--cream:#FFF3E6}
5
  .headline h1 .num{color:var(--muted);font-weight:700;font-size:.62em}
sw.js CHANGED
@@ -1,8 +1,8 @@
1
  // CyberMax game service worker: the game works offline after the first visit. Cache-first for the game's own files,
2
  // network-first for the page itself (so updates arrive), never caches API calls or other sites. (c) 2026 CyberMax.
3
  const GAME = 'hushfox';
4
- const CACHE = `cmx-${GAME}-250a33fa82`;
5
- const FILES = ["index.html", "cmx.js", "core.js", "fun.css", "funpass.js", "game.js", "icon-192.png", "icon-512.png", "icon-maskable.png", "icon.svg", "index.html", "levels.js", "manifest.webmanifest", "net.js", "offer.js", "packs.js", "safety.js", "style.css", "words.js"];
6
 
7
  self.addEventListener('install', (e) => {
8
  e.waitUntil(caches.open(CACHE).then((c) => c.addAll(FILES)).then(() => self.skipWaiting()));
 
1
  // CyberMax game service worker: the game works offline after the first visit. Cache-first for the game's own files,
2
  // network-first for the page itself (so updates arrive), never caches API calls or other sites. (c) 2026 CyberMax.
3
  const GAME = 'hushfox';
4
+ const CACHE = `cmx-${GAME}-ad64dd6ee3`;
5
+ const FILES = ["index.html", "cmx.js", "core.js", "fun.css", "funpass.js", "game.js", "icon-192.png", "icon-512.png", "icon-maskable.png", "icon.svg", "index.html", "levels.js", "looks.js", "manifest.webmanifest", "net.js", "offer.js", "packs.js", "safety.js", "skins.js", "style.css", "words.js"];
6
 
7
  self.addEventListener('install', (e) => {
8
  e.waitUntil(caches.open(CACHE).then((c) => c.addAll(FILES)).then(() => self.skipWaiting()));