naider 1.24.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/gen.js CHANGED
@@ -100,6 +100,20 @@ const GAME_TYPES = {
100
100
  reaction: ['reaction', 'reflex', 'リアクション', '反射', '反応'],
101
101
  snake: ['snake', 'スネーク', 'ヘビ'],
102
102
  breakout: ['breakout', 'brick', 'block', 'ブロック崩し', 'ブレイクアウト'],
103
+ dodge: ['dodge', 'avoid', 'escape', '避け', 'ドッジ', '回避'],
104
+ color: ['color', 'colour', 'stroop', 'カラー', '色'],
105
+ pong: ['pong', 'paddle', 'ポン', 'パドル', 'ピンポン'],
106
+ };
107
+
108
+ const APP_TYPES = {
109
+ pomodoro: ['pomodoro', 'ポモドーロ'],
110
+ calculator: ['calculator', 'calc', '計算', '計算機', '電卓'],
111
+ drawing: ['drawing', 'draw', 'paint', 'sketch', 'お絵描き', 'ペイント', '落書き'],
112
+ timer: ['timer', 'stopwatch', 'タイマー', 'ストップウォッチ'],
113
+ counter: ['counter', 'カウンター', 'カウント'],
114
+ todo: ['todo', 'todos', 'タスク', 'やること', 'やることリスト'],
115
+ clock: ['clock', 'watch', '時計', 'クロック'],
116
+ notes: ['notes', 'note', 'memo', 'メモ', 'ノート'],
103
117
  };
104
118
 
105
119
  const NOISE = new Set([
@@ -111,6 +125,9 @@ const NOISE = new Set([
111
125
  'game', 'play', 'player', 'score', 'level', 'arcade', 'highscore',
112
126
  'tapping', 'tap', 'clicker', 'click', 'quiz', 'trivia', 'memory', 'matching', 'typing',
113
127
  'reaction', 'reflex', 'snake', 'breakout', 'brick', 'block', 'pong', 'puzzle',
128
+ 'dodge', 'avoid', 'escape', 'color', 'colour', 'stroop', 'paddle',
129
+ 'calculator', 'calc', 'timer', 'stopwatch', 'counter', 'pomodoro',
130
+ 'clock', 'watch', 'drawing', 'draw', 'paint', 'sketch', 'notes', 'note', 'memo', 'app',
114
131
  'の', 'を', 'に', 'で', 'は', 'が', 'と', 'も', 'から', 'まで', 'より', 'って',
115
132
  'な', 'ような', 'みたいな', 'っぽい', 'ような感じ',
116
133
  '作って', '作る', '作成', '生成', '追加', '実装', 'ください', 'して', 'つけて',
@@ -129,6 +146,16 @@ function detectGameType(words, raw) {
129
146
  return 'tapping';
130
147
  }
131
148
 
149
+ function detectAppType(words, raw) {
150
+ const lower = raw.toLowerCase();
151
+ for (const [type, kws] of Object.entries(APP_TYPES)) {
152
+ for (const kw of kws) {
153
+ if (lower.includes(kw)) return type;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
132
159
  // ── Field Normalization ─────────────────────────────────────
133
160
 
134
161
  const FIELD_SYNONYMS = {
@@ -524,6 +551,21 @@ function classify(words, raw) {
524
551
  mods.isGame = true;
525
552
  }
526
553
 
554
+ // App intent: standalone frontend apps
555
+ if (!mods.isGame) {
556
+ const appType = detectAppType(words, raw);
557
+ if (appType) {
558
+ const strongServerKw = ['server', 'api', 'rest', 'restful', 'endpoint', 'backend', 'service', 'microservice', 'http', 'web'];
559
+ const hasStrongServer = strongServerKw.some(k => lower.includes(k));
560
+ if (!hasStrongServer && !intents.has('crud') && !intents.has('database') && !intents.has('schema') && !intents.has('auth')) {
561
+ intents.delete('server');
562
+ mods.appType = appType;
563
+ mods.isApp = true;
564
+ intents.add('app');
565
+ }
566
+ }
567
+ }
568
+
527
569
  // Default fallback: if nothing detected, server + schema
528
570
  if (intents.size === 0) {
529
571
  intents.add('server'); intents.add('schema'); intents.add('crud'); intents.add('database');
@@ -544,6 +586,12 @@ function extractParams(words, raw, intents, mods) {
544
586
  return params;
545
587
  }
546
588
 
589
+ if (mods.isApp) {
590
+ params.appType = mods.appType;
591
+ params.appTitle = raw.replace(/\b(make|create|build|generate|simple|basic|new|a|an|the)\b/gi, '').trim();
592
+ return params;
593
+ }
594
+
547
595
  // Extract port number
548
596
  for (let i = 0; i < words.length; i++) {
549
597
  const n = parseInt(words[i]);
@@ -1781,19 +1829,30 @@ function migrationFile(entities, relationships, m2mRels) {
1781
1829
 
1782
1830
  // ── Code Composer ───────────────────────────────────────────
1783
1831
 
1784
- // ── Game Engine (compositional) ──────────────────────────
1785
-
1786
- const GAME_PALETTES = {
1787
- tapping: { bg:'#1a0a2e', sf:'#2d1a42', pr:'#FF6B35', sc:'#FF3864', ac:'#FFD700', tx:'#fff', mt:'#a89cc8' },
1788
- quiz: { bg:'#0d1b2a', sf:'#1b2838', pr:'#3A86FF', sc:'#8338EC', ac:'#06D6A0', tx:'#e8ecf1', mt:'#7b8ca3' },
1789
- memory: { bg:'#1a0f25', sf:'#2d1f3d', pr:'#C77DFF', sc:'#E05297', ac:'#FF6B9D', tx:'#f5f0ff', mt:'#9b8bb4' },
1790
- snake: { bg:'#071a0e', sf:'#0f2918', pr:'#4ADE80', sc:'#22D3EE', ac:'#FCD34D', tx:'#e8f5e9', mt:'#6b8a7a' },
1791
- typing: { bg:'#0a192f', sf:'#112240', pr:'#64FFDA', sc:'#00B4D8', ac:'#CCD6F6', tx:'#CCD6F6', mt:'#5c7a8a' },
1792
- reaction: { bg:'#18181b', sf:'#27272a', pr:'#F87171', sc:'#EF4444', ac:'#4ADE80', tx:'#fafafa', mt:'#71717a' },
1793
- breakout: { bg:'#0c0a1d', sf:'#1c1833', pr:'#A78BFA', sc:'#8B5CF6', ac:'#F59E0B', tx:'#f0f0ff', mt:'#8f85a3' },
1832
+ // ── Frontend Code Engine (module-based) ──────────────────
1833
+
1834
+ const THEMES = {
1835
+ tapping: { bg:'#1a0a2e', sf:'#2d1a42', pr:'#FF6B35', sc:'#FF3864', ac:'#FFD700', tx:'#fff', mt:'#a89cc8' },
1836
+ quiz: { bg:'#0d1b2a', sf:'#1b2838', pr:'#3A86FF', sc:'#8338EC', ac:'#06D6A0', tx:'#e8ecf1', mt:'#7b8ca3' },
1837
+ memory: { bg:'#1a0f25', sf:'#2d1f3d', pr:'#C77DFF', sc:'#E05297', ac:'#FF6B9D', tx:'#f5f0ff', mt:'#9b8bb4' },
1838
+ snake: { bg:'#071a0e', sf:'#0f2918', pr:'#4ADE80', sc:'#22D3EE', ac:'#FCD34D', tx:'#e8f5e9', mt:'#6b8a7a' },
1839
+ typing: { bg:'#0a192f', sf:'#112240', pr:'#64FFDA', sc:'#00B4D8', ac:'#CCD6F6', tx:'#CCD6F6', mt:'#5c7a8a' },
1840
+ reaction: { bg:'#18181b', sf:'#27272a', pr:'#F87171', sc:'#EF4444', ac:'#4ADE80', tx:'#fafafa', mt:'#71717a' },
1841
+ breakout: { bg:'#0c0a1d', sf:'#1c1833', pr:'#A78BFA', sc:'#8B5CF6', ac:'#F59E0B', tx:'#f0f0ff', mt:'#8f85a3' },
1842
+ dodge: { bg:'#1a0005', sf:'#2d0a12', pr:'#FF4757', sc:'#FF6B81', ac:'#2ED573', tx:'#fff', mt:'#a88a8f' },
1843
+ color: { bg:'#0a1628', sf:'#132240', pr:'#FF6348', sc:'#FFA502', ac:'#7BED9F', tx:'#f5f6fa', mt:'#778ca3' },
1844
+ pong: { bg:'#0a0a0a', sf:'#1a1a2e', pr:'#00FF87', sc:'#60EFFF', ac:'#FFE66D', tx:'#f0f0f0', mt:'#666680' },
1845
+ calculator: { bg:'#1a1a2e', sf:'#25254a', pr:'#667eea', sc:'#764ba2', ac:'#f093fb', tx:'#e8e8ff', mt:'#8888aa' },
1846
+ timer: { bg:'#0f172a', sf:'#1e293b', pr:'#38bdf8', sc:'#818cf8', ac:'#34d399', tx:'#f1f5f9', mt:'#64748b' },
1847
+ counter: { bg:'#18181b', sf:'#27272a', pr:'#a78bfa', sc:'#c084fc', ac:'#fbbf24', tx:'#fafafa', mt:'#71717a' },
1848
+ todo: { bg:'#fafaf9', sf:'#f5f5f4', pr:'#4f46e5', sc:'#7c3aed', ac:'#10b981', tx:'#1c1917', mt:'#78716c' },
1849
+ pomodoro: { bg:'#1a0000', sf:'#2d0a0a', pr:'#ef4444', sc:'#f97316', ac:'#22c55e', tx:'#fff', mt:'#a89c8c' },
1850
+ clock: { bg:'#020617', sf:'#0f172a', pr:'#e2e8f0', sc:'#94a3b8', ac:'#38bdf8', tx:'#f8fafc', mt:'#475569' },
1851
+ drawing: { bg:'#ffffff', sf:'#f3f4f6', pr:'#3b82f6', sc:'#8b5cf6', ac:'#ef4444', tx:'#111827', mt:'#9ca3af' },
1852
+ notes: { bg:'#fffbeb', sf:'#fef3c7', pr:'#f59e0b', sc:'#d97706', ac:'#059669', tx:'#1c1917', mt:'#92400e' },
1794
1853
  };
1795
1854
 
1796
- function extractGameOpts(raw) {
1855
+ function extractBuildOpts(raw) {
1797
1856
  const o = {};
1798
1857
  const d = raw.match(/(\d+)\s*(?:sec|seconds|秒|s\b)/i);
1799
1858
  if (d) o.duration = Math.min(120, Math.max(3, parseInt(d[1])));
@@ -1802,42 +1861,177 @@ function extractGameOpts(raw) {
1802
1861
  if (/fast|高速|はやい/i.test(raw)) o.speed = 'fast';
1803
1862
  if (/hard|難しい|ハード/i.test(raw)) o.difficulty = 'hard';
1804
1863
  if (/easy|簡単|イージー/i.test(raw)) o.difficulty = 'easy';
1864
+ if (/dark|ダーク|暗い/i.test(raw)) o.dark = true;
1865
+ if (/sound|sfx|音|サウンド/i.test(raw)) o.sfx = true;
1866
+ if (/history|履歴|ヒストリー/i.test(raw)) o.history = true;
1867
+ if (/category|カテゴリ|分類/i.test(raw)) o.categories = true;
1805
1868
  return o;
1806
1869
  }
1807
1870
 
1808
- function cssVars(p) {
1809
- return `:root{--bg:${p.bg};--sf:${p.sf};--pr:${p.pr};--sc:${p.sc};--ac:${p.ac};--tx:${p.tx};--mt:${p.mt}}`;
1810
- }
1871
+ // ── CSS Modules ──
1811
1872
 
1812
- function cssBase(p) {
1873
+ function cssBase(t) {
1813
1874
  return `*{margin:0;padding:0;box-sizing:border-box}
1814
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:${p.bg};color:${p.tx};min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
1815
- .ct{width:min(420px,92vw);text-align:center}
1816
- h1{font-size:1.8rem;margin-bottom:.5rem;background:linear-gradient(135deg,${p.pr},${p.sc});-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
1817
- .sc{font-size:3rem;font-weight:800;margin:.5rem 0;font-variant-numeric:tabular-nums}
1818
- .btn{display:inline-block;padding:.75rem 2rem;border:none;border-radius:12px;font-size:1.1rem;font-weight:600;cursor:pointer;transition:transform .1s,box-shadow .2s;color:#fff;background:linear-gradient(135deg,${p.pr},${p.sc})}
1819
- .btn:active{transform:scale(.95)}.btn:hover{box-shadow:0 0 24px ${p.pr}66}
1820
- .st{display:flex;gap:1.5rem;justify-content:center;margin:.75rem 0;font-size:.9rem;color:${p.mt}}
1821
- .st span{display:flex;flex-direction:column;align-items:center;gap:.2rem}.st b{color:${p.tx};font-size:1.1rem}
1822
- .hd{display:none!important}.hi{color:${p.ac}}
1875
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:${t.bg};color:${t.tx};min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow-x:hidden;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
1876
+ .ct{width:min(420px,92vw);text-align:center;position:relative}
1877
+ h1{font-size:1.8rem;margin-bottom:.5rem;background:linear-gradient(135deg,${t.pr},${t.sc});-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}`;
1878
+ }
1879
+
1880
+ function cssBtn(t) {
1881
+ return `.btn{display:inline-block;padding:.75rem 2rem;border:none;border-radius:12px;font-size:1.1rem;font-weight:600;cursor:pointer;transition:transform .1s,box-shadow .2s;color:#fff;background:linear-gradient(135deg,${t.pr},${t.sc})}
1882
+ .btn:active{transform:scale(.95)}.btn:hover{box-shadow:0 0 24px ${t.pr}66}`;
1883
+ }
1884
+
1885
+ function cssScr() {
1886
+ return `.scr{display:none}.scr.on{display:block}`;
1887
+ }
1888
+
1889
+ function cssDsp() {
1890
+ return `.dsp{font-size:3rem;font-weight:800;margin:.5rem 0;font-variant-numeric:tabular-nums}`;
1891
+ }
1892
+
1893
+ function cssStat(t) {
1894
+ return `.st{display:flex;gap:1.5rem;justify-content:center;margin:.75rem 0;font-size:.9rem;color:${t.mt}}
1895
+ .st span{display:flex;flex-direction:column;align-items:center;gap:.2rem}.st b{color:${t.tx};font-size:1.1rem}`;
1896
+ }
1897
+
1898
+ function cssBar(t) {
1899
+ return `.bar{width:100%;height:6px;background:${t.sf};border-radius:3px;margin:.5rem 0;overflow:hidden}
1900
+ .bar .fl{height:100%;background:linear-gradient(90deg,${t.ac},${t.pr});transition:width .1s linear;border-radius:3px}`;
1901
+ }
1902
+
1903
+ function cssInput(t) {
1904
+ return `.input{background:${t.sf};border:2px solid ${t.pr}44;border-radius:12px;padding:.75rem 1rem;font-size:1.1rem;color:${t.tx};width:100%;outline:none;font-family:inherit;transition:border-color .2s}
1905
+ .input:focus{border-color:${t.pr}}`;
1906
+ }
1907
+
1908
+ function cssList(t) {
1909
+ return `.list{display:flex;flex-direction:column;gap:8px;margin:1rem 0;text-align:left;max-height:60vh;overflow-y:auto}
1910
+ .list-item{padding:.75rem 1rem;background:${t.sf};border-radius:10px;display:flex;align-items:center;gap:.75rem;transition:opacity .3s,transform .2s;animation:fadeIn .2s ease-out}
1911
+ .list-item.done{opacity:.5;text-decoration:line-through}
1912
+ .list-item .del{margin-left:auto;cursor:pointer;opacity:.4;transition:opacity .2s;background:none;border:none;color:${t.tx};font-size:1.2rem}.list-item .del:hover{opacity:1}`;
1913
+ }
1914
+
1915
+ function cssFx(t) {
1916
+ return `.cdwn{position:fixed;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:6rem;font-weight:900;color:${t.pr};z-index:99;pointer-events:none;animation:cdPop .6s ease-out}
1917
+ @keyframes cdPop{0%{transform:scale(3);opacity:0}30%{opacity:1}100%{transform:scale(1);opacity:0}}
1823
1918
  @keyframes pop{0%{transform:scale(1)}50%{transform:scale(1.15)}100%{transform:scale(1)}}
1824
- @keyframes fadeUp{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-60px)}}`;
1919
+ @keyframes fadeUp{0%{opacity:1;transform:translateY(0)}100%{opacity:0;transform:translateY(-60px)}}
1920
+ @keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
1921
+ @keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
1922
+ .pcv{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:90}
1923
+ .hi{color:${t.ac}}`;
1924
+ }
1925
+
1926
+ function cssCanvas(t) {
1927
+ return `canvas{border-radius:12px;background:${t.sf};display:block;margin:1rem auto;touch-action:none}`;
1928
+ }
1929
+
1930
+ function cssGrid(t, cols) {
1931
+ return `.grid{display:grid;grid-template-columns:repeat(${cols},1fr);gap:8px;margin:1rem auto;width:min(${cols*90}px,88vw)}`;
1932
+ }
1933
+
1934
+ function cssCards(t, cols) {
1935
+ return `.card{aspect-ratio:1;border-radius:12px;cursor:pointer;perspective:600px;-webkit-perspective:600px}
1936
+ .card .inn{position:relative;width:100%;height:100%;transition:transform .4s;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}
1937
+ .card.flip .inn{transform:rotateY(180deg)}
1938
+ .card .fr,.card .bk{position:absolute;width:100%;height:100%;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:${cols<=4?'2rem':'1.4rem'};backface-visibility:hidden;-webkit-backface-visibility:hidden}
1939
+ .card .fr{background:linear-gradient(135deg,${t.pr},${t.sc})}
1940
+ .card .bk{background:${t.sf};transform:rotateY(180deg);border:2px solid ${t.pr}}
1941
+ .card.done .fr{background:linear-gradient(135deg,${t.ac},#059669)}
1942
+ .card.done{pointer-events:none}`;
1943
+ }
1944
+
1945
+ // ── JS Modules ──
1946
+
1947
+ function jsHelper() {
1948
+ return `const $=id=>document.getElementById(id);`;
1825
1949
  }
1826
1950
 
1827
- function cssTimerBar(p) {
1828
- return `.bar{width:100%;height:6px;background:${p.sf};border-radius:3px;margin:.5rem 0;overflow:hidden}
1829
- .bar .fl{height:100%;background:linear-gradient(90deg,${p.ac},${p.pr});transition:width .1s linear;border-radius:3px}`;
1951
+ function jsScreen() {
1952
+ return `function show(id){document.querySelectorAll('.scr').forEach(s=>s.classList.remove('on'));$(id).classList.add('on')}`;
1830
1953
  }
1831
1954
 
1832
- function jsCore(storageKey) {
1833
- return `const $=id=>document.getElementById(id);
1834
- let best=+(localStorage.getItem('${storageKey}')||0);
1955
+ function jsStore(ns) {
1956
+ return `const _ns='${ns}';
1957
+ function sGet(k){try{return JSON.parse(localStorage.getItem(_ns+'_'+k))}catch(e){return null}}
1958
+ function sSet(k,v){localStorage.setItem(_ns+'_'+k,JSON.stringify(v))}
1959
+ function sDel(k){localStorage.removeItem(_ns+'_'+k)}`;
1960
+ }
1961
+
1962
+ function jsScore() {
1963
+ return `let _score=0,best=+(sGet('best')||0);
1835
1964
  if($('best'))$('best').textContent=best;
1836
- function show(s){document.querySelectorAll('[id^=s-]').forEach(e=>e.classList.add('hd'));$('s-'+s).classList.remove('hd')}
1837
- function saveBest(v){if(v>best){best=v;localStorage.setItem('${storageKey}',best);if($('best'))$('best').textContent=best;return true}return false}`;
1965
+ function addPts(n){_score+=n;return _score}
1966
+ function getScore(){return _score}
1967
+ function resetScore(){_score=0}
1968
+ function saveBest(v){if(v>best){best=v;sSet('best',best);if($('best'))$('best').textContent=best;return true}return false}`;
1969
+ }
1970
+
1971
+ function jsTimer() {
1972
+ return `let _tmrId;
1973
+ function startTimer(ms,onTick,onEnd){const t0=Date.now();(function lp(){const e=Date.now()-t0;if(e>=ms){onEnd();return}onTick(e,ms);_tmrId=requestAnimationFrame(lp)})()}
1974
+ function stopTimer(){cancelAnimationFrame(_tmrId)}`;
1975
+ }
1976
+
1977
+ function jsSfx() {
1978
+ return `const _ax=new(window.AudioContext||window.webkitAudioContext)();
1979
+ function sfx(freq,type,dur,vol){const o=_ax.createOscillator(),g=_ax.createGain();o.type=type||'sine';o.frequency.value=freq;g.gain.value=vol||.15;g.gain.exponentialRampToValueAtTime(.001,_ax.currentTime+(dur||.15));o.connect(g);g.connect(_ax.destination);o.start();o.stop(_ax.currentTime+(dur||.15))}
1980
+ function sfxTap(){sfx(800,'square',.06,.12)}
1981
+ function sfxHit(){sfx(520,'sine',.1,.15)}
1982
+ function sfxScore(){sfx(660,'sine',.08,.12);setTimeout(()=>sfx(880,'sine',.08,.12),60)}
1983
+ function sfxWin(){[523,659,784,1047].forEach((f,i)=>setTimeout(()=>sfx(f,'sine',.2,.1),i*100))}
1984
+ function sfxFail(){sfx(200,'sawtooth',.3,.1)}
1985
+ function sfxTick(){sfx(1200,'sine',.03,.08)}
1986
+ function sfxCombo(){sfx(1000,'square',.05,.1);setTimeout(()=>sfx(1400,'square',.05,.1),50)}`;
1987
+ }
1988
+
1989
+ function jsParticles(color1, color2) {
1990
+ return `const _pcv=document.createElement('canvas');_pcv.className='pcv';document.body.appendChild(_pcv);const _pctx=_pcv.getContext('2d');
1991
+ let _pts=[];function _pResize(){_pcv.width=innerWidth;_pcv.height=innerHeight}window.addEventListener('resize',_pResize);_pResize();
1992
+ function emitParticles(x,y,count,c1,c2){for(let i=0;i<(count||12);i++){const a=Math.random()*Math.PI*2,sp=2+Math.random()*4;_pts.push({x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-2,life:1,sz:2+Math.random()*4,c:Math.random()>.5?(c1||'${color1}'):(c2||'${color2}')})}}
1993
+ function _pTick(){_pctx.clearRect(0,0,_pcv.width,_pcv.height);_pts=_pts.filter(p=>{p.x+=p.vx;p.y+=p.vy;p.vy+=.12;p.life-=.025;if(p.life<=0)return false;_pctx.globalAlpha=p.life;_pctx.fillStyle=p.c;_pctx.beginPath();_pctx.arc(p.x,p.y,p.sz*p.life,0,Math.PI*2);_pctx.fill();return true});_pctx.globalAlpha=1;requestAnimationFrame(_pTick)}_pTick();
1994
+ function burstAt(el,n){const r=el.getBoundingClientRect();emitParticles(r.left+r.width/2,r.top+r.height/2,n||16)}
1995
+ function shakeEl(el){el.style.animation='shake .2s';setTimeout(()=>el.style.animation='',200)}`;
1996
+ }
1997
+
1998
+ function jsCountdown(acColor) {
1999
+ return `function countdown(fn){let n=3;const t=()=>{if(n>0){const d=document.createElement('div');d.className='cdwn';d.textContent=n;document.body.appendChild(d);sfxTick();setTimeout(()=>d.remove(),600);n--;setTimeout(t,600)}else{const d=document.createElement('div');d.className='cdwn';d.textContent='GO!';d.style.color='${acColor}';document.body.appendChild(d);sfxScore();setTimeout(()=>d.remove(),500);fn()}};t()}`;
2000
+ }
2001
+
2002
+ function jsShuffle() {
2003
+ return `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`;
2004
+ }
2005
+
2006
+ // ── HTML Helpers ──
2007
+
2008
+ function htmlScr(id, content, active) {
2009
+ return `<div class="scr${active?' on':''}" id="${id}">${Array.isArray(content)?content.join('\n'):content}</div>`;
2010
+ }
2011
+
2012
+ function htmlBtn(text, onclick) {
2013
+ return `<button class="btn" onclick="${onclick}">${text}</button>`;
2014
+ }
2015
+
2016
+ function htmlStats(items) {
2017
+ return `<div class="st">${items.map(i=>`<span>${i.label}<b id="${i.id}">${i.value}</b></span>`).join('')}</div>`;
2018
+ }
2019
+
2020
+ function htmlDsp(id, val) {
2021
+ return `<div class="dsp" id="${id}">${val}</div>`;
1838
2022
  }
1839
2023
 
1840
- function htmlWrap(title, css, sections, js) {
2024
+ function htmlBar() {
2025
+ return `<div class="bar"><div class="fl" id="bar" style="width:100%"></div></div>`;
2026
+ }
2027
+
2028
+ function htmlP(text, style) {
2029
+ return `<p${style?' style="'+style+'"':''}>${text}</p>`;
2030
+ }
2031
+
2032
+ // ── Page Assembler ──
2033
+
2034
+ function assemblePage(title, css, body, js) {
1841
2035
  return `<!DOCTYPE html>
1842
2036
  <html lang="en">
1843
2037
  <head>
@@ -1851,7 +2045,7 @@ ${css}
1851
2045
  <body>
1852
2046
  <div class="ct">
1853
2047
  <h1>${title}</h1>
1854
- ${sections}
2048
+ ${body}
1855
2049
  </div>
1856
2050
  <script>
1857
2051
  ${js}
@@ -1860,342 +2054,838 @@ ${js}
1860
2054
  </html>`;
1861
2055
  }
1862
2056
 
1863
- // ── Tapping Game Builder ──
2057
+ // ── Game Builders ──
1864
2058
 
1865
- function buildTapping(title, opts) {
1866
- const p = GAME_PALETTES.tapping;
2059
+ function buildTapping(title, t, opts) {
1867
2060
  const dur = opts.duration || 10;
1868
2061
 
1869
- const css = [cssVars(p), cssBase(p), cssTimerBar(p),
1870
- `.tap{width:min(300px,75vw);height:min(300px,75vw);border-radius:50%;background:radial-gradient(circle at 35% 35%,${p.pr},${p.sc});display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;position:relative;font-size:3.5rem;font-weight:900;transition:box-shadow .15s;letter-spacing:-.05em}`,
1871
- `.tap:active{box-shadow:0 0 60px ${p.pr}88}`,
2062
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssBar(t), cssFx(t),
2063
+ `.tap{width:min(300px,75vw);height:min(300px,75vw);border-radius:50%;background:radial-gradient(circle at 35% 35%,${t.pr},${t.sc});display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;position:relative;font-size:3.5rem;font-weight:900;transition:box-shadow .15s;letter-spacing:-.05em;overflow:hidden}`,
2064
+ `.tap:active{box-shadow:0 0 60px ${t.pr}88}`,
1872
2065
  `.tap.on{animation:pulse 1s infinite}`,
1873
- `@keyframes pulse{0%,100%{box-shadow:0 0 0 0 ${p.pr}66}50%{box-shadow:0 0 0 20px ${p.pr}00}}`,
2066
+ `@keyframes pulse{0%,100%{box-shadow:0 0 0 0 ${t.pr}66}50%{box-shadow:0 0 0 20px ${t.pr}00}}`,
1874
2067
  `.rp{position:absolute;border-radius:50%;background:rgba(255,255,255,.25);animation:rpAn .5s ease-out forwards;pointer-events:none}`,
1875
2068
  `@keyframes rpAn{0%{width:0;height:0;opacity:.6}100%{width:200px;height:200px;opacity:0}}`,
1876
- `.cb{position:fixed;font-weight:800;font-size:1.5rem;color:${p.ac};pointer-events:none;animation:fadeUp .6s ease-out forwards}`,
2069
+ `.cb{position:fixed;font-weight:800;font-size:1.5rem;color:${t.ac};pointer-events:none;animation:fadeUp .6s ease-out forwards}`,
1877
2070
  ].join('\n');
1878
2071
 
1879
- const html = `<div id="s-menu">
1880
- <p style="color:${p.mt};margin-bottom:1.5rem">Tap as fast as you can — ${dur} seconds!</p>
1881
- <div class="st"><span>Best<b id="best">0</b></span><span>Last<b id="last">-</b></span></div>
1882
- <button class="btn" onclick="go()">Start</button></div>
1883
- <div id="s-play" class="hd">
1884
- <div class="bar"><div class="fl" id="bar" style="width:100%"></div></div>
1885
- <div class="sc" id="cnt">0</div>
1886
- <div class="tap on" id="tap" ontouchstart="hit(event)" onmousedown="hit(event)">TAP</div>
1887
- <div class="st"><span>Combo<b id="cmb">x1</b></span><span>TPS<b id="tps">0</b></span></div></div>
1888
- <div id="s-end" class="hd">
1889
- <div class="sc" id="fin">0</div>
1890
- <div id="msg" style="font-size:1.2rem;line-height:2"></div>
1891
- <button class="btn" style="margin-top:1rem" onclick="go()">Again</button></div>`;
1892
-
1893
- const js = [jsCore('tap_best'),
1894
- `const DUR=${dur*1000};let taps,t0,raf,combo,lh;`,
1895
- `function go(){taps=0;combo=1;lh=0;t0=Date.now();$('cnt').textContent='0';$('cmb').textContent='x1';$('bar').style.width='100%';show('play');raf=requestAnimationFrame(tick)}`,
1896
- `function tick(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('tps').textContent=(taps/Math.max(.1,e/1000)).toFixed(1);if(e>=DUR)return end();raf=requestAnimationFrame(tick)}`,
1897
- `function hit(ev){ev.preventDefault();if(Date.now()-t0>DUR)return;taps++;const now=Date.now(),gap=now-lh;combo=gap<200?Math.min(combo+1,99):gap>500?1:combo;lh=now;$('cnt').textContent=taps;$('cmb').textContent='x'+combo;$('cnt').style.animation='none';void $('cnt').offsetWidth;$('cnt').style.animation='pop .15s';const el=$('tap'),rc=el.getBoundingClientRect(),cx=(ev.touches?ev.touches[0].clientX:ev.clientX)-rc.left,cy=(ev.touches?ev.touches[0].clientY:ev.clientY)-rc.top;const r=document.createElement('div');r.className='rp';r.style.cssText='left:'+(cx-100)+'px;top:'+(cy-100)+'px';el.appendChild(r);setTimeout(()=>r.remove(),500);if(combo>=3){const c=document.createElement('div');c.className='cb';c.textContent='x'+combo;c.style.cssText='left:'+Math.random()*60+20+'%;top:'+Math.random()*30+20+'%';document.body.appendChild(c);setTimeout(()=>c.remove(),600)}}`,
1898
- `function end(){cancelAnimationFrame(raf);const tps=(taps/(DUR/1000)).toFixed(1);const nb=saveBest(taps);$('last').textContent=taps;$('fin').textContent=taps;const r=taps>=100?'Insane!':taps>=70?'Amazing!':taps>=50?'Great!':taps>=30?'Nice!':'Keep trying!';$('msg').innerHTML=r+'<br>'+tps+' taps/sec'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2072
+ const html = [
2073
+ htmlScr('menu', [
2074
+ htmlP(`Tap as fast as you can — ${dur} seconds!`, `color:${t.mt};margin-bottom:1.5rem`),
2075
+ htmlStats([{label:'Best',id:'best',value:'0'},{label:'Last',id:'last',value:'-'}]),
2076
+ htmlBtn('Start', 'go()'),
2077
+ ].join('\n'), true),
2078
+ htmlScr('play', [
2079
+ htmlBar(),
2080
+ htmlDsp('cnt', '0'),
2081
+ `<div class="tap on" id="tap" ontouchstart="hit(event)" onmousedown="hit(event)">TAP</div>`,
2082
+ htmlStats([{label:'Combo',id:'cmb',value:'x1'},{label:'TPS',id:'tps',value:'0'}]),
2083
+ ].join('\n')),
2084
+ htmlScr('end', [
2085
+ htmlDsp('fin', '0'),
2086
+ `<div id="msg" style="font-size:1.2rem;line-height:2"></div>`,
2087
+ htmlBtn('Again', 'go()'),
2088
+ ].join('\n')),
1899
2089
  ].join('\n');
1900
2090
 
1901
- return htmlWrap(title, css, html, js);
1902
- }
2091
+ const js = [jsHelper(), jsScreen(), jsStore('tap'), jsScore(), jsTimer(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac),
2092
+ `const DUR=${dur*1000};let taps,t0,combo,lh;`,
2093
+ `function go(){show('play');countdown(startGame)}`,
2094
+ `function startGame(){taps=0;combo=1;lh=0;t0=Date.now();$('cnt').textContent='0';$('cmb').textContent='x1';$('bar').style.width='100%';startTimer(DUR,tick,end)}`,
2095
+ `function tick(e,ms){const pc=Math.max(0,1-e/ms)*100;$('bar').style.width=pc+'%';$('tps').textContent=(taps/Math.max(.1,e/1000)).toFixed(1)}`,
2096
+ `function hit(ev){ev.preventDefault();if(!t0||Date.now()-t0>DUR)return;taps++;sfxTap();const now=Date.now(),gap=now-lh;combo=gap<200?Math.min(combo+1,99):gap>500?1:combo;lh=now;$('cnt').textContent=taps;$('cmb').textContent='x'+combo;$('cnt').style.animation='none';void $('cnt').offsetWidth;$('cnt').style.animation='pop .15s';const el=$('tap'),rc=el.getBoundingClientRect(),cx=(ev.touches?ev.touches[0].clientX:ev.clientX)-rc.left,cy=(ev.touches?ev.touches[0].clientY:ev.clientY)-rc.top;const r=document.createElement('div');r.className='rp';r.style.cssText='left:'+(cx-100)+'px;top:'+(cy-100)+'px';el.appendChild(r);setTimeout(()=>r.remove(),500);emitParticles((ev.touches?ev.touches[0].clientX:ev.clientX),(ev.touches?ev.touches[0].clientY:ev.clientY),combo>=3?10:4);if(combo>=3){sfxCombo();shakeEl($('cnt'));const c=document.createElement('div');c.className='cb';c.textContent='x'+combo;c.style.cssText='left:'+Math.random()*60+20+'%;top:'+Math.random()*30+20+'%';document.body.appendChild(c);setTimeout(()=>c.remove(),600)}}`,
2097
+ `function end(){stopTimer();const tps=(taps/(DUR/1000)).toFixed(1);const nb=saveBest(taps);$('last').textContent=taps;$('fin').textContent=taps;if(nb&&taps>0){sfxWin();burstAt($('fin'),30)}else sfxFail();const r=taps>=100?'Insane!':taps>=70?'Amazing!':taps>=50?'Great!':taps>=30?'Nice!':'Keep trying!';$('msg').innerHTML=r+'<br>'+tps+' taps/sec'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2098
+ ].join('\n');
1903
2099
 
1904
- // ── Quiz Game Builder ──
2100
+ return assemblePage(title, css, html, js);
2101
+ }
1905
2102
 
1906
- function buildQuiz(title, opts) {
1907
- const p = GAME_PALETTES.quiz;
2103
+ function buildQuiz(title, t, opts) {
1908
2104
  const perQ = opts.duration || 15;
1909
2105
 
1910
- const css = [cssVars(p), cssBase(p),
2106
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t),
1911
2107
  `.q{font-size:1.3rem;margin:1.5rem 0;min-height:3em;line-height:1.5}`,
1912
2108
  `.ch{display:flex;flex-direction:column;gap:.75rem;margin:1rem 0}`,
1913
- `.ch div{padding:1rem;border:2px solid ${p.sf};border-radius:12px;background:${p.sf};cursor:pointer;font-size:1rem;text-align:left;transition:border-color .2s,background .2s}`,
1914
- `.ch div:hover{border-color:${p.pr}}`,
1915
- `.ch .ok{border-color:${p.ac};background:${p.ac}22}`,
2109
+ `.ch div{padding:1rem;border:2px solid ${t.sf};border-radius:12px;background:${t.sf};cursor:pointer;font-size:1rem;text-align:left;transition:border-color .2s,background .2s}`,
2110
+ `.ch div:hover{border-color:${t.pr}}`,
2111
+ `.ch .ok{border-color:${t.ac};background:${t.ac}22}`,
1916
2112
  `.ch .no{border-color:#F87171;background:#F8717122}`,
1917
2113
  `.dots{display:flex;gap:4px;justify-content:center;margin-bottom:1rem}`,
1918
- `.dot{width:10px;height:10px;border-radius:50%;background:${p.sf}}`,
1919
- `.dot.pass{background:${p.ac}}.dot.fail{background:#F87171}.dot.now{background:${p.pr};box-shadow:0 0 8px ${p.pr}}`,
2114
+ `.dot{width:10px;height:10px;border-radius:50%;background:${t.sf}}`,
2115
+ `.dot.pass{background:${t.ac}}.dot.fail{background:#F87171}.dot.now{background:${t.pr};box-shadow:0 0 8px ${t.pr}}`,
1920
2116
  `.tm{font-size:2.5rem;font-weight:800;font-variant-numeric:tabular-nums}`,
1921
2117
  ].join('\n');
1922
2118
 
1923
- const html = `<div id="s-menu">
1924
- <p style="color:${p.mt};margin:1rem 0">10 questions, ${perQ} seconds each</p>
1925
- <div class="st"><span>Best<b id="best">0</b></span><span>Last<b id="last">-</b></span></div>
1926
- <button class="btn" onclick="go()">Start Quiz</button></div>
1927
- <div id="s-play" class="hd">
1928
- <div class="dots" id="dots"></div>
2119
+ const html = [
2120
+ htmlScr('menu', [
2121
+ htmlP(`10 questions, ${perQ} seconds each`, `color:${t.mt};margin:1rem 0`),
2122
+ htmlStats([{label:'Best',id:'best',value:'0'},{label:'Last',id:'last',value:'-'}]),
2123
+ htmlBtn('Start Quiz', 'go()'),
2124
+ ].join('\n'), true),
2125
+ htmlScr('play', `<div class="dots" id="dots"></div>
1929
2126
  <div class="tm" id="timer">${perQ}</div>
1930
2127
  <div class="q" id="qtext"></div>
1931
- <div class="ch" id="choices"></div></div>
1932
- <div id="s-end" class="hd">
1933
- <div class="sc" id="fin">0/10</div>
1934
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
1935
- <button class="btn" onclick="go()">Play Again</button></div>`;
1936
-
1937
- const js = [jsCore('quiz_best'),
1938
- `const QS=[{q:"What does HTML stand for?",a:["HyperText Markup Language","High Tech Modern Language","Home Tool Markup Language","Hyper Transfer Markup Language"],c:0},{q:"Which planet is closest to the Sun?",a:["Mercury","Venus","Mars","Earth"],c:0},{q:"What is the largest ocean on Earth?",a:["Pacific","Atlantic","Indian","Arctic"],c:0},{q:"In JavaScript, which keyword declares a constant?",a:["const","let","var","static"],c:0},{q:"What year did the World Wide Web go public?",a:["1991","1989","1995","2000"],c:0},{q:"What is the chemical symbol for gold?",a:["Au","Ag","Go","Gd"],c:0},{q:"How many bits are in a byte?",a:["8","4","16","32"],c:0},{q:"What does CSS stand for?",a:["Cascading Style Sheets","Computer Style Sheets","Creative Style System","Coded Style Sheets"],c:0},{q:"What is the speed of light (approx km/s)?",a:["300,000","150,000","500,000","1,000,000"],c:0},{q:"Which data structure uses FIFO?",a:["Queue","Stack","Tree","Graph"],c:0},{q:"What is the square root of 144?",a:["12","14","10","16"],c:0},{q:"Who painted the Mona Lisa?",a:["Da Vinci","Picasso","Van Gogh","Monet"],c:0},{q:"What gas do plants absorb?",a:["CO2","O2","N2","H2"],c:0},{q:"Which protocol is used for secure browsing?",a:["HTTPS","FTP","SMTP","SSH"],c:0},{q:"What is 2^10?",a:["1024","512","2048","4096"],c:0}];`,
1939
- `const PER_Q=${perQ};let cur,score,pool,tmr,tl;`,
1940
- `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
1941
- `function go(){pool=shuf([...QS]).slice(0,10);cur=0;score=0;$('dots').innerHTML=pool.map((_,i)=>'<div class="dot" id="d'+i+'"></div>').join('');show('play');next()}`,
1942
- `function next(){if(cur>=pool.length)return end();$('d'+cur).classList.add('now');const q=pool[cur],mx=q.a.map((t,i)=>({t,ok:i===q.c}));shuf(mx);$('qtext').textContent=q.q;const ch=$('choices');ch.innerHTML='';mx.forEach(m=>{const d=document.createElement('div');d.textContent=m.t;d.onclick=()=>pick(d,m.ok,ch);ch.appendChild(d)});tl=PER_Q;$('timer').textContent=tl;clearInterval(tmr);tmr=setInterval(()=>{tl--;$('timer').textContent=Math.max(0,tl);if(tl<=0){clearInterval(tmr);timeout()}},1000)}`,
1943
- `function pick(el,ok,ch){clearInterval(tmr);ch.querySelectorAll('div').forEach(c=>{c.onclick=null;if(c===el)c.classList.add(ok?'ok':'no');else if(!ok){const t=pool[cur].a[pool[cur].c];if(c.textContent===t)c.classList.add('ok')}});$('d'+cur).classList.remove('now');$('d'+cur).classList.add(ok?'pass':'fail');if(ok)score++;cur++;setTimeout(next,800)}`,
1944
- `function timeout(){const ch=$('choices');ch.querySelectorAll('div').forEach(c=>{c.onclick=null;if(c.textContent===pool[cur].a[pool[cur].c])c.classList.add('ok')});$('d'+cur).classList.remove('now');$('d'+cur).classList.add('fail');cur++;setTimeout(next,800)}`,
1945
- `function end(){clearInterval(tmr);const nb=saveBest(score);$('last').textContent=score;$('fin').textContent=score+'/'+pool.length;const pc=Math.round(score/pool.length*100);const r=pc===100?'Perfect!':pc>=80?'Excellent!':pc>=60?'Good!':pc>=40?'Not bad!':'Study more!';$('msg').innerHTML=r+'<br>'+pc+'% correct'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2128
+ <div class="ch" id="choices"></div>`),
2129
+ htmlScr('end', [
2130
+ htmlDsp('fin', '0/10'),
2131
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2132
+ htmlBtn('Play Again', 'go()'),
2133
+ ].join('\n')),
1946
2134
  ].join('\n');
1947
2135
 
1948
- return htmlWrap(title, css, html, js);
1949
- }
2136
+ const js = [jsHelper(), jsScreen(), jsStore('quiz'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac), jsShuffle(),
2137
+ `const QS=[{q:"What does HTML stand for?",a:["HyperText Markup Language","High Tech Modern Language","Home Tool Markup Language","Hyper Transfer Markup Language"],c:0},{q:"Which planet is closest to the Sun?",a:["Mercury","Venus","Mars","Earth"],c:0},{q:"What is the largest ocean on Earth?",a:["Pacific","Atlantic","Indian","Arctic"],c:0},{q:"In JavaScript, which keyword declares a constant?",a:["const","let","var","static"],c:0},{q:"What year did the World Wide Web go public?",a:["1991","1989","1995","2000"],c:0},{q:"What is the chemical symbol for gold?",a:["Au","Ag","Go","Gd"],c:0},{q:"How many bits are in a byte?",a:["8","4","16","32"],c:0},{q:"What does CSS stand for?",a:["Cascading Style Sheets","Computer Style Sheets","Creative Style System","Coded Style Sheets"],c:0},{q:"What is the speed of light (approx km/s)?",a:["300,000","150,000","500,000","1,000,000"],c:0},{q:"Which data structure uses FIFO?",a:["Queue","Stack","Tree","Graph"],c:0},{q:"What is the square root of 144?",a:["12","14","10","16"],c:0},{q:"Who painted the Mona Lisa?",a:["Da Vinci","Picasso","Van Gogh","Monet"],c:0},{q:"What gas do plants absorb?",a:["CO2","O2","N2","H2"],c:0},{q:"Which protocol is used for secure browsing?",a:["HTTPS","FTP","SMTP","SSH"],c:0},{q:"What is 2^10?",a:["1024","512","2048","4096"],c:0},{q:"What is the binary representation of 10?",a:["1010","1100","1001","0110"],c:0},{q:"Which HTTP status code means Not Found?",a:["404","500","403","301"],c:0},{q:"What is the capital of Japan?",a:["Tokyo","Osaka","Kyoto","Nagoya"],c:0},{q:"How many continents are there?",a:["7","6","5","8"],c:0},{q:"What does API stand for?",a:["Application Programming Interface","Advanced Program Integration","Application Process Interface","Automated Programming Interface"],c:0}];`,
2138
+ `const PER_Q=${perQ};let cur,score,pool,tmr,tl,streak=0;`,
2139
+ `function go(){pool=shuf([...QS]).slice(0,10);cur=0;score=0;streak=0;$('dots').innerHTML=pool.map((_,i)=>'<div class="dot" id="d'+i+'"></div>').join('');show('play');next()}`,
2140
+ `function next(){if(cur>=pool.length)return end();$('d'+cur).classList.add('now');const q=pool[cur],mx=q.a.map((t,i)=>({t,ok:i===q.c}));shuf(mx);$('qtext').textContent=q.q;const ch=$('choices');ch.innerHTML='';mx.forEach(m=>{const d=document.createElement('div');d.textContent=m.t;d.onclick=()=>pick(d,m.ok,ch);ch.appendChild(d)});tl=PER_Q;$('timer').textContent=tl;clearInterval(tmr);tmr=setInterval(()=>{tl--;$('timer').textContent=Math.max(0,tl);if(tl<=3&&tl>0)sfxTick();if(tl<=0){clearInterval(tmr);timeout()}},1000)}`,
2141
+ `function pick(el,ok,ch){clearInterval(tmr);ch.querySelectorAll('div').forEach(c=>{c.onclick=null;if(c===el)c.classList.add(ok?'ok':'no');else if(!ok){const t=pool[cur].a[pool[cur].c];if(c.textContent===t)c.classList.add('ok')}});$('d'+cur).classList.remove('now');$('d'+cur).classList.add(ok?'pass':'fail');if(ok){score++;streak++;sfxScore();burstAt(el,streak>=3?20:8)}else{streak=0;sfxFail();shakeEl($('qtext'))}cur++;setTimeout(next,800)}`,
2142
+ `function timeout(){sfxFail();const ch=$('choices');ch.querySelectorAll('div').forEach(c=>{c.onclick=null;if(c.textContent===pool[cur].a[pool[cur].c])c.classList.add('ok')});$('d'+cur).classList.remove('now');$('d'+cur).classList.add('fail');streak=0;cur++;setTimeout(next,800)}`,
2143
+ `function end(){clearInterval(tmr);const nb=saveBest(score);$('last').textContent=score;$('fin').textContent=score+'/'+pool.length;if(nb&&score>0){sfxWin();burstAt($('fin'),30)}const pc=Math.round(score/pool.length*100);const r=pc===100?'Perfect!':pc>=80?'Excellent!':pc>=60?'Good!':pc>=40?'Not bad!':'Study more!';$('msg').innerHTML=r+'<br>'+pc+'% correct'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2144
+ ].join('\n');
1950
2145
 
1951
- // ── Memory Game Builder ──
2146
+ return assemblePage(title, css, html, js);
2147
+ }
1952
2148
 
1953
- function buildMemory(title, opts) {
1954
- const p = GAME_PALETTES.memory;
2149
+ function buildMemory(title, t, opts) {
1955
2150
  const cols = opts.cols || 4;
1956
2151
  const pairs = Math.floor((cols * (opts.rows || cols)) / 2);
1957
2152
 
1958
- const css = [cssVars(p), cssBase(p),
1959
- `.grid{display:grid;grid-template-columns:repeat(${cols},1fr);gap:8px;margin:1rem auto;width:min(${cols*90}px,88vw)}`,
1960
- `.card{aspect-ratio:1;border-radius:12px;cursor:pointer;perspective:600px;-webkit-perspective:600px}`,
1961
- `.card .inn{position:relative;width:100%;height:100%;transition:transform .4s;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}`,
1962
- `.card.flip .inn{transform:rotateY(180deg)}`,
1963
- `.card .fr,.card .bk{position:absolute;width:100%;height:100%;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:${cols<=4?'2rem':'1.4rem'};backface-visibility:hidden;-webkit-backface-visibility:hidden}`,
1964
- `.card .fr{background:linear-gradient(135deg,${p.pr},${p.sc})}`,
1965
- `.card .bk{background:${p.sf};transform:rotateY(180deg);border:2px solid ${p.pr}}`,
1966
- `.card.done .fr{background:linear-gradient(135deg,${p.ac},#059669)}`,
1967
- `.card.done{pointer-events:none}`,
2153
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t),
2154
+ cssGrid(t, cols), cssCards(t, cols),
1968
2155
  ].join('\n');
1969
2156
 
1970
- const html = `<div id="s-menu">
1971
- <p style="color:${p.mt};margin:1rem 0">Match all ${pairs} pairs with fewest moves</p>
1972
- <div class="st"><span>Best<b id="best">-</b></span></div>
1973
- <button class="btn" onclick="go()">Start</button></div>
1974
- <div id="s-play" class="hd">
1975
- <div class="st"><span>Moves<b id="mv">0</b></span><span>Pairs<b id="pr">0/${pairs}</b></span><span>Time<b id="tm">0s</b></span></div>
1976
- <div class="grid" id="grid"></div></div>
1977
- <div id="s-end" class="hd">
1978
- <div class="sc" id="fin">0</div>
1979
- <p style="font-size:1.2rem;margin:.5rem 0">moves</p>
1980
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
1981
- <button class="btn" onclick="go()">Play Again</button></div>`;
1982
-
1983
- const js = [jsCore('mem_best'),
2157
+ const html = [
2158
+ htmlScr('menu', [
2159
+ htmlP(`Match all ${pairs} pairs with fewest moves`, `color:${t.mt};margin:1rem 0`),
2160
+ htmlStats([{label:'Best',id:'best',value:'-'}]),
2161
+ htmlBtn('Start', 'go()'),
2162
+ ].join('\n'), true),
2163
+ htmlScr('play', [
2164
+ htmlStats([{label:'Moves',id:'mv',value:'0'},{label:'Pairs',id:'pr',value:`0/${pairs}`},{label:'Time',id:'tm',value:'0s'}]),
2165
+ `<div class="grid" id="grid"></div>`,
2166
+ ].join('\n')),
2167
+ htmlScr('end', [
2168
+ htmlDsp('fin', '0'),
2169
+ htmlP('moves', 'font-size:1.2rem;margin:.5rem 0'),
2170
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2171
+ htmlBtn('Play Again', 'go()'),
2172
+ ].join('\n')),
2173
+ ].join('\n');
2174
+
2175
+ const js = [jsHelper(), jsScreen(), jsStore('mem'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac), jsShuffle(),
1984
2176
  `const EM=['🎮','🎯','🚀','💎','🔥','⚡','🌟','🎵','🎨','🏆','🍕','🌈','🦊','🐙','🎃','🍀','🎲','🌸'];`,
1985
- `const PAIRS=${pairs};let cards,flp,done,moves,tmr,t0;`,
1986
- `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
1987
- `function go(){const pick=shuf([...EM]).slice(0,PAIRS);cards=shuf([...pick,...pick]);flp=[];done=0;moves=0;$('mv').textContent=0;$('pr').textContent='0/'+PAIRS;const g=$('grid');g.innerHTML='';cards.forEach((em,i)=>{const d=document.createElement('div');d.className='card';d.innerHTML='<div class="inn"><div class="fr">?</div><div class="bk">'+em+'</div></div>';d.onclick=()=>flip(d,i);g.appendChild(d)});t0=Date.now();tmr=setInterval(()=>{$('tm').textContent=Math.floor((Date.now()-t0)/1000)+'s'},200);show('play')}`,
1988
- `function flip(el,i){if(flp.length>=2||el.classList.contains('flip')||el.classList.contains('done'))return;el.classList.add('flip');flp.push({el,i,v:cards[i]});if(flp.length===2){moves++;$('mv').textContent=moves;const[a,b]=flp;if(a.v===b.v){a.el.classList.add('done');b.el.classList.add('done');done++;$('pr').textContent=done+'/'+PAIRS;flp=[];if(done===PAIRS)end()}else{setTimeout(()=>{a.el.classList.remove('flip');b.el.classList.remove('flip');flp=[]},700)}}}`,
1989
- `function end(){clearInterval(tmr);const sec=Math.floor((Date.now()-t0)/1000);const nb=saveBest(moves);$('fin').textContent=moves;const r=moves<=PAIRS+2?'Perfect memory!':moves<=PAIRS*2?'Great!':moves<=PAIRS*3?'Good!':'Keep practicing!';$('msg').innerHTML=r+'<br>Completed in '+sec+'s'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2177
+ `const PAIRS=${pairs};let cards,flp,done,moves,tmr,t0,cstrk=0;`,
2178
+ `function go(){const pick=shuf([...EM]).slice(0,PAIRS);cards=shuf([...pick,...pick]);flp=[];done=0;moves=0;cstrk=0;$('mv').textContent=0;$('pr').textContent='0/'+PAIRS;const g=$('grid');g.innerHTML='';cards.forEach((em,i)=>{const d=document.createElement('div');d.className='card';d.innerHTML='<div class="inn"><div class="fr">?</div><div class="bk">'+em+'</div></div>';d.onclick=()=>flip(d,i);g.appendChild(d)});t0=Date.now();tmr=setInterval(()=>{$('tm').textContent=Math.floor((Date.now()-t0)/1000)+'s'},200);show('play')}`,
2179
+ `function flip(el,i){if(flp.length>=2||el.classList.contains('flip')||el.classList.contains('done'))return;sfxTap();el.classList.add('flip');flp.push({el,i,v:cards[i]});if(flp.length===2){moves++;$('mv').textContent=moves;const[a,b]=flp;if(a.v===b.v){cstrk++;if(cstrk>=2)sfxCombo();else sfxScore();a.el.classList.add('done');b.el.classList.add('done');burstAt(a.el,cstrk>=2?16:8);burstAt(b.el,cstrk>=2?16:8);done++;$('pr').textContent=done+'/'+PAIRS;flp=[];if(done===PAIRS)end()}else{cstrk=0;sfxHit();setTimeout(()=>{a.el.classList.remove('flip');b.el.classList.remove('flip');flp=[]},700)}}}`,
2180
+ `function end(){clearInterval(tmr);const sec=Math.floor((Date.now()-t0)/1000);const nb=saveBest(moves);if(nb)sfxWin();$('fin').textContent=moves;burstAt($('fin'),30);const r=moves<=PAIRS+2?'Perfect memory!':moves<=PAIRS*2?'Great!':moves<=PAIRS*3?'Good!':'Keep practicing!';$('msg').innerHTML=r+'<br>Completed in '+sec+'s'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
1990
2181
  ].join('\n');
1991
2182
 
1992
- return htmlWrap(title, css, html, js);
2183
+ return assemblePage(title, css, html, js);
1993
2184
  }
1994
2185
 
1995
- // ── Snake Game Builder ──
1996
-
1997
- function buildSnake(title, opts) {
1998
- const p = GAME_PALETTES.snake;
2186
+ function buildSnake(title, t, opts) {
1999
2187
  const spd = opts.speed === 'fast' ? 80 : opts.difficulty === 'hard' ? 90 : 120;
2000
2188
  const W = 16, S = 20;
2001
2189
 
2002
- const css = [cssVars(p), cssBase(p),
2003
- `canvas{border-radius:12px;background:${p.sf};display:block;margin:1rem auto;image-rendering:pixelated;touch-action:none}`,
2190
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t), cssCanvas(t),
2004
2191
  `.ctl{display:grid;grid-template-columns:repeat(3,56px);grid-template-rows:repeat(2,56px);gap:6px;justify-content:center;margin:1rem auto}`,
2005
- `.ctl button{border:none;border-radius:10px;background:${p.sf};color:${p.tx};font-size:1.4rem;cursor:pointer;display:flex;align-items:center;justify-content:center}`,
2006
- `.ctl button:active{background:${p.pr}}`,
2192
+ `.ctl button{border:none;border-radius:10px;background:${t.sf};color:${t.tx};font-size:1.4rem;cursor:pointer;display:flex;align-items:center;justify-content:center}`,
2193
+ `.ctl button:active{background:${t.pr}}`,
2194
+ ].join('\n');
2195
+
2196
+ const html = [
2197
+ htmlScr('menu', [
2198
+ htmlP('Arrow keys or swipe to control', `color:${t.mt};margin:1rem 0`),
2199
+ htmlStats([{label:'Best',id:'best',value:'0'}]),
2200
+ htmlBtn('Play', 'go()'),
2201
+ ].join('\n'), true),
2202
+ htmlScr('play', [
2203
+ htmlStats([{label:'Score',id:'score',value:'0'},{label:'Best',id:'hi',value:'0'}]),
2204
+ `<canvas id="cv" width="${W*S}" height="${W*S}"></canvas>`,
2205
+ `<div class="ctl"><div></div><button ontouchstart="sd(0,-1)" onclick="sd(0,-1)">▲</button><div></div><button ontouchstart="sd(-1,0)" onclick="sd(-1,0)">◀</button><button ontouchstart="sd(0,1)" onclick="sd(0,1)">▼</button><button ontouchstart="sd(1,0)" onclick="sd(1,0)">▶</button></div>`,
2206
+ ].join('\n')),
2207
+ htmlScr('end', [
2208
+ htmlDsp('fin', '0'),
2209
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2210
+ htmlBtn('Play Again', 'go()'),
2211
+ ].join('\n')),
2007
2212
  ].join('\n');
2008
2213
 
2009
- const html = `<div id="s-menu">
2010
- <p style="color:${p.mt};margin:1rem 0">Arrow keys or swipe to control</p>
2011
- <div class="st"><span>Best<b id="best">0</b></span></div>
2012
- <button class="btn" onclick="go()">Play</button></div>
2013
- <div id="s-play" class="hd">
2014
- <div class="st"><span>Score<b id="score">0</b></span><span>Best<b id="hi">0</b></span></div>
2015
- <canvas id="cv" width="${W*S}" height="${W*S}"></canvas>
2016
- <div class="ctl">
2017
- <div></div><button ontouchstart="sd(0,-1)" onclick="sd(0,-1)">▲</button><div></div>
2018
- <button ontouchstart="sd(-1,0)" onclick="sd(-1,0)">◀</button><button ontouchstart="sd(0,1)" onclick="sd(0,1)">▼</button><button ontouchstart="sd(1,0)" onclick="sd(1,0)">▶</button></div></div>
2019
- <div id="s-end" class="hd">
2020
- <div class="sc" id="fin">0</div>
2021
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2022
- <button class="btn" onclick="go()">Play Again</button></div>`;
2023
-
2024
- const js = [jsCore('snake_best'),
2025
- `const GS=${S},GW=${W},cv=$('cv'),ctx=cv.getContext('2d');`,
2026
- `let sn,dir,nd,food,sc,lp;$('hi').textContent=best;`,
2214
+ const js = [jsHelper(), jsScreen(), jsStore('snake'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac),
2215
+ `const GS=${S},GW=${W},cv=$('cv'),ctx=cv.getContext('2d'),BASE_SPD=${spd};`,
2216
+ `let sn,dir,nd,food,sc,lp,spd;$('hi').textContent=best;`,
2027
2217
  `function rnd(){let p;do{p={x:Math.floor(Math.random()*GW),y:Math.floor(Math.random()*GW)}}while(sn.some(s=>s.x===p.x&&s.y===p.y));return p}`,
2028
- `function go(){sn=[{x:8,y:8},{x:7,y:8},{x:6,y:8}];dir={x:1,y:0};nd={x:1,y:0};food=rnd();sc=0;$('score').textContent=0;show('play');clearInterval(lp);lp=setInterval(tick,${spd})}`,
2218
+ `function go(){show('play');countdown(startGame)}`,
2219
+ `function startGame(){sn=[{x:8,y:8},{x:7,y:8},{x:6,y:8}];dir={x:1,y:0};nd={x:1,y:0};food=rnd();sc=0;spd=BASE_SPD;$('score').textContent=0;clearInterval(lp);lp=setInterval(tick,spd)}`,
2029
2220
  `function sd(x,y){if(x===-dir.x&&y===-dir.y)return;nd={x,y}}`,
2030
- `function tick(){dir=nd;const h={x:sn[0].x+dir.x,y:sn[0].y+dir.y};if(h.x<0||h.x>=GW||h.y<0||h.y>=GW||sn.some(s=>s.x===h.x&&s.y===h.y))return end();sn.unshift(h);if(h.x===food.x&&h.y===food.y){sc++;$('score').textContent=sc;food=rnd()}else sn.pop();draw()}`,
2031
- `function draw(){ctx.fillStyle='${p.sf}';ctx.fillRect(0,0,GW*GS,GW*GS);ctx.fillStyle='${p.ac}';ctx.beginPath();ctx.arc(food.x*GS+GS/2,food.y*GS+GS/2,GS/2-2,0,Math.PI*2);ctx.fill();sn.forEach((s,i)=>{const t=i/sn.length;ctx.fillStyle=i===0?'${p.pr}':'hsl('+(140+t*40)+',70%,'+(55-t*20)+'%)';ctx.beginPath();ctx.roundRect(s.x*GS+1,s.y*GS+1,GS-2,GS-2,4);ctx.fill()})}`,
2032
- `function end(){clearInterval(lp);const nb=saveBest(sc);$('hi').textContent=best;$('fin').textContent=sc;const r=sc>=30?'Legend!':sc>=20?'Amazing!':sc>=10?'Great!':sc>=5?'Good!':'Try again!';$('msg').innerHTML=r+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2033
- `document.addEventListener('keydown',e=>{if(e.key==='ArrowUp')sd(0,-1);if(e.key==='ArrowDown')sd(0,1);if(e.key==='ArrowLeft')sd(-1,0);if(e.key==='ArrowRight')sd(1,0)});`,
2221
+ `function tick(){dir=nd;const h={x:sn[0].x+dir.x,y:sn[0].y+dir.y};if(h.x<0||h.x>=GW||h.y<0||h.y>=GW||sn.some(s=>s.x===h.x&&s.y===h.y))return end();sn.unshift(h);if(h.x===food.x&&h.y===food.y){sc++;sfxScore();$('score').textContent=sc;const rc=cv.getBoundingClientRect();emitParticles(rc.left+food.x*GS*(rc.width/(GW*GS))+GS/2,rc.top+food.y*GS*(rc.height/(GW*GS))+GS/2,10);food=rnd();if(sc%5===0&&spd>50){spd-=8;clearInterval(lp);lp=setInterval(tick,spd)}}else sn.pop();draw()}`,
2222
+ `function draw(){ctx.fillStyle='${t.sf}';ctx.fillRect(0,0,GW*GS,GW*GS);for(let x=0;x<GW;x++)for(let y=0;y<GW;y++){ctx.fillStyle='rgba(255,255,255,.015)';if((x+y)%2===0)ctx.fillRect(x*GS,y*GS,GS,GS)}ctx.fillStyle='${t.ac}';ctx.beginPath();ctx.arc(food.x*GS+GS/2,food.y*GS+GS/2,GS/2-2+Math.sin(Date.now()/200)*2,0,Math.PI*2);ctx.fill();ctx.shadowColor='${t.ac}';ctx.shadowBlur=8;ctx.fill();ctx.shadowBlur=0;sn.forEach((s,i)=>{const t=i/sn.length;ctx.fillStyle=i===0?'${t.pr}':'hsl('+(140+t*40)+',70%,'+(55-t*20)+'%)';ctx.beginPath();ctx.roundRect(s.x*GS+1,s.y*GS+1,GS-2,GS-2,4);ctx.fill();if(i===0){ctx.fillStyle='${t.bg}';const ex=dir.x===1?GS-6:dir.x===-1?4:dir.y===0?GS/2:dir.y===1?GS-6:4;const ey=dir.y===1?GS-6:dir.y===-1?4:dir.x===0?GS/2:dir.x===1?GS/2-2:GS/2-2;ctx.beginPath();ctx.arc(s.x*GS+ex,s.y*GS+ey,2,0,Math.PI*2);ctx.fill()}})}`,
2223
+ `function end(){clearInterval(lp);sfxFail();const nb=saveBest(sc);$('hi').textContent=best;$('fin').textContent=sc;if(nb&&sc>0){sfxWin();burstAt($('fin'),30)}const r=sc>=30?'Legend!':sc>=20?'Amazing!':sc>=10?'Great!':sc>=5?'Good!':'Try again!';$('msg').innerHTML=r+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2224
+ `document.addEventListener('keydown',e=>{if(e.key==='ArrowUp'){e.preventDefault();sd(0,-1)}if(e.key==='ArrowDown'){e.preventDefault();sd(0,1)}if(e.key==='ArrowLeft')sd(-1,0);if(e.key==='ArrowRight')sd(1,0)});`,
2034
2225
  `let tx,ty;cv.addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});cv.addEventListener('touchend',e=>{const dx=e.changedTouches[0].clientX-tx,dy=e.changedTouches[0].clientY-ty;if(Math.abs(dx)>Math.abs(dy))sd(dx>0?1:-1,0);else sd(0,dy>0?1:-1)},{passive:true});`,
2035
2226
  ].join('\n');
2036
2227
 
2037
- return htmlWrap(title, css, html, js);
2228
+ return assemblePage(title, css, html, js);
2038
2229
  }
2039
2230
 
2040
- // ── Reaction Game Builder ──
2041
-
2042
- function buildReaction(title, opts) {
2043
- const p = GAME_PALETTES.reaction;
2231
+ function buildReaction(title, t, opts) {
2044
2232
  const rounds = opts.difficulty === 'hard' ? 10 : 5;
2045
2233
 
2046
- const css = [cssVars(p), cssBase(p),
2234
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t),
2047
2235
  `.zone{width:min(340px,85vw);height:min(340px,85vw);border-radius:24px;display:flex;align-items:center;justify-content:center;margin:1.5rem auto;cursor:pointer;font-size:1.2rem;font-weight:600;transition:background .2s}`,
2048
- `.zone.wait{background:${p.sf}}`,
2049
- `.zone.ready{background:${p.pr};color:#fff}`,
2050
- `.zone.go{background:${p.ac};color:${p.bg}}`,
2236
+ `.zone.wait{background:${t.sf}}`,
2237
+ `.zone.ready{background:${t.pr};color:#fff}`,
2238
+ `.zone.go{background:${t.ac};color:${t.bg}}`,
2051
2239
  `.zone.early{background:#DC2626}`,
2052
2240
  `.times{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:1rem 0}`,
2053
- `.times span{background:${p.sf};padding:.3rem .6rem;border-radius:6px;font-size:.85rem;font-variant-numeric:tabular-nums}`,
2241
+ `.times span{background:${t.sf};padding:.3rem .6rem;border-radius:6px;font-size:.85rem;font-variant-numeric:tabular-nums}`,
2054
2242
  `.ms{font-size:3.5rem;font-weight:900;font-variant-numeric:tabular-nums}`,
2055
2243
  ].join('\n');
2056
2244
 
2057
- const html = `<div id="s-menu">
2058
- <p style="color:${p.mt};margin:1rem 0">Tap when it turns green — ${rounds} rounds</p>
2059
- <div class="st"><span>Best Avg<b id="best">-</b></span></div>
2060
- <button class="btn" onclick="go()">Start</button></div>
2061
- <div id="s-play" class="hd">
2062
- <div class="st"><span>Round<b id="rnd">1/${rounds}</b></span><span>Avg<b id="avg">-</b></span></div>
2063
- <div class="zone wait" id="zone" onmousedown="tap()" ontouchstart="tap()"><span id="ztxt">Wait...</span></div>
2064
- <div class="times" id="times"></div></div>
2065
- <div id="s-end" class="hd">
2066
- <div class="ms" id="fin">0</div>
2067
- <p style="font-size:1.2rem;margin:.5rem 0">ms average</p>
2068
- <div id="msg" style="margin:1rem 0"></div>
2069
- <div class="times" id="ftimes"></div>
2070
- <button class="btn" style="margin-top:1rem" onclick="go()">Try Again</button></div>`;
2071
-
2072
- const js = [jsCore('react_best'),
2245
+ const html = [
2246
+ htmlScr('menu', [
2247
+ htmlP(`Tap when it turns green — ${rounds} rounds`, `color:${t.mt};margin:1rem 0`),
2248
+ htmlStats([{label:'Best Avg',id:'best',value:'-'}]),
2249
+ htmlBtn('Start', 'go()'),
2250
+ ].join('\n'), true),
2251
+ htmlScr('play', [
2252
+ htmlStats([{label:'Round',id:'rnd',value:`1/${rounds}`},{label:'Avg',id:'avg',value:'-'}]),
2253
+ `<div class="zone wait" id="zone" onmousedown="tap()" ontouchstart="tap()"><span id="ztxt">Wait...</span></div>`,
2254
+ `<div class="times" id="times"></div>`,
2255
+ ].join('\n')),
2256
+ htmlScr('end', [
2257
+ `<div class="ms" id="fin">0</div>`,
2258
+ htmlP('ms average', 'font-size:1.2rem;margin:.5rem 0'),
2259
+ `<div id="msg" style="margin:1rem 0"></div>`,
2260
+ `<div class="times" id="ftimes"></div>`,
2261
+ htmlBtn('Try Again', 'go()'),
2262
+ ].join('\n')),
2263
+ ].join('\n');
2264
+
2265
+ const js = [jsHelper(), jsScreen(), jsStore('react'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac),
2073
2266
  `if(best)$('best').textContent=best+'ms';`,
2074
2267
  `const RDS=${rounds};let rd,res,phase,gAt,tmr;`,
2075
2268
  `function go(){rd=0;res=[];$('times').innerHTML='';show('play');nr()}`,
2076
- `function nr(){if(rd>=RDS)return end();$('rnd').textContent=(rd+1)+'/'+RDS;const z=$('zone');z.className='zone ready';$('ztxt').textContent='Wait for green...';phase='wait';clearTimeout(tmr);tmr=setTimeout(()=>{z.className='zone go';$('ztxt').textContent='TAP NOW!';gAt=Date.now();phase='go'},1500+Math.random()*3000)}`,
2077
- `function tap(){if(phase==='wait'){clearTimeout(tmr);$('zone').className='zone early';$('ztxt').textContent='Too early! Tap to retry';phase='early'}else if(phase==='early'){nr()}else if(phase==='go'){const ms=Date.now()-gAt;res.push(ms);const t=document.createElement('span');t.textContent=ms+'ms';$('times').appendChild(t);$('avg').textContent=Math.round(res.reduce((a,b)=>a+b,0)/res.length)+'ms';rd++;phase='done';setTimeout(nr,600)}}`,
2078
- `function end(){const avg=Math.round(res.reduce((a,b)=>a+b,0)/res.length);const nb=saveBest(avg);if(nb)$('best').textContent=avg+'ms';$('fin').textContent=avg;$('ftimes').innerHTML=res.map((r,i)=>'<span>R'+(i+1)+': '+r+'ms</span>').join('');const r=avg<=200?'Superhuman!':avg<=250?'Lightning!':avg<=300?'Fast!':avg<=400?'Average':'Keep practicing!';$('msg').innerHTML='<span style="font-size:1.2rem">'+r+'</span>'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2269
+ `function nr(){if(rd>=RDS)return end();$('rnd').textContent=(rd+1)+'/'+RDS;const z=$('zone');z.className='zone ready';$('ztxt').textContent='Wait for green...';phase='wait';clearTimeout(tmr);tmr=setTimeout(()=>{z.className='zone go';$('ztxt').textContent='TAP NOW!';sfxTick();gAt=Date.now();phase='go'},1500+Math.random()*3000)}`,
2270
+ `function tap(){if(phase==='wait'){clearTimeout(tmr);sfxFail();shakeEl($('zone'));$('zone').className='zone early';$('ztxt').textContent='Too early! Tap to retry';phase='early'}else if(phase==='early'){nr()}else if(phase==='go'){const ms=Date.now()-gAt;res.push(ms);sfxScore();burstAt($('zone'),ms<=250?20:8);const t=document.createElement('span');t.textContent=ms+'ms';if(ms<=200)t.style.color='${t.ac}';$('times').appendChild(t);$('avg').textContent=Math.round(res.reduce((a,b)=>a+b,0)/res.length)+'ms';rd++;phase='done';setTimeout(nr,600)}}`,
2271
+ `function end(){const avg=Math.round(res.reduce((a,b)=>a+b,0)/res.length);const nb=saveBest(avg);if(nb){$('best').textContent=avg+'ms';sfxWin();burstAt($('fin'),30)}$('fin').textContent=avg;$('ftimes').innerHTML=res.map((r,i)=>'<span>R'+(i+1)+': '+r+'ms</span>').join('');const r=avg<=200?'Superhuman!':avg<=250?'Lightning!':avg<=300?'Fast!':avg<=400?'Average':'Keep practicing!';$('msg').innerHTML='<span style="font-size:1.2rem">'+r+'</span>'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2079
2272
  ].join('\n');
2080
2273
 
2081
- return htmlWrap(title, css, html, js);
2274
+ return assemblePage(title, css, html, js);
2082
2275
  }
2083
2276
 
2084
- // ── Typing Game Builder ──
2085
-
2086
- function buildTyping(title, opts) {
2087
- const p = GAME_PALETTES.typing;
2277
+ function buildTyping(title, t, opts) {
2088
2278
  const dur = opts.duration || 30;
2089
2279
 
2090
- const css = [cssVars(p), cssBase(p), cssTimerBar(p),
2280
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssBar(t), cssFx(t), cssInput(t),
2091
2281
  `.wd{font-size:2rem;font-weight:700;letter-spacing:.1em;margin:1.5rem 0;min-height:3rem;display:flex;justify-content:center;gap:2px;flex-wrap:wrap}`,
2092
- `.wd .c{transition:color .1s}.wd .y{color:${p.ac}}.wd .n{color:#F87171}`,
2093
- `.wd .cur{border-bottom:3px solid ${p.pr};animation:blink 1s step-end infinite}`,
2282
+ `.wd .c{transition:color .1s}.wd .y{color:${t.ac}}.wd .n{color:#F87171}`,
2283
+ `.wd .cur{border-bottom:3px solid ${t.pr};animation:blink 1s step-end infinite}`,
2094
2284
  `@keyframes blink{50%{border-color:transparent}}`,
2095
- `.ti{background:${p.sf};border:2px solid ${p.pr};border-radius:12px;padding:.75rem 1rem;font-size:1.2rem;color:${p.tx};width:100%;text-align:center;outline:none;caret-color:${p.pr};font-family:inherit}`,
2096
- `.wq{color:${p.mt};font-size:1rem;margin:.5rem 0;height:2em;overflow:hidden;display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}`,
2285
+ `.wq{color:${t.mt};font-size:1rem;margin:.5rem 0;height:2em;overflow:hidden;display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}`,
2097
2286
  ].join('\n');
2098
2287
 
2099
- const html = `<div id="s-menu">
2100
- <p style="color:${p.mt};margin:1rem 0">Type as many words as you can in ${dur} seconds</p>
2101
- <div class="st"><span>Best WPM<b id="best">0</b></span></div>
2102
- <button class="btn" onclick="go()">Start</button></div>
2103
- <div id="s-play" class="hd">
2104
- <div class="st"><span>Time<b id="timer">${dur}</b></span><span>Words<b id="wc">0</b></span><span>WPM<b id="wpm">0</b></span></div>
2105
- <div class="bar"><div class="fl" id="bar" style="width:100%"></div></div>
2106
- <div class="wd" id="disp"></div>
2107
- <div class="wq" id="queue"></div>
2108
- <input class="ti" id="inp" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" placeholder="Type here..."></div>
2109
- <div id="s-end" class="hd">
2110
- <div class="sc" id="fin">0</div>
2111
- <p style="font-size:1.2rem;margin:.5rem 0">WPM</p>
2112
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2113
- <button class="btn" onclick="go()">Play Again</button></div>`;
2114
-
2115
- const js = [jsCore('type_best'),
2288
+ const html = [
2289
+ htmlScr('menu', [
2290
+ htmlP(`Type as many words as you can in ${dur} seconds`, `color:${t.mt};margin:1rem 0`),
2291
+ htmlStats([{label:'Best WPM',id:'best',value:'0'}]),
2292
+ htmlBtn('Start', 'go()'),
2293
+ ].join('\n'), true),
2294
+ htmlScr('play', [
2295
+ htmlStats([{label:'Time',id:'timer',value:dur},{label:'Words',id:'wc',value:'0'},{label:'WPM',id:'wpm',value:'0'}]),
2296
+ htmlBar(),
2297
+ `<div class="wd" id="disp"></div>`,
2298
+ `<div class="wq" id="queue"></div>`,
2299
+ `<input class="input" id="inp" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" placeholder="Type here...">`,
2300
+ ].join('\n')),
2301
+ htmlScr('end', [
2302
+ htmlDsp('fin', '0'),
2303
+ htmlP('WPM', 'font-size:1.2rem;margin:.5rem 0'),
2304
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2305
+ htmlBtn('Play Again', 'go()'),
2306
+ ].join('\n')),
2307
+ ].join('\n');
2308
+
2309
+ const js = [jsHelper(), jsScreen(), jsStore('type'), jsScore(), jsTimer(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac), jsShuffle(),
2116
2310
  `const WDS=["the","be","to","and","in","that","have","for","not","with","you","this","but","from","they","say","she","or","will","my","one","all","would","there","what","so","up","out","if","about","who","get","which","go","when","make","can","like","time","just","know","take","people","into","year","good","some","could","them","see","other","than","then","now","look","only","come","its","over","think","also","back","after","use","two","how","work","first","well","even","new","want","give","most","find","here","thing","code","fast","type","game","play","next","open","read","test","data","file","line","word","text","long","name","part","best","help","made","move","hand","high","keep","last","same","tell","set","each","much","turn","real","show","full","form","left","start","might","run","need","home","life","old","big","end","point","still","call","live","away","right","hard","plan","team","late","mind","wait","stop","must","land","close","draw","press","mark","step","pick","rock","blue","deep","dark","hold","four","week","room","free","fall","base","rest","less","note","hear"];`,
2117
- `const DUR=${dur*1000};let pool,ci,pos,done,t0,tmr;`,
2118
- `function shuf(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}`,
2119
- `function go(){pool=shuf([...WDS]);ci=0;pos=0;done=0;t0=Date.now();$('wc').textContent=0;$('wpm').textContent=0;$('inp').value='';show('play');$('inp').focus();render();tmr=setInterval(tick,100)}`,
2311
+ `const DUR=${dur*1000};let pool,ci,pos,done,t0,tmr,tried=0,streak=0;`,
2312
+ `function go(){show('play');countdown(()=>{pool=shuf([...WDS]);ci=0;pos=0;done=0;tried=0;streak=0;t0=Date.now();$('wc').textContent=0;$('wpm').textContent=0;$('inp').value='';$('inp').focus();render();tmr=setInterval(tick,100)})}`,
2120
2313
  `function render(){const w=pool[ci%pool.length];const v=$('inp').value;$('disp').innerHTML=w.split('').map((c,i)=>{let cl=i<pos?(v[i]===c?'y':'n'):'';if(i===pos)cl+=' cur';return'<span class="c '+cl+'">'+c+'</span>'}).join('');$('queue').innerHTML='';for(let i=1;i<=5;i++)$('queue').innerHTML+='<span>'+pool[(ci+i)%pool.length]+'</span>'}`,
2121
- `function tick(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('timer').textContent=Math.max(0,Math.ceil((DUR-e)/1000));if(e>=DUR)end()}`,
2122
- `$('inp').addEventListener('input',function(){if(Date.now()-t0>DUR)return;const w=pool[ci%pool.length],v=this.value;pos=v.length;if(v.endsWith(' ')||v.length>=w.length){if(v.trim()===w){done++;$('wc').textContent=done;$('wpm').textContent=Math.round(done/((Date.now()-t0)/60000))}ci++;pos=0;this.value=''}render()});`,
2123
- `function end(){clearInterval(tmr);$('inp').blur();const wpm=Math.round(done/(DUR/60000));const nb=saveBest(wpm);$('fin').textContent=wpm;const r=wpm>=80?'Blazing!':wpm>=60?'Excellent!':wpm>=40?'Good!':wpm>=20?'Decent!':'Keep practicing!';$('msg').innerHTML=r+'<br>'+done+' words in ${dur}s'+(nb&&wpm>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2314
+ `function tick(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('timer').textContent=Math.max(0,Math.ceil((DUR-e)/1000));$('wpm').textContent=Math.round(done/Math.max(.01,(e/60000)));if(e>=DUR)end()}`,
2315
+ `$('inp').addEventListener('input',function(){if(!t0||Date.now()-t0>DUR)return;const w=pool[ci%pool.length],v=this.value;pos=v.length;if(v.endsWith(' ')||v.length>=w.length){tried++;if(v.trim()===w){done++;streak++;sfxScore();if(streak>=3){sfxCombo();burstAt($('disp'),12)}$('wc').textContent=done}else{streak=0;sfxHit();shakeEl($('disp'))}ci++;pos=0;this.value=''}render()});`,
2316
+ `function end(){clearInterval(tmr);$('inp').blur();const wpm=Math.round(done/(DUR/60000));const acc=tried>0?Math.round(done/tried*100):0;const nb=saveBest(wpm);if(nb&&wpm>0){sfxWin();burstAt($('fin'),30)}else sfxFail();$('fin').textContent=wpm;const r=wpm>=80?'Blazing!':wpm>=60?'Excellent!':wpm>=40?'Good!':wpm>=20?'Decent!':'Keep practicing!';$('msg').innerHTML=r+'<br>'+done+' words in ${dur}s — '+acc+'% accuracy'+(nb&&wpm>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2124
2317
  ].join('\n');
2125
2318
 
2126
- return htmlWrap(title, css, html, js);
2319
+ return assemblePage(title, css, html, js);
2127
2320
  }
2128
2321
 
2129
- // ── Breakout Game Builder ──
2130
-
2131
- function buildBreakout(title, opts) {
2132
- const p = GAME_PALETTES.breakout;
2322
+ function buildBreakout(title, t, opts) {
2133
2323
  const COLS = 8, ROWS = opts.difficulty === 'hard' ? 7 : 5;
2134
2324
 
2135
- const css = [cssVars(p), cssBase(p),
2136
- `canvas{border-radius:12px;background:${p.sf};display:block;margin:1rem auto;touch-action:none}`,
2325
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t), cssCanvas(t)].join('\n');
2326
+
2327
+ const html = [
2328
+ htmlScr('menu', [
2329
+ htmlP(`Move the paddle to break all ${COLS * ROWS} blocks`, `color:${t.mt};margin:1rem 0`),
2330
+ htmlStats([{label:'Best',id:'best',value:'0'}]),
2331
+ htmlBtn('Play', 'go()'),
2332
+ ].join('\n'), true),
2333
+ htmlScr('play', [
2334
+ htmlStats([{label:'Score',id:'score',value:'0'},{label:'Lives',id:'lives',value:'3'}]),
2335
+ `<canvas id="cv" width="360" height="480"></canvas>`,
2336
+ ].join('\n')),
2337
+ htmlScr('end', [
2338
+ htmlDsp('fin', '0'),
2339
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2340
+ htmlBtn('Play Again', 'go()'),
2341
+ ].join('\n')),
2137
2342
  ].join('\n');
2138
2343
 
2139
- const html = `<div id="s-menu">
2140
- <p style="color:${p.mt};margin:1rem 0">Move the paddle to break all ${COLS * ROWS} blocks</p>
2141
- <div class="st"><span>Best<b id="best">0</b></span></div>
2142
- <button class="btn" onclick="go()">Play</button></div>
2143
- <div id="s-play" class="hd">
2144
- <div class="st"><span>Score<b id="score">0</b></span><span>Lives<b id="lives">3</b></span></div>
2145
- <canvas id="cv" width="360" height="480"></canvas></div>
2146
- <div id="s-end" class="hd">
2147
- <div class="sc" id="fin">0</div>
2148
- <div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>
2149
- <button class="btn" onclick="go()">Play Again</button></div>`;
2150
-
2151
- const brickColors = `['${p.pr}','${p.sc}','#A78BFA','${p.ac}','#EC4899']`;
2152
- const js = [jsCore('brk_best'),
2344
+ const brickColors = `['${t.pr}','${t.sc}','#A78BFA','${t.ac}','#EC4899']`;
2345
+ const js = [jsHelper(), jsScreen(), jsStore('brk'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac),
2153
2346
  `const cv=$('cv'),ctx=cv.getContext('2d'),CW=360,CH=480,COLS=${COLS},ROWS=${ROWS},BW=CW/COLS-4,BH=18,PW=70,PH=12,BR=6;`,
2154
2347
  `const BC=${brickColors};`,
2155
- `let px,ball,dx,dy,bricks,sc,lives,raf;`,
2348
+ `let px,ball,dx,dy,bricks,sc,lives,raf,cmb=0;`,
2156
2349
  `function mkBricks(){bricks=[];for(let r=0;r<ROWS;r++)for(let c=0;c<COLS;c++)bricks.push({x:c*(BW+4)+2,y:r*(BH+4)+40,w:BW,h:BH,color:BC[r%BC.length],alive:true})}`,
2157
- `function go(){px=CW/2;ball={x:CW/2,y:CH-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5;sc=0;lives=3;mkBricks();$('score').textContent=0;$('lives').textContent=3;show('play');cancelAnimationFrame(raf);tick()}`,
2350
+ `function go(){px=CW/2;ball={x:CW/2,y:CH-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5;sc=0;lives=3;cmb=0;mkBricks();$('score').textContent=0;$('lives').textContent=3;show('play');cancelAnimationFrame(raf);tick()}`,
2158
2351
  `function tick(){update();draw();if(lives>0&&bricks.some(b=>b.alive))raf=requestAnimationFrame(tick);else end()}`,
2159
- `function update(){ball.x+=dx;ball.y+=dy;if(ball.x<=BR||ball.x>=CW-BR)dx=-dx;if(ball.y<=BR)dy=-dy;if(ball.y>=CH-PH-BR-8&&ball.y<=CH-8&&ball.x>=px-PW/2&&ball.x<=px+PW/2){dy=-Math.abs(dy);dx+=((ball.x-px)/(PW/2))*2}if(ball.y>CH){lives--;$('lives').textContent=lives;if(lives>0){ball={x:CW/2,y:CH-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5}}for(const b of bricks){if(!b.alive)continue;if(ball.x+BR>b.x&&ball.x-BR<b.x+b.w&&ball.y+BR>b.y&&ball.y-BR<b.y+b.h){b.alive=false;dy=-dy;sc+=10;$('score').textContent=sc}}}`,
2160
- `function draw(){ctx.clearRect(0,0,CW,CH);for(const b of bricks){if(!b.alive)continue;ctx.fillStyle=b.color;ctx.beginPath();ctx.roundRect(b.x,b.y,b.w,b.h,4);ctx.fill()}ctx.fillStyle='${p.pr}';ctx.beginPath();ctx.roundRect(px-PW/2,CH-PH-8,PW,PH,6);ctx.fill();ctx.fillStyle='${p.ac}';ctx.beginPath();ctx.arc(ball.x,ball.y,BR,0,Math.PI*2);ctx.fill()}`,
2161
- `function end(){cancelAnimationFrame(raf);const won=!bricks.some(b=>b.alive);const nb=saveBest(sc);$('fin').textContent=sc;$('msg').innerHTML=(won?'You cleared it!':'Game Over')+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2352
+ `function update(){ball.x+=dx;ball.y+=dy;if(ball.x<=BR||ball.x>=CW-BR)dx=-dx;if(ball.y<=BR)dy=-dy;if(ball.y>=CH-PH-BR-8&&ball.y<=CH-8&&ball.x>=px-PW/2&&ball.x<=px+PW/2){dy=-Math.abs(dy);dx+=((ball.x-px)/(PW/2))*2;sfxTap();cmb=0}if(ball.y>CH){lives--;sfxFail();$('lives').textContent=lives;cmb=0;if(lives>0){ball={x:CW/2,y:CH-60};dx=3*(Math.random()>.5?1:-1);dy=-3.5}}let hitAny=false;for(const b of bricks){if(!b.alive)continue;if(ball.x+BR>b.x&&ball.x-BR<b.x+b.w&&ball.y+BR>b.y&&ball.y-BR<b.y+b.h){b.alive=false;dy=-dy;cmb++;const pts=10*(cmb>=5?3:cmb>=3?2:1);sc+=pts;hitAny=true;sfxHit();const rc=cv.getBoundingClientRect();emitParticles(rc.left+b.x*(rc.width/CW)+BW/2,rc.top+b.y*(rc.height/CH)+BH/2,cmb>=3?12:6,b.color);$('score').textContent=sc;if(cmb>=5){const sp=Math.sqrt(dx*dx+dy*dy);if(sp<7){dx*=1.05;dy*=1.05}}}}}`,
2353
+ `function draw(){ctx.clearRect(0,0,CW,CH);for(const b of bricks){if(!b.alive)continue;ctx.fillStyle=b.color;ctx.beginPath();ctx.roundRect(b.x,b.y,b.w,b.h,4);ctx.fill();ctx.fillStyle='rgba(255,255,255,.15)';ctx.fillRect(b.x+2,b.y+2,b.w-4,BH/3)}ctx.fillStyle='${t.pr}';ctx.shadowColor='${t.pr}';ctx.shadowBlur=10;ctx.beginPath();ctx.roundRect(px-PW/2,CH-PH-8,PW,PH,6);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='${t.ac}';ctx.shadowColor='${t.ac}';ctx.shadowBlur=6;ctx.beginPath();ctx.arc(ball.x,ball.y,BR,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0}`,
2354
+ `function end(){cancelAnimationFrame(raf);const won=!bricks.some(b=>b.alive);const nb=saveBest(sc);if(won||nb)sfxWin();$('fin').textContent=sc;if(nb&&sc>0)burstAt($('fin'),30);$('msg').innerHTML=(won?'You cleared it!':'Game Over')+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2162
2355
  `cv.addEventListener('mousemove',e=>{const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.clientX-r.left)*(CW/r.width)))});`,
2163
2356
  `cv.addEventListener('touchmove',e=>{e.preventDefault();const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.touches[0].clientX-r.left)*(CW/r.width)))},{passive:false});`,
2164
2357
  `document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft')px=Math.max(PW/2,px-20);if(e.key==='ArrowRight')px=Math.min(CW-PW/2,px+20)});`,
2165
2358
  ].join('\n');
2166
2359
 
2167
- return htmlWrap(title, css, html, js);
2360
+ return assemblePage(title, css, html, js);
2361
+ }
2362
+
2363
+ function buildDodge(title, t, opts) {
2364
+ const spd = opts.difficulty === 'hard' ? 1.4 : opts.difficulty === 'easy' ? 0.7 : 1.0;
2365
+
2366
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t), cssCanvas(t)].join('\n');
2367
+
2368
+ const html = [
2369
+ htmlScr('menu', [
2370
+ htmlP('Move to dodge the falling obstacles!', `color:${t.mt};margin:1rem 0`),
2371
+ htmlStats([{label:'Best',id:'best',value:'0'}]),
2372
+ htmlBtn('Play', 'go()'),
2373
+ ].join('\n'), true),
2374
+ htmlScr('play', [
2375
+ htmlStats([{label:'Score',id:'score',value:'0'},{label:'Best',id:'hi',value:'0'}]),
2376
+ `<canvas id="cv" width="360" height="520"></canvas>`,
2377
+ ].join('\n')),
2378
+ htmlScr('end', [
2379
+ htmlDsp('fin', '0'),
2380
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2381
+ htmlBtn('Play Again', 'go()'),
2382
+ ].join('\n')),
2383
+ ].join('\n');
2384
+
2385
+ const js = [jsHelper(), jsScreen(), jsStore('dodge'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac),
2386
+ `const cv=$('cv'),ctx=cv.getContext('2d'),CW=360,CH=520,PW=30,PH=30,SPD_MUL=${spd};`,
2387
+ `let px,py,obs,sc,raf,t0,spd,spawnT,alive;$('hi').textContent=best;`,
2388
+ `function go(){show('play');countdown(startGame)}`,
2389
+ `function startGame(){px=CW/2;py=CH-60;obs=[];sc=0;spd=2*SPD_MUL;spawnT=0;alive=true;t0=Date.now();$('score').textContent=0;cancelAnimationFrame(raf);tick()}`,
2390
+ `function tick(){if(!alive)return;update();draw();raf=requestAnimationFrame(tick)}`,
2391
+ `function update(){spawnT++;const diff=Math.min(30,Math.floor((Date.now()-t0)/5000));if(spawnT%(Math.max(8,30-diff*2))===0){const w=20+Math.random()*30;obs.push({x:Math.random()*(CW-w),y:-30,w,h:20+Math.random()*10,s:spd+Math.random()*2+diff*0.3,c:['${t.pr}','${t.sc}','#FF4757','#FF6B81','#FFA502'][Math.floor(Math.random()*5)]})}obs.forEach(o=>o.y+=o.s);obs=obs.filter(o=>{if(o.y>CH){sc++;$('score').textContent=sc;if(sc%10===0)sfxScore();return false}return true});for(const o of obs){if(px+PW/2>o.x&&px-PW/2<o.x+o.w&&py+PH/2>o.y&&py-PH/2<o.y+o.h){alive=false;sfxFail();const rc=cv.getBoundingClientRect();emitParticles(rc.left+px*(rc.width/CW),rc.top+py*(rc.height/CH),25);end();return}}spd=2*SPD_MUL+Math.floor((Date.now()-t0)/10000)*0.5}`,
2392
+ `function draw(){ctx.fillStyle='${t.sf}';ctx.fillRect(0,0,CW,CH);for(let i=0;i<CW;i+=40)for(let j=0;j<CH;j+=40){ctx.fillStyle='rgba(255,255,255,.01)';if((Math.floor(i/40)+Math.floor(j/40))%2===0)ctx.fillRect(i,j,40,40)}obs.forEach(o=>{ctx.fillStyle=o.c;ctx.beginPath();ctx.roundRect(o.x,o.y,o.w,o.h,4);ctx.fill()});ctx.fillStyle='${t.ac}';ctx.shadowColor='${t.ac}';ctx.shadowBlur=12;ctx.beginPath();ctx.arc(px,py,PW/2,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='${t.bg}';ctx.beginPath();ctx.arc(px-5,py-4,3,0,Math.PI*2);ctx.arc(px+5,py-4,3,0,Math.PI*2);ctx.fill();ctx.strokeStyle='${t.bg}';ctx.lineWidth=2;ctx.beginPath();ctx.arc(px,py+2,6,0,Math.PI);ctx.stroke()}`,
2393
+ `function end(){cancelAnimationFrame(raf);const nb=saveBest(sc);$('hi').textContent=best;$('fin').textContent=sc;if(nb&&sc>0){sfxWin();burstAt($('fin'),30)}const r=sc>=100?'Untouchable!':sc>=60?'Amazing!':sc>=30?'Great!':sc>=15?'Good!':'Keep dodging!';$('msg').innerHTML=r+(nb&&sc>0?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2394
+ `let keys={};document.addEventListener('keydown',e=>{keys[e.key]=true;e.preventDefault()});document.addEventListener('keyup',e=>keys[e.key]=false);`,
2395
+ `setInterval(()=>{if(!alive)return;const s=5;if(keys.ArrowLeft||keys.a)px=Math.max(PW/2,px-s);if(keys.ArrowRight||keys.d)px=Math.min(CW-PW/2,px+s);if(keys.ArrowUp||keys.w)py=Math.max(PH/2,py-s);if(keys.ArrowDown||keys.s)py=Math.min(CH-PH/2,py+s)},16);`,
2396
+ `let dragging=false;cv.addEventListener('touchstart',e=>{dragging=true;const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.touches[0].clientX-r.left)*(CW/r.width)));py=Math.max(PH/2,Math.min(CH-PH/2,(e.touches[0].clientY-r.top)*(CH/r.height)))},{passive:true});`,
2397
+ `cv.addEventListener('touchmove',e=>{e.preventDefault();if(!dragging)return;const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.touches[0].clientX-r.left)*(CW/r.width)));py=Math.max(PH/2,Math.min(CH-PH/2,(e.touches[0].clientY-r.top)*(CH/r.height)))},{passive:false});`,
2398
+ `cv.addEventListener('touchend',()=>dragging=false,{passive:true});`,
2399
+ `cv.addEventListener('mousemove',e=>{if(!alive)return;const r=cv.getBoundingClientRect();px=Math.max(PW/2,Math.min(CW-PW/2,(e.clientX-r.left)*(CW/r.width)));py=Math.max(PH/2,Math.min(CH-PH/2,(e.clientY-r.top)*(CH/r.height)))});`,
2400
+ ].join('\n');
2401
+
2402
+ return assemblePage(title, css, html, js);
2403
+ }
2404
+
2405
+ function buildColor(title, t, opts) {
2406
+ const dur = opts.duration || 30;
2407
+ const COLORS = [
2408
+ {name:'Red',hex:'#FF4757'},{name:'Blue',hex:'#3742FA'},{name:'Green',hex:'#2ED573'},
2409
+ {name:'Yellow',hex:'#FFA502'},{name:'Purple',hex:'#8854D0'},{name:'Orange',hex:'#FF6348'},
2410
+ {name:'Pink',hex:'#FF6B81'},{name:'Cyan',hex:'#18DCFF'},
2411
+ ];
2412
+
2413
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssBar(t), cssFx(t),
2414
+ `.clr-name{font-size:2.8rem;font-weight:900;margin:1.5rem 0;letter-spacing:.05em;transition:transform .1s}`,
2415
+ `.opts{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:1rem 0}`,
2416
+ `.opts div{padding:1.2rem;border-radius:14px;cursor:pointer;font-size:1.1rem;font-weight:700;text-align:center;transition:transform .1s,box-shadow .2s;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.4)}`,
2417
+ `.opts div:active{transform:scale(.93)}`,
2418
+ `.strk{font-size:.95rem;color:${t.ac};min-height:1.5em;font-weight:700}`,
2419
+ ].join('\n');
2420
+
2421
+ const html = [
2422
+ htmlScr('menu', [
2423
+ htmlP(`Tap the color shown, not the word! ${dur}s`, `color:${t.mt};margin:1rem 0`),
2424
+ htmlStats([{label:'Best',id:'best',value:'0'},{label:'Last',id:'last',value:'-'}]),
2425
+ htmlBtn('Start', 'go()'),
2426
+ ].join('\n'), true),
2427
+ htmlScr('play', [
2428
+ htmlStats([{label:'Score',id:'sc',value:'0'},{label:'Time',id:'timer',value:dur},{label:'Streak',id:'strk',value:'0'}]),
2429
+ htmlBar(),
2430
+ `<div class="clr-name" id="word">-</div>`,
2431
+ `<div class="strk" id="stxt"></div>`,
2432
+ `<div class="opts" id="opts"></div>`,
2433
+ ].join('\n')),
2434
+ htmlScr('end', [
2435
+ htmlDsp('fin', '0'),
2436
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2437
+ htmlBtn('Play Again', 'go()'),
2438
+ ].join('\n')),
2439
+ ].join('\n');
2440
+
2441
+ const colorsJS = JSON.stringify(COLORS);
2442
+ const js = [jsHelper(), jsScreen(), jsStore('color'), jsScore(), jsTimer(), jsSfx(), jsParticles(t.pr, t.ac), jsCountdown(t.ac), jsShuffle(),
2443
+ `const CS=${colorsJS};`,
2444
+ `const DUR=${dur*1000};let sc,t0,tmrI,streak,correct;`,
2445
+ `function go(){show('play');countdown(startGame)}`,
2446
+ `function startGame(){sc=0;streak=0;t0=Date.now();$('sc').textContent=0;$('strk').textContent=0;$('stxt').textContent='';tmrI=setInterval(tickI,100);nextRound()}`,
2447
+ `function tickI(){const e=Date.now()-t0,pc=Math.max(0,1-e/DUR)*100;$('bar').style.width=pc+'%';$('timer').textContent=Math.max(0,Math.ceil((DUR-e)/1000));if(e>=DUR)end()}`,
2448
+ `function nextRound(){const pool=shuf([...CS]);const ans=pool[0];const fakeText=pool[1+Math.floor(Math.random()*(pool.length-1))];correct=ans.name;$('word').textContent=fakeText.name;$('word').style.color=ans.hex;const choices=shuf([ans,...shuf(pool.filter(c=>c.name!==ans.name)).slice(0,3)]);const o=$('opts');o.innerHTML='';choices.forEach(c=>{const d=document.createElement('div');d.textContent=c.name;d.style.background=c.hex;d.onclick=()=>pick(c.name===correct,d);o.appendChild(d)})}`,
2449
+ `function pick(ok,el){if(Date.now()-t0>DUR)return;$('opts').querySelectorAll('div').forEach(d=>d.onclick=null);if(ok){sc++;streak++;sfxScore();$('sc').textContent=sc;$('strk').textContent=streak;burstAt(el,streak>=5?20:8);if(streak>=3){sfxCombo();$('stxt').textContent=streak>=10?'ON FIRE!':streak>=5?'AMAZING!':'STREAK!'}}else{streak=0;sfxFail();shakeEl($('word'));$('strk').textContent=0;$('stxt').textContent=''}setTimeout(nextRound,ok?300:600)}`,
2450
+ `function end(){clearInterval(tmrI);const nb=saveBest(sc);$('last').textContent=sc;$('fin').textContent=sc;if(nb&&sc>0){sfxWin();burstAt($('fin'),30)}const r=sc>=50?'Color Master!':sc>=35?'Amazing!':sc>=20?'Great!':sc>=10?'Good!':'Keep trying!';$('msg').innerHTML=r+'<br>'+sc+' in ${dur}s'+(nb?' <span class=hi>★ New Best!</span>':'');show('end')}`,
2451
+ ].join('\n');
2452
+
2453
+ return assemblePage(title, css, html, js);
2454
+ }
2455
+
2456
+ function buildPong(title, t, opts) {
2457
+ const winScore = opts.difficulty === 'hard' ? 11 : 7;
2458
+
2459
+ const css = [cssBase(t), cssBtn(t), cssScr(), cssDsp(), cssStat(t), cssFx(t),
2460
+ `canvas{border-radius:12px;background:${t.bg};display:block;margin:1rem auto;touch-action:none;border:2px solid ${t.sf}}`,
2461
+ `.vs{display:flex;justify-content:center;gap:3rem;font-size:2.5rem;font-weight:900;font-variant-numeric:tabular-nums;margin:.5rem 0}`,
2462
+ `.vs span:first-child{color:${t.pr}}.vs span:last-child{color:${t.sc}}`,
2463
+ ].join('\n');
2464
+
2465
+ const html = [
2466
+ htmlScr('menu', [
2467
+ htmlP(`First to ${winScore} wins — you vs AI`, `color:${t.mt};margin:1rem 0`),
2468
+ htmlStats([{label:'Wins',id:'best',value:'0'}]),
2469
+ htmlBtn('Play', 'go()'),
2470
+ ].join('\n'), true),
2471
+ htmlScr('play', [
2472
+ `<div class="vs"><span id="p1">0</span><span id="p2">0</span></div>`,
2473
+ `<canvas id="cv" width="360" height="420"></canvas>`,
2474
+ htmlP('Move mouse/touch or arrow keys', `color:${t.mt};font-size:.8rem;margin:.5rem`),
2475
+ ].join('\n')),
2476
+ htmlScr('end', [
2477
+ htmlDsp('fin', 'WIN'),
2478
+ `<div id="msg" style="font-size:1.1rem;margin:1rem 0"></div>`,
2479
+ htmlBtn('Rematch', 'go()'),
2480
+ ].join('\n')),
2481
+ ].join('\n');
2482
+
2483
+ const js = [jsHelper(), jsScreen(), jsStore('pong'), jsScore(), jsSfx(), jsParticles(t.pr, t.ac),
2484
+ `const cv=$('cv'),ctx=cv.getContext('2d'),CW=360,CH=420,PW=70,PH=10,BR=7,WIN=${winScore};`,
2485
+ `let py,ay,bx,by,bdx,bdy,s1,s2,raf,aiSpd;`,
2486
+ `function go(){py=CW/2;ay=CW/2;bx=CW/2;by=CH/2;bdx=3*(Math.random()>.5?1:-1);bdy=3*(Math.random()>.5?1:-1);s1=0;s2=0;aiSpd=3;$('p1').textContent=0;$('p2').textContent=0;show('play');cancelAnimationFrame(raf);tick()}`,
2487
+ `function tick(){update();draw();if(s1<WIN&&s2<WIN)raf=requestAnimationFrame(tick);else end()}`,
2488
+ `function update(){bx+=bdx;by+=bdy;if(bx<=BR||bx>=CW-BR)bdx=-bdx;if(by<=PH+BR+4&&bx>=ay-PW/2&&bx<=ay+PW/2){bdy=Math.abs(bdy);bdx+=((bx-ay)/(PW/2))*1.5;sfxTap();const rc=cv.getBoundingClientRect();emitParticles(rc.left+bx*(rc.width/CW),rc.top+by*(rc.height/CH),4,'${t.sc}')}if(by>=CH-PH-BR-4&&bx>=py-PW/2&&bx<=py+PW/2){bdy=-Math.abs(bdy);bdx+=((bx-py)/(PW/2))*1.5;sfxHit();const rc=cv.getBoundingClientRect();emitParticles(rc.left+bx*(rc.width/CW),rc.top+by*(rc.height/CH),4,'${t.pr}')}if(by<0){s2++;sfxScore();$('p1').textContent=s2;reset()}if(by>CH){s1++;sfxFail();$('p2').textContent=s1;reset()}const diff=bx-ay;ay+=Math.min(aiSpd,Math.abs(diff))*(diff>0?1:-1)*(0.7+Math.random()*0.3);ay=Math.max(PW/2,Math.min(CW-PW/2,ay));aiSpd=3+Math.max(s1,s2)*0.3}`,
2489
+ `function reset(){bx=CW/2;by=CH/2;bdx=3*(Math.random()>.5?1:-1);bdy=3*(Math.random()>.5?1:-1)}`,
2490
+ `function draw(){ctx.fillStyle='${t.bg}';ctx.fillRect(0,0,CW,CH);ctx.setLineDash([6,6]);ctx.strokeStyle='${t.sf}';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(0,CH/2);ctx.lineTo(CW,CH/2);ctx.stroke();ctx.setLineDash([]);ctx.fillStyle='${t.sc}';ctx.shadowColor='${t.sc}';ctx.shadowBlur=8;ctx.beginPath();ctx.roundRect(ay-PW/2,4,PW,PH,4);ctx.fill();ctx.fillStyle='${t.pr}';ctx.shadowColor='${t.pr}';ctx.beginPath();ctx.roundRect(py-PW/2,CH-PH-4,PW,PH,4);ctx.fill();ctx.shadowBlur=0;ctx.fillStyle='${t.ac}';ctx.shadowColor='${t.ac}';ctx.shadowBlur=10;ctx.beginPath();ctx.arc(bx,by,BR,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0}`,
2491
+ `function end(){cancelAnimationFrame(raf);const won=s1<s2;if(won){sfxWin();saveBest(best+1);$('fin').textContent='YOU WIN!';$('fin').style.color='${t.ac}';burstAt($('fin'),30)}else{sfxFail();$('fin').textContent='AI WINS';$('fin').style.color='${t.pr}'}$('msg').innerHTML=s2+' - '+s1+(won?' — Great match!':' — Try again!');show('end')}`,
2492
+ `cv.addEventListener('mousemove',e=>{const r=cv.getBoundingClientRect();py=Math.max(PW/2,Math.min(CW-PW/2,(e.clientX-r.left)*(CW/r.width)))});`,
2493
+ `cv.addEventListener('touchmove',e=>{e.preventDefault();const r=cv.getBoundingClientRect();py=Math.max(PW/2,Math.min(CW-PW/2,(e.touches[0].clientX-r.left)*(CW/r.width)))},{passive:false});`,
2494
+ `document.addEventListener('keydown',e=>{if(e.key==='ArrowLeft')py=Math.max(PW/2,py-20);if(e.key==='ArrowRight')py=Math.min(CW-PW/2,py+20)});`,
2495
+ ].join('\n');
2496
+
2497
+ return assemblePage(title, css, html, js);
2498
+ }
2499
+
2500
+ // ── App Builders ──
2501
+
2502
+ function buildCalculator(title, t, opts) {
2503
+ const css = [cssBase(t), cssBtn(t), cssFx(t),
2504
+ `.calc-dsp{background:${t.sf};border-radius:16px;padding:1.2rem;margin:1rem 0;text-align:right;min-height:4.5rem;display:flex;flex-direction:column;justify-content:center}`,
2505
+ `.calc-expr{font-size:.9rem;color:${t.mt};min-height:1.2em;word-break:break-all}`,
2506
+ `.calc-val{font-size:2.4rem;font-weight:800;font-variant-numeric:tabular-nums;word-break:break-all}`,
2507
+ `.calc-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:.5rem 0}`,
2508
+ `.calc-grid button{padding:1rem;border:none;border-radius:12px;font-size:1.2rem;font-weight:600;cursor:pointer;transition:transform .1s}`,
2509
+ `.calc-grid button:active{transform:scale(.93)}`,
2510
+ `.calc-num{background:${t.sf};color:${t.tx}}`,
2511
+ `.calc-op{background:${t.pr};color:#fff}`,
2512
+ `.calc-fn{background:${t.sf}88;color:${t.mt}}`,
2513
+ `.calc-eq{background:linear-gradient(135deg,${t.pr},${t.sc});color:#fff}`,
2514
+ opts.history ? `.calc-hist{max-height:30vh;overflow-y:auto;margin-top:.5rem}.calc-hist div{font-size:.85rem;color:${t.mt};padding:.3rem 0;border-bottom:1px solid ${t.sf};text-align:right}` : '',
2515
+ ].join('\n');
2516
+
2517
+ const histHTML = opts.history ? `<div class="calc-hist" id="hist"></div>` : '';
2518
+ const html = `<div class="calc-dsp">
2519
+ <div class="calc-expr" id="expr"></div>
2520
+ <div class="calc-val" id="val">0</div></div>
2521
+ <div class="calc-grid">
2522
+ <button class="calc-fn" onclick="clr()">C</button>
2523
+ <button class="calc-fn" onclick="pct()">%</button>
2524
+ <button class="calc-fn" onclick="neg()">±</button>
2525
+ <button class="calc-op" onclick="op('/')">÷</button>
2526
+ <button class="calc-num" onclick="num('7')">7</button>
2527
+ <button class="calc-num" onclick="num('8')">8</button>
2528
+ <button class="calc-num" onclick="num('9')">9</button>
2529
+ <button class="calc-op" onclick="op('*')">×</button>
2530
+ <button class="calc-num" onclick="num('4')">4</button>
2531
+ <button class="calc-num" onclick="num('5')">5</button>
2532
+ <button class="calc-num" onclick="num('6')">6</button>
2533
+ <button class="calc-op" onclick="op('-')">−</button>
2534
+ <button class="calc-num" onclick="num('1')">1</button>
2535
+ <button class="calc-num" onclick="num('2')">2</button>
2536
+ <button class="calc-num" onclick="num('3')">3</button>
2537
+ <button class="calc-op" onclick="op('+')">+</button>
2538
+ <button class="calc-num" style="grid-column:span 2" onclick="num('0')">0</button>
2539
+ <button class="calc-num" onclick="dot()">.</button>
2540
+ <button class="calc-eq" onclick="eq()">=</button></div>${histHTML}`;
2541
+
2542
+ const histJS = opts.history ? `function addHist(expr,res){const d=document.createElement('div');d.textContent=expr+' = '+res;$('hist').prepend(d)}` : `function addHist(){}`;
2543
+ const js = [jsHelper(),
2544
+ `let cur='0',prev='',oper='',fresh=true;`,
2545
+ `function upd(){$('val').textContent=cur;$('expr').textContent=prev+(oper?{'+':' + ','-':' − ','*':' × ','/':' ÷ '}[oper]:'')}`,
2546
+ `function num(n){if(fresh){cur='';fresh=false}if(cur==='0'&&n!=='.')cur=n;else if(cur.length<15)cur+=n;upd()}`,
2547
+ `function dot(){if(!cur.includes('.')){if(fresh){cur='0';fresh=false}cur+='.'}upd()}`,
2548
+ `function op(o){if(prev&&oper&&!fresh)eq();prev=cur;oper=o;fresh=true;upd()}`,
2549
+ `function eq(){if(!oper||!prev)return;const a=parseFloat(prev),b=parseFloat(cur);let r;if(oper==='+')r=a+b;else if(oper==='-')r=a-b;else if(oper==='*')r=a*b;else r=b!==0?a/b:'Error';const expr=prev+{'+':'+','-':'-','*':'×','/':'÷'}[oper]+cur;addHist(expr,typeof r==='number'?+r.toPrecision(12):r);prev='';oper='';cur=typeof r==='number'?String(+r.toPrecision(12)):String(r);fresh=true;upd()}`,
2550
+ `function clr(){cur='0';prev='';oper='';fresh=true;upd()}`,
2551
+ `function pct(){cur=String(parseFloat(cur)/100);upd()}`,
2552
+ `function neg(){cur=String(-parseFloat(cur));upd()}`,
2553
+ histJS,
2554
+ `document.addEventListener('keydown',e=>{if(e.key>='0'&&e.key<='9')num(e.key);else if(e.key==='.')dot();else if(e.key==='+')op('+');else if(e.key==='-')op('-');else if(e.key==='*')op('*');else if(e.key==='/')op('/');else if(e.key==='Enter'||e.key==='=')eq();else if(e.key==='Escape'||e.key==='c'||e.key==='C')clr();e.preventDefault()});`,
2555
+ ].join('\n');
2556
+
2557
+ return assemblePage(title, css, html, js);
2558
+ }
2559
+
2560
+ function buildTimerApp(title, t, opts) {
2561
+ const css = [cssBase(t), cssBtn(t), cssDsp(), cssFx(t),
2562
+ `.tmr-dsp{font-size:4rem;font-weight:200;font-variant-numeric:tabular-nums;letter-spacing:.05em;margin:2rem 0;font-family:'Courier New',monospace}`,
2563
+ `.tmr-btns{display:flex;gap:12px;justify-content:center;margin:1rem 0}`,
2564
+ `.tmr-btns .btn{padding:.6rem 1.5rem;font-size:1rem}`,
2565
+ `.lap{max-height:40vh;overflow-y:auto;margin-top:1rem}`,
2566
+ `.lap div{display:flex;justify-content:space-between;padding:.4rem .5rem;font-size:.9rem;color:${t.mt};border-bottom:1px solid ${t.sf}22}`,
2567
+ `.lap div b{color:${t.tx}}`,
2568
+ ].join('\n');
2569
+
2570
+ const html = `<div class="tmr-dsp" id="dsp">00:00.00</div>
2571
+ <div class="tmr-btns">
2572
+ <button class="btn" id="main" onclick="toggle()">Start</button>
2573
+ <button class="btn" onclick="lap()" style="background:${t.sf};color:${t.tx}">Lap</button>
2574
+ <button class="btn" onclick="rst()" style="background:${t.sf};color:${t.tx}">Reset</button></div>
2575
+ <div class="lap" id="laps"></div>`;
2576
+
2577
+ const js = [jsHelper(), jsSfx(),
2578
+ `let t0=0,elapsed=0,running=false,raf,lapN=0;`,
2579
+ `function fmt(ms){const m=Math.floor(ms/60000),s=Math.floor(ms%60000/1000),cs=Math.floor(ms%1000/10);return String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')+'.'+String(cs).padStart(2,'0')}`,
2580
+ `function upd(){$('dsp').textContent=fmt(elapsed+(running?Date.now()-t0:0))}`,
2581
+ `function loop(){upd();if(running)raf=requestAnimationFrame(loop)}`,
2582
+ `function toggle(){if(running){elapsed+=Date.now()-t0;running=false;cancelAnimationFrame(raf);$('main').textContent='Resume';sfxHit()}else{t0=Date.now();running=true;$('main').textContent='Stop';loop();sfxTap()}}`,
2583
+ `function lap(){if(!running&&elapsed===0)return;lapN++;const ms=elapsed+(running?Date.now()-t0:0);const d=document.createElement('div');d.innerHTML='<span>Lap '+lapN+'</span><b>'+fmt(ms)+'</b>';$('laps').prepend(d);sfxScore()}`,
2584
+ `function rst(){running=false;elapsed=0;lapN=0;cancelAnimationFrame(raf);$('dsp').textContent='00:00.00';$('laps').innerHTML='';$('main').textContent='Start'}`,
2585
+ `document.addEventListener('keydown',e=>{if(e.code==='Space'){e.preventDefault();toggle()}else if(e.key==='l')lap();else if(e.key==='r')rst()});`,
2586
+ ].join('\n');
2587
+
2588
+ return assemblePage(title, css, html, js);
2589
+ }
2590
+
2591
+ function buildCounter(title, t, opts) {
2592
+ const css = [cssBase(t), cssBtn(t), cssDsp(), cssFx(t), cssStat(t),
2593
+ `.cnt-val{font-size:6rem;font-weight:900;margin:1.5rem 0;font-variant-numeric:tabular-nums;transition:transform .1s}`,
2594
+ `.cnt-btns{display:flex;gap:16px;justify-content:center;margin:1rem 0}`,
2595
+ `.cnt-btns .btn{width:70px;height:70px;border-radius:50%;font-size:2rem;padding:0;display:flex;align-items:center;justify-content:center}`,
2596
+ `.cnt-rst{margin-top:1.5rem;background:${t.sf}!important;color:${t.mt}!important;padding:.5rem 1.2rem!important;font-size:.9rem!important}`,
2597
+ ].join('\n');
2598
+
2599
+ const html = [
2600
+ htmlStats([{label:'Total taps',id:'total',value:'0'}]),
2601
+ `<div class="cnt-val" id="val">0</div>`,
2602
+ `<div class="cnt-btns">`,
2603
+ `<button class="btn" onclick="dec()" style="background:${t.sf};color:${t.tx}">−</button>`,
2604
+ `<button class="btn" onclick="inc()">+</button>`,
2605
+ `</div>`,
2606
+ `<button class="btn cnt-rst" onclick="rst()">Reset</button>`,
2607
+ ].join('\n');
2608
+
2609
+ const js = [jsHelper(), jsStore('cnt'), jsSfx(), jsParticles(t.pr, t.ac),
2610
+ `let val=+(sGet('val')||0),total=+(sGet('total')||0);`,
2611
+ `$('val').textContent=val;$('total').textContent=total;`,
2612
+ `function upd(){$('val').textContent=val;sSet('val',val);$('val').style.animation='none';void $('val').offsetWidth;$('val').style.animation='pop .15s'}`,
2613
+ `function inc(){val++;total++;$('total').textContent=total;sSet('total',total);sfxTap();burstAt($('val'),4);upd()}`,
2614
+ `function dec(){val--;sfxHit();upd()}`,
2615
+ `function rst(){val=0;sfxFail();upd()}`,
2616
+ `document.addEventListener('keydown',e=>{if(e.key==='ArrowUp'||e.key==='+')inc();else if(e.key==='ArrowDown'||e.key==='-')dec();else if(e.key==='r')rst()});`,
2617
+ ].join('\n');
2618
+
2619
+ return assemblePage(title, css, html, js);
2620
+ }
2621
+
2622
+ function buildTodo(title, t, opts) {
2623
+ const css = [cssBase(t), cssBtn(t), cssInput(t), cssList(t), cssFx(t), cssStat(t),
2624
+ `.todo-add{display:flex;gap:8px;margin:1rem 0}`,
2625
+ `.todo-add .input{flex:1}`,
2626
+ `.todo-add .btn{padding:.75rem 1.2rem}`,
2627
+ `.list-item .chk{width:22px;height:22px;border-radius:6px;border:2px solid ${t.pr};background:none;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;color:${t.ac};font-size:14px;transition:background .2s}`,
2628
+ `.list-item.done .chk{background:${t.ac};border-color:${t.ac};color:#fff}`,
2629
+ `.list-item .text{flex:1;word-break:break-word}`,
2630
+ `.filters{display:flex;gap:8px;justify-content:center;margin:.5rem 0}`,
2631
+ `.filters button{background:none;border:none;color:${t.mt};cursor:pointer;font-size:.85rem;padding:.3rem .6rem;border-radius:6px}`,
2632
+ `.filters button.on{background:${t.pr};color:#fff}`,
2633
+ opts.categories ? `.cat{font-size:.7rem;padding:.15rem .4rem;border-radius:4px;background:${t.pr}33;color:${t.pr};margin-left:.3rem}` : '',
2634
+ ].join('\n');
2635
+
2636
+ const html = [
2637
+ htmlStats([{label:'Done',id:'done',value:'0'},{label:'Total',id:'total',value:'0'}]),
2638
+ `<div class="todo-add">`,
2639
+ `<input class="input" id="inp" placeholder="Add a task..." autocomplete="off">`,
2640
+ `<button class="btn" onclick="add()">+</button></div>`,
2641
+ `<div class="filters">`,
2642
+ `<button class="on" id="fAll" onclick="filt('all')">All</button>`,
2643
+ `<button id="fAct" onclick="filt('active')">Active</button>`,
2644
+ `<button id="fDon" onclick="filt('done')">Done</button></div>`,
2645
+ `<div class="list" id="list"></div>`,
2646
+ ].join('\n');
2647
+
2648
+ const js = [jsHelper(), jsStore('todo'), jsSfx(), jsParticles(t.pr, t.ac),
2649
+ `let items=sGet('items')||[],filter='all';`,
2650
+ `function render(){const f=filter==='done'?items.filter(i=>i.done):filter==='active'?items.filter(i=>!i.done):items;$('list').innerHTML='';f.forEach((item,idx)=>{const realIdx=items.indexOf(item);const d=document.createElement('div');d.className='list-item'+(item.done?' done':'');d.innerHTML='<button class="chk" onclick="tog('+realIdx+')">'+(item.done?'✓':'')+'</button><span class="text">'+item.text+'</span><button class="del" onclick="del('+realIdx+')">×</button>';$('list').appendChild(d)});$('done').textContent=items.filter(i=>i.done).length;$('total').textContent=items.length;['fAll','fAct','fDon'].forEach(id=>$(id).classList.remove('on'));$({all:'fAll',active:'fAct',done:'fDon'}[filter]).classList.add('on')}`,
2651
+ `function save(){sSet('items',items)}`,
2652
+ `function add(){const v=$('inp').value.trim();if(!v)return;items.unshift({text:v,done:false,id:Date.now()});save();render();$('inp').value='';sfxTap();burstAt($('inp'),6)}`,
2653
+ `function tog(i){items[i].done=!items[i].done;save();render();if(items[i].done){sfxScore();burstAt($('list').children[0],8)}else sfxHit()}`,
2654
+ `function del(i){items.splice(i,1);save();render();sfxFail()}`,
2655
+ `function filt(f){filter=f;render()}`,
2656
+ `$('inp').addEventListener('keydown',e=>{if(e.key==='Enter')add()});`,
2657
+ `render();`,
2658
+ ].join('\n');
2659
+
2660
+ return assemblePage(title, css, html, js);
2661
+ }
2662
+
2663
+ function buildPomodoro(title, t, opts) {
2664
+ const workMin = 25, breakMin = 5;
2665
+
2666
+ const css = [cssBase(t), cssBtn(t), cssDsp(), cssFx(t), cssStat(t),
2667
+ `.pom-ring{width:min(280px,70vw);height:min(280px,70vw);border-radius:50%;border:6px solid ${t.sf};display:flex;align-items:center;justify-content:center;flex-direction:column;margin:1.5rem auto;position:relative}`,
2668
+ `.pom-time{font-size:3.5rem;font-weight:200;font-variant-numeric:tabular-nums;font-family:monospace}`,
2669
+ `.pom-label{font-size:.9rem;color:${t.mt};margin-top:.3rem;text-transform:uppercase;letter-spacing:.1em}`,
2670
+ `.pom-ring svg{position:absolute;top:-6px;left:-6px;transform:rotate(-90deg)}`,
2671
+ `.pom-btns{display:flex;gap:12px;justify-content:center;margin:1rem 0}`,
2672
+ `.pom-btns .btn{padding:.6rem 1.5rem;font-size:1rem}`,
2673
+ ].join('\n');
2674
+
2675
+ const ringSize = 280;
2676
+ const r = (ringSize - 12) / 2;
2677
+ const circ = Math.round(2 * Math.PI * r);
2678
+
2679
+ const html = [
2680
+ htmlStats([{label:'Sessions',id:'sessions',value:'0'},{label:'Total',id:'totalMin',value:'0 min'}]),
2681
+ `<div class="pom-ring">`,
2682
+ `<svg width="${ringSize}" height="${ringSize}"><circle cx="${ringSize/2}" cy="${ringSize/2}" r="${r}" fill="none" stroke="${t.pr}" stroke-width="6" stroke-dasharray="${circ}" stroke-dashoffset="0" stroke-linecap="round" id="ring"/></svg>`,
2683
+ `<div class="pom-time" id="time">${workMin}:00</div>`,
2684
+ `<div class="pom-label" id="phase">Focus</div></div>`,
2685
+ `<div class="pom-btns">`,
2686
+ `<button class="btn" id="main" onclick="toggle()">Start</button>`,
2687
+ `<button class="btn" onclick="skip()" style="background:${t.sf};color:${t.tx}">Skip</button>`,
2688
+ `<button class="btn" onclick="rst()" style="background:${t.sf};color:${t.tx}">Reset</button></div>`,
2689
+ ].join('\n');
2690
+
2691
+ const js = [jsHelper(), jsStore('pom'), jsSfx(), jsParticles(t.pr, t.ac),
2692
+ `const WORK=${workMin*60},BRK=${breakMin*60},CIRC=${circ};`,
2693
+ `let sessions=+(sGet('sessions')||0),totalSec=+(sGet('totalSec')||0),remain=WORK,phase='work',running=false,raf,lastT;`,
2694
+ `$('sessions').textContent=sessions;$('totalMin').textContent=Math.floor(totalSec/60)+' min';`,
2695
+ `function fmt(s){return String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0')}`,
2696
+ `function upd(){const dur=phase==='work'?WORK:BRK;const pct=remain/dur;$('time').textContent=fmt(remain);$('ring').setAttribute('stroke-dashoffset',CIRC*(1-pct));$('ring').setAttribute('stroke',phase==='work'?'${t.pr}':'${t.ac}');$('phase').textContent=phase==='work'?'Focus':'Break'}`,
2697
+ `function tick(){if(!running)return;const now=Date.now();const dt=Math.floor((now-lastT)/1000);if(dt>=1){lastT=now;remain=Math.max(0,remain-dt);if(phase==='work')totalSec+=dt;upd();if(remain<=0){done()}}raf=requestAnimationFrame(tick)}`,
2698
+ `function toggle(){if(running){running=false;cancelAnimationFrame(raf);$('main').textContent='Resume';sfxHit()}else{running=true;lastT=Date.now();$('main').textContent='Pause';tick();sfxTap()}}`,
2699
+ `function done(){running=false;cancelAnimationFrame(raf);if(phase==='work'){sessions++;sSet('sessions',sessions);sSet('totalSec',totalSec);$('sessions').textContent=sessions;$('totalMin').textContent=Math.floor(totalSec/60)+' min';sfxWin();burstAt($('time'),20)}else{sfxScore();burstAt($('time'),10)}phase=phase==='work'?'break':'work';remain=phase==='work'?WORK:BRK;$('main').textContent='Start';upd()}`,
2700
+ `function skip(){running=false;cancelAnimationFrame(raf);phase=phase==='work'?'break':'work';remain=phase==='work'?WORK:BRK;$('main').textContent='Start';upd();sfxFail()}`,
2701
+ `function rst(){running=false;cancelAnimationFrame(raf);phase='work';remain=WORK;$('main').textContent='Start';upd()}`,
2702
+ `upd();`,
2703
+ ].join('\n');
2704
+
2705
+ return assemblePage(title, css, html, js);
2706
+ }
2707
+
2708
+ function buildClock(title, t, opts) {
2709
+ const css = [cssBase(t), cssDsp(), cssFx(t),
2710
+ `.clock-face{width:min(300px,75vw);height:min(300px,75vw);border-radius:50%;border:4px solid ${t.pr}44;margin:1.5rem auto;position:relative;background:${t.sf}}`,
2711
+ `.clock-face .mark{position:absolute;width:2px;height:10px;background:${t.mt};top:8px;left:50%;transform-origin:0 142px}`,
2712
+ `.clock-face .mark.hr{height:16px;width:3px;background:${t.tx}}`,
2713
+ `.hand{position:absolute;bottom:50%;left:50%;transform-origin:50% 100%;border-radius:2px}`,
2714
+ `.hand-h{width:4px;height:60px;background:${t.tx}}`,
2715
+ `.hand-m{width:3px;height:80px;background:${t.pr}}`,
2716
+ `.hand-s{width:1.5px;height:90px;background:${t.ac}}`,
2717
+ `.clock-dot{position:absolute;width:8px;height:8px;border-radius:50%;background:${t.ac};top:50%;left:50%;transform:translate(-50%,-50%);z-index:5}`,
2718
+ `.digital{font-size:2rem;font-weight:300;font-variant-numeric:tabular-nums;font-family:monospace;margin:1rem 0;letter-spacing:.1em}`,
2719
+ `.date-str{color:${t.mt};font-size:.9rem}`,
2720
+ ].join('\n');
2721
+
2722
+ const html = `<div class="clock-face" id="face"></div>
2723
+ <div class="digital" id="digital">00:00:00</div>
2724
+ <div class="date-str" id="date"></div>`;
2725
+
2726
+ const js = [jsHelper(),
2727
+ `const face=$('face');`,
2728
+ `for(let i=0;i<60;i++){const m=document.createElement('div');m.className='mark'+(i%5===0?' hr':'');m.style.transform='translateX(-50%) rotate('+i*6+'deg)';face.appendChild(m)}`,
2729
+ `['hand-h','hand-m','hand-s'].forEach(c=>{const h=document.createElement('div');h.className='hand '+c;h.id=c;face.appendChild(h)});`,
2730
+ `const dot=document.createElement('div');dot.className='clock-dot';face.appendChild(dot);`,
2731
+ `function tick(){const d=new Date();const h=d.getHours()%12,m=d.getMinutes(),s=d.getSeconds(),ms=d.getMilliseconds();const hDeg=h*30+m*.5;const mDeg=m*6+s*.1;const sDeg=s*6+ms*.006;$('hand-h').style.transform='translateX(-50%) rotate('+hDeg+'deg)';$('hand-m').style.transform='translateX(-50%) rotate('+mDeg+'deg)';$('hand-s').style.transform='translateX(-50%) rotate('+sDeg+'deg)';$('digital').textContent=String(d.getHours()).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0');$('date').textContent=d.toLocaleDateString(undefined,{weekday:'long',year:'numeric',month:'long',day:'numeric'});requestAnimationFrame(tick)}`,
2732
+ `tick();`,
2733
+ ].join('\n');
2734
+
2735
+ return assemblePage(title, css, html, js);
2736
+ }
2737
+
2738
+ function buildDrawing(title, t, opts) {
2739
+ const css = [cssBase(t), cssBtn(t), cssFx(t),
2740
+ `body{overflow:hidden}`,
2741
+ `.toolbar{display:flex;gap:6px;justify-content:center;align-items:center;flex-wrap:wrap;margin:.5rem 0}`,
2742
+ `.toolbar button{border:none;border-radius:8px;padding:.5rem .8rem;font-size:.85rem;cursor:pointer;background:${t.sf};color:${t.tx};transition:transform .1s}`,
2743
+ `.toolbar button:active{transform:scale(.93)}`,
2744
+ `.toolbar button.on{background:${t.pr};color:#fff}`,
2745
+ `.colors{display:flex;gap:4px;justify-content:center;margin:.3rem 0}`,
2746
+ `.colors div{width:28px;height:28px;border-radius:50%;cursor:pointer;border:3px solid transparent;transition:border-color .2s}`,
2747
+ `.colors div.on{border-color:${t.tx}}`,
2748
+ `.sizes{display:flex;gap:8px;align-items:center;justify-content:center;margin:.3rem 0}`,
2749
+ `.sizes div{border-radius:50%;background:${t.tx};cursor:pointer;opacity:.4;transition:opacity .2s}`,
2750
+ `.sizes div.on{opacity:1}`,
2751
+ `canvas{cursor:crosshair;border:2px solid ${t.sf};background:#fff}`,
2752
+ ].join('\n');
2753
+
2754
+ const palette = ['#111827','#ef4444','#f97316','#eab308','#22c55e','#3b82f6','#8b5cf6','#ec4899','#ffffff'];
2755
+ const sizes = [2, 5, 10, 20];
2756
+
2757
+ const html = [
2758
+ `<div class="toolbar">`,
2759
+ `<button class="on" id="tb-pen" onclick="setTool('pen')">Pen</button>`,
2760
+ `<button id="tb-eraser" onclick="setTool('eraser')">Eraser</button>`,
2761
+ `<button onclick="undo()">Undo</button>`,
2762
+ `<button onclick="clearAll()">Clear</button>`,
2763
+ `<button onclick="save()">Save</button></div>`,
2764
+ `<div class="colors" id="colors">`,
2765
+ palette.map((c,i) => `<div style="background:${c}" onclick="setColor('${c}',this)"${i===0?' class="on"':''}></div>`).join(''),
2766
+ `</div>`,
2767
+ `<div class="sizes" id="sizes">`,
2768
+ sizes.map((s,i) => `<div style="width:${s+8}px;height:${s+8}px" onclick="setSize(${s},this)"${i===1?' class="on"':''}></div>`).join(''),
2769
+ `</div>`,
2770
+ `<canvas id="cv"></canvas>`,
2771
+ ].join('\n');
2772
+
2773
+ const js = [jsHelper(), jsSfx(),
2774
+ `const cv=$('cv'),ctx=cv.getContext('2d');`,
2775
+ `let tool='pen',color='#111827',size=5,drawing=false,history=[],histIdx=-1;`,
2776
+ `function resize(){const w=Math.min(420,window.innerWidth*.92);cv.width=w;cv.height=w;ctx.fillStyle='#fff';ctx.fillRect(0,0,w,w);redraw()}window.addEventListener('resize',resize);resize();`,
2777
+ `function snapshot(){histIdx++;history=history.slice(0,histIdx);history.push(cv.toDataURL());if(history.length>50)history.shift()}`,
2778
+ `function redraw(){if(histIdx>=0){const img=new Image();img.onload=()=>{ctx.clearRect(0,0,cv.width,cv.height);ctx.drawImage(img,0,0)};img.src=history[histIdx]}}`,
2779
+ `function getPos(e){const r=cv.getBoundingClientRect();const t=e.touches?e.touches[0]:e;return{x:t.clientX-r.left,y:t.clientY-r.top}}`,
2780
+ `function startDraw(e){e.preventDefault();drawing=true;const p=getPos(e);ctx.beginPath();ctx.moveTo(p.x,p.y)}`,
2781
+ `function draw(e){if(!drawing)return;e.preventDefault();const p=getPos(e);ctx.lineTo(p.x,p.y);ctx.strokeStyle=tool==='eraser'?'#fff':color;ctx.lineWidth=tool==='eraser'?size*3:size;ctx.lineCap='round';ctx.lineJoin='round';ctx.stroke()}`,
2782
+ `function endDraw(){if(drawing){drawing=false;snapshot()}}`,
2783
+ `cv.addEventListener('mousedown',startDraw);cv.addEventListener('mousemove',draw);cv.addEventListener('mouseup',endDraw);cv.addEventListener('mouseleave',endDraw);`,
2784
+ `cv.addEventListener('touchstart',startDraw,{passive:false});cv.addEventListener('touchmove',draw,{passive:false});cv.addEventListener('touchend',endDraw);`,
2785
+ `function setTool(t){tool=t;document.querySelectorAll('.toolbar button').forEach(b=>b.classList.remove('on'));$('tb-'+t).classList.add('on');sfxTap()}`,
2786
+ `function setColor(c,el){color=c;document.querySelectorAll('.colors div').forEach(d=>d.classList.remove('on'));el.classList.add('on')}`,
2787
+ `function setSize(s,el){size=s;document.querySelectorAll('.sizes div').forEach(d=>d.classList.remove('on'));el.classList.add('on')}`,
2788
+ `function undo(){if(histIdx>0){histIdx--;redraw();sfxHit()}else if(histIdx===0){histIdx=-1;ctx.fillStyle='#fff';ctx.fillRect(0,0,cv.width,cv.height);sfxHit()}}`,
2789
+ `function clearAll(){ctx.fillStyle='#fff';ctx.fillRect(0,0,cv.width,cv.height);snapshot();sfxFail()}`,
2790
+ `function save(){const a=document.createElement('a');a.download='drawing.png';a.href=cv.toDataURL();a.click();sfxWin()}`,
2791
+ `snapshot();`,
2792
+ ].join('\n');
2793
+
2794
+ return assemblePage(title, css, html, js);
2795
+ }
2796
+
2797
+ function buildNotes(title, t, opts) {
2798
+ const css = [cssBase(t), cssBtn(t), cssInput(t), cssList(t), cssFx(t),
2799
+ `body{justify-content:flex-start;padding-top:2rem}`,
2800
+ `.note-card{background:${t.sf};border-radius:12px;padding:1rem;margin:.5rem 0;text-align:left;cursor:pointer;transition:transform .2s,box-shadow .2s;animation:fadeIn .2s ease-out}`,
2801
+ `.note-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.1)}`,
2802
+ `.note-card h3{font-size:1rem;margin-bottom:.3rem;color:${t.tx}}`,
2803
+ `.note-card p{font-size:.85rem;color:${t.mt};display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}`,
2804
+ `.note-card .time{font-size:.7rem;color:${t.mt};margin-top:.4rem}`,
2805
+ `.editor{display:none;flex-direction:column;gap:.5rem;margin:1rem 0}`,
2806
+ `.editor.on{display:flex}`,
2807
+ `.editor textarea{background:${t.sf};border:2px solid ${t.pr}44;border-radius:12px;padding:.75rem;font-size:1rem;color:${t.tx};min-height:200px;resize:vertical;outline:none;font-family:inherit;transition:border-color .2s}`,
2808
+ `.editor textarea:focus{border-color:${t.pr}}`,
2809
+ `.editor-btns{display:flex;gap:8px}`,
2810
+ `.notes-list.hide{display:none}`,
2811
+ ].join('\n');
2812
+
2813
+ const html = [
2814
+ `<div class="notes-list" id="notesList">`,
2815
+ `<div style="display:flex;gap:8px;margin:1rem 0">`,
2816
+ `<input class="input" id="search" placeholder="Search notes..." oninput="render()">`,
2817
+ `<button class="btn" onclick="newNote()" style="padding:.75rem 1rem">+</button></div>`,
2818
+ `<div id="list"></div></div>`,
2819
+ `<div class="editor" id="editor">`,
2820
+ `<input class="input" id="etitle" placeholder="Title">`,
2821
+ `<textarea id="ebody" placeholder="Write your note..."></textarea>`,
2822
+ `<div class="editor-btns">`,
2823
+ `<button class="btn" onclick="saveNote()">Save</button>`,
2824
+ `<button class="btn" onclick="cancelEdit()" style="background:${t.sf};color:${t.tx}">Cancel</button>`,
2825
+ `<button class="btn" onclick="delNote()" style="background:#ef4444;color:#fff;margin-left:auto">Delete</button></div></div>`,
2826
+ ].join('\n');
2827
+
2828
+ const js = [jsHelper(), jsStore('notes'), jsSfx(),
2829
+ `let notes=sGet('list')||[],editId=null;`,
2830
+ `function render(){const q=$('search').value.toLowerCase();const f=q?notes.filter(n=>n.title.toLowerCase().includes(q)||n.body.toLowerCase().includes(q)):notes;$('list').innerHTML='';f.forEach(n=>{const d=document.createElement('div');d.className='note-card';d.innerHTML='<h3>'+esc(n.title||'Untitled')+'</h3><p>'+esc(n.body||'')+'</p><div class="time">'+new Date(n.updated).toLocaleString()+'</div>';d.onclick=()=>openNote(n.id);$('list').appendChild(d)})}`,
2831
+ `function esc(s){return s.replace(/</g,'&lt;').replace(/>/g,'&gt;')}`,
2832
+ `function save(){sSet('list',notes)}`,
2833
+ `function newNote(){editId=null;$('etitle').value='';$('ebody').value='';$('editor').classList.add('on');$('notesList').classList.add('hide');$('etitle').focus();sfxTap()}`,
2834
+ `function openNote(id){const n=notes.find(x=>x.id===id);if(!n)return;editId=id;$('etitle').value=n.title;$('ebody').value=n.body;$('editor').classList.add('on');$('notesList').classList.add('hide')}`,
2835
+ `function saveNote(){const title=$('etitle').value.trim()||'Untitled';const body=$('ebody').value;const now=Date.now();if(editId){const n=notes.find(x=>x.id===editId);if(n){n.title=title;n.body=body;n.updated=now}}else{notes.unshift({id:now,title,body,created:now,updated:now})}save();cancelEdit();sfxScore()}`,
2836
+ `function cancelEdit(){$('editor').classList.remove('on');$('notesList').classList.remove('hide');render()}`,
2837
+ `function delNote(){if(editId){notes=notes.filter(n=>n.id!==editId);save();sfxFail()}cancelEdit()}`,
2838
+ `render();`,
2839
+ ].join('\n');
2840
+
2841
+ return assemblePage(title, css, html, js);
2168
2842
  }
2169
2843
 
2170
- // ── Game Router ──
2844
+ // ── Builder Registry ──
2171
2845
 
2172
- const GAME_BUILDERS = {
2846
+ const BUILDERS = {
2173
2847
  tapping: buildTapping, quiz: buildQuiz, memory: buildMemory,
2174
- snake: buildSnake, reaction: buildReaction, typing: buildTyping, breakout: buildBreakout,
2848
+ snake: buildSnake, reaction: buildReaction, typing: buildTyping,
2849
+ breakout: buildBreakout, dodge: buildDodge, color: buildColor, pong: buildPong,
2850
+ calculator: buildCalculator, timer: buildTimerApp, counter: buildCounter,
2851
+ todo: buildTodo, pomodoro: buildPomodoro, clock: buildClock,
2852
+ drawing: buildDrawing, notes: buildNotes,
2175
2853
  };
2176
2854
 
2855
+ function frontendHTML(type, title, rawInstruction) {
2856
+ const t = THEMES[type] || THEMES.tapping;
2857
+ const opts = extractBuildOpts(rawInstruction || '');
2858
+ const builder = BUILDERS[type];
2859
+ if (!builder) return buildTapping(title || type, t, opts);
2860
+ return builder(title || type, t, opts);
2861
+ }
2862
+
2177
2863
  function gameHTML(type, title, rawInstruction) {
2178
- const opts = extractGameOpts(rawInstruction || '');
2179
- const builder = GAME_BUILDERS[type] || buildTapping;
2180
- return builder(title || type, opts);
2864
+ return frontendHTML(type, title, rawInstruction);
2181
2865
  }
2182
2866
 
2183
- function gameNaideCode(title, port) {
2184
- return `server game port ${port}:
2867
+ function appNaideCode(title, port, type) {
2868
+ const label = GAME_TYPES[type] ? 'game' : 'app';
2869
+ return `server ${label} port ${port}:
2185
2870
  cors "*"
2186
2871
 
2187
2872
  get "/" (req, res):
2188
2873
  res.sendFile("index.html", {root: "."})
2189
2874
 
2190
2875
  get "/api/health" (req, res):
2191
- ret {status: "ok", game: "${title || 'game'}"}
2876
+ ret {status: "ok", ${label}: "${title || type || 'app'}"}
2192
2877
  `;
2193
2878
  }
2194
2879
 
2880
+
2195
2881
  function compose(intents, params) {
2196
- // Game intent: generate minimal server code (game HTML is generated separately in project mode)
2882
+ // Game/App intent: generate minimal server code (HTML is generated separately in project mode)
2197
2883
  if (params.gameType && intents.has('game') && !intents.has('crud')) {
2198
- return gameNaideCode(params.gameTitle || params.gameType, params.port);
2884
+ return appNaideCode(params.gameTitle || params.gameType, params.port, params.gameType);
2885
+ }
2886
+
2887
+ if (params.appType && intents.has('app') && !intents.has('crud')) {
2888
+ return appNaideCode(params.appTitle || params.appType, params.port, params.appType);
2199
2889
  }
2200
2890
 
2201
2891
  const sections = [];
@@ -2527,7 +3217,8 @@ function heal(code, err) {
2527
3217
  const KEYWORD_CHEATSHEET = {
2528
3218
  'Intent': 'server, api, rest, bot, cli, page, test, database, ai, crud, auth, websocket, mail, graphql, game',
2529
3219
  'Composite': 'todo, blog, chat, shop, fullstack, board, crm, inventory, booking, sns',
2530
- 'Game': 'tapping, quiz, memory, snake, typing, reaction, breakout',
3220
+ 'Game': 'tapping, quiz, memory, snake, typing, reaction, breakout, dodge, color, pong',
3221
+ 'App': 'calculator, timer, counter, todo, pomodoro, clock, drawing, notes',
2531
3222
  'Platform': 'discord, slack, telegram, line',
2532
3223
  'DB': 'sqlite, postgres',
2533
3224
  'Japanese': 'サーバー, 認証, ログイン, 会員, CRUD, 管理, データベース, ボット, テスト, ページ',
@@ -2550,12 +3241,14 @@ export function generateProject(instruction, options = {}) {
2550
3241
  const files = [];
2551
3242
  const intents = new Set(result.intents);
2552
3243
 
2553
- // Game project: generate standalone HTML game
2554
- if (intents.has('game') && result.analysis && result.analysis.gameType) {
2555
- const gt = result.analysis.gameType;
2556
- const title = result.analysis.gameTitle || gt;
2557
- const projectName = safeName(gt) + '-game';
2558
- const html = gameHTML(gt, title, instruction);
3244
+ // Game/App project: generate standalone HTML frontend
3245
+ const frontendType = (intents.has('game') && result.analysis && result.analysis.gameType) ? result.analysis.gameType
3246
+ : (intents.has('app') && result.analysis && result.analysis.appType) ? result.analysis.appType : null;
3247
+ if (frontendType) {
3248
+ const isGame = !!result.analysis.gameType;
3249
+ const title = result.analysis.gameTitle || result.analysis.appTitle || frontendType;
3250
+ const projectName = safeName(frontendType) + (isGame ? '-game' : '-app');
3251
+ const html = frontendHTML(frontendType, title, instruction);
2559
3252
 
2560
3253
  files.push({ path: 'index.html', content: html });
2561
3254
  files.push({ path: 'app.naide', content: result.code });
@@ -2565,12 +3258,17 @@ export function generateProject(instruction, options = {}) {
2565
3258
  dependencies: { express: '^4.21.0', naider: '^1.21.0' },
2566
3259
  }, null, 2) + '\n' });
2567
3260
  files.push({ path: '.gitignore', content: 'node_modules/\n' });
2568
- files.push({ path: 'README.md', content: `# ${title}\n\nGenerated by NAIDE Agent Coder (~~)\n\n## Play\n\nOpen \`index.html\` directly in a browser, or:\n\n\`\`\`bash\nnpm install && npm run dev\n\`\`\`\n\nThen visit http://localhost:${result.port}\n` });
3261
+ const verb = isGame ? 'Play' : 'Use';
3262
+ files.push({ path: 'README.md', content: `# ${title}\n\nGenerated by NAIDE Agent Coder (~~)\n\n## ${verb}\n\nOpen \`index.html\` directly in a browser, or:\n\n\`\`\`bash\nnpm install && npm run dev\n\`\`\`\n\nThen visit http://localhost:${result.port}\n` });
3263
+
3264
+ const plan = [`${isGame ? 'Game' : 'App'}: ${frontendType}`, 'Standalone HTML + CSS + JS'];
3265
+ if (isGame) plan.push('High scores saved in localStorage');
3266
+ plan.push('Touch & keyboard support', `Server on port ${result.port}`);
2569
3267
 
2570
3268
  return {
2571
3269
  ...result,
2572
3270
  files,
2573
- analysis: { ...result.analysis, plan: [`Game: ${gt}`, `Standalone HTML + CSS + JS`, `High scores saved in localStorage`, `Touch & keyboard support`, `Server on port ${result.port}`] },
3271
+ analysis: { ...result.analysis, plan },
2574
3272
  };
2575
3273
  }
2576
3274
 
@@ -3058,10 +3756,12 @@ export function expandDirectives(source) {
3058
3756
  function buildAnalysis(instruction, intents, params) {
3059
3757
  const plan = [];
3060
3758
 
3061
- if (params.gameType) {
3062
- plan.push(`Game: ${params.gameType}`);
3759
+ if (params.gameType || params.appType) {
3760
+ const type = params.gameType || params.appType;
3761
+ const isGame = !!params.gameType;
3762
+ plan.push(`${isGame ? 'Game' : 'App'}: ${type}`);
3063
3763
  plan.push('Standalone HTML + CSS + JS');
3064
- plan.push('High scores saved in localStorage');
3764
+ if (isGame) plan.push('High scores saved in localStorage');
3065
3765
  plan.push('Touch & keyboard support');
3066
3766
  return {
3067
3767
  instruction,
@@ -3071,6 +3771,8 @@ function buildAnalysis(instruction, intents, params) {
3071
3771
  plan,
3072
3772
  gameType: params.gameType,
3073
3773
  gameTitle: params.gameTitle,
3774
+ appType: params.appType,
3775
+ appTitle: params.appTitle,
3074
3776
  };
3075
3777
  }
3076
3778