PageSourceSearch

https://tibetankeys.com/timer.html

html tibetankeys.com collected 2026-09-24 11:07:29 UTC 70,367 bytes, 978 lines download raw bytes

1<!DOCTYPE html>
2<html lang="bo">
3<head>
vendor: 69 bytes, lines 3-4
3
4    <script async src="https://www.googletagmanager.com/gtag/js?id=
4G-WQ62DL1S2M
vendor: 17 bytes, lines 4-5
4"></script>
5    
5<script>
6      
vendor: 148 bytes, lines 6-9
6window.dataLayer = window.dataLayer || [];
7      function gtag(){dataLayer.push(arguments);}
8      gtag('js', new Date());
9      gtag('config', '
9G-WQ62DL1S2M
vendor: 9 bytes, lines 9-10
9');
10    
10</script>
10
11    
11<script src="timer.js"></script>
11
12
13    <meta charset="UTF-8">
14    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
15    <title>TibetanKeys | Timer Room</title>
16    <link rel="icon" type="image/png" href="og.png">
17    <style>
18        @font-face { font-family: 'OuChan2'; src: url('Monlam Uni OuChan2.ttf'); }
19        @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@700;900&display=swap');
20        * { box-sizing: border-box; }
21        *:focus, *:focus-visible { outline: none !important; }
22        :root {
23            --bg:#000000; --accent:#d4af37; --text:#ffffff;
24            --error:#ff4d4d; --success:#00e676; --blue-glow:#00ffff;
25            --cursor-bg:rgba(0,255,255,.15); --cursor-border:rgba(0,255,255,.5);
26            --frost-cyan:#00ffff;
27        }
28        body,html { margin:0;padding:0;background:var(--bg);color:var(--text);font-family:'Segoe UI',sans-serif;height:100vh;width:100vw;max-height:100vh;max-width:100vw;overflow:hidden !important;display:flex;flex-direction:column;user-select:none;overscroll-behavior:none; }
29
30        body.layout-medium { --dynamic-font-size:55px; --line-gap:20px; }
31        body.layout-small  { --dynamic-font-size:50px; --line-gap:20px; }
32        body.layout-clean  { --dynamic-font-size:60px; --line-gap:20px; }
33
34        /* --- REMOVE SPINNERS FOR NUMBER INPUTS --- */
35        input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
36        input[type=number] { -moz-appearance: textfield; }
37
38        /* GLITCH */
39        @keyframes chromaticGlitch {
40            0%  {filter:none;transform:none;}
41            12% {filter:drop-shadow(-5px 0 0 rgba(255,0,0,.85)) drop-shadow(5px 0 0 rgba(0,255,255,.85));transform:translate(-2px,1px) skewX(-1.5deg);}
42            24% {filter:drop-shadow(4px 0 0 rgba(255,0,0,.6)) drop-shadow(-4px 0 0 rgba(0,255,255,.6));transform:translate(3px,-1px);}
43            38% {filter:drop-shadow(-3px 0 0 rgba(255,0,0,.5)) drop-shadow(3px 0 0 rgba(0,255,255,.5)) brightness(1.15);transform:translate(-1px,2px) skewX(1deg);}
44            55% {filter:drop-shadow(2px 0 0 rgba(255,0,0,.3)) drop-shadow(-2px 0 0 rgba(0,255,255,.3));transform:translate(1px,0);}
45            75% {filter:drop-shadow(-1px 0 0 rgba(255,0,0,.15)) drop-shadow(1px 0 0 rgba(0,255,255,.15));transform:translate(-1px,0);}
46            100%{filter:none;transform:none;}
47        }
48        body.chromatic-glitch { animation:chromaticGlitch .55s steps(1) forwards; }
49        .glitch-band { position:fixed;left:0;width:100%;pointer-events:none;z-index:9001;animation:bandFlash .25s ease-out forwards; }
50        @keyframes bandFlash { 0%{opacity:.85;} 60%{opacity:.4;} 100%{opacity:0;} }
51        #glitch-container { position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9001;overflow:hidden; }
52
53        /* PRELOADER - Fixed display:none to prevent blocking setup */
54        #preloader { position:fixed;top:0;left:0;width:100%;height:100%;background:#000;z-index:20000;display:none;flex-direction:column;justify-content:center;align-items:center;transition:opacity .5s ease-out; }
55        .loader-dots { display:flex;gap:12px; }
56        .dot { width:15px;height:15px;background-color:var(--accent);border-radius:50%;box-shadow:0 0 10px rgba(212,175,55,.5);animation:bounce 1.4s infinite ease-in-out both; }
57        .dot:nth-child(1){animation-delay:-.32s;}.dot:nth-child(2){animation-delay:-.16s;}.dot:nth-child(3){animation-delay:0s;}.dot:nth-child(4){animation-delay:.16s;}.dot:nth-child(5){animation-delay:.32s;}
58        @keyframes bounce { 0%,80%,100%{transform:scale(0);opacity:.3;} 40%{transform:scale(1.2);opacity:1;box-shadow:0 0 20px var(--accent);} }
59
60        /* SETUP OVERLAY */
61        #setup-overlay { position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);backdrop-filter:blur(10px);z-index:15000;display:flex;justify-content:center;align-items:center; }
62        #setup-panel { background:#0a0f18;border:1px solid rgba(212,175,55,.3);padding:50px 70px;border-radius:24px;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,.9), 0 0 0 1px rgba(212,175,55,.1); }
63        .setup-title { color:var(--accent);margin:0 0 25px 0;font-size:22px;letter-spacing:6px;text-transform:uppercase;font-weight:900;text-shadow:0 0 20px rgba(212,175,55,.4); }
64        .time-presets { display:flex;gap:15px;margin-bottom:30px;justify-content:center; }
65        .time-btn { background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.1);color:#fff;padding:12px 24px;border-radius:10px;cursor:pointer;font-size:1rem;font-weight:bold;transition:0.3s; }
66        .time-btn:hover, .time-btn.selected { background:var(--accent);color:#000;border-color:var(--accent);box-shadow:0 0 20px rgba(212, 175, 55, 0.5);transform:translateY(-2px); }
67        .custom-time-box { display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:35px;font-size:1.4rem;color:#888;font-weight:900; }
68        .custom-input { width:80px;padding:12px;text-align:center;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.1);color:var(--accent);font-size:1.4rem;border-radius:10px;font-family:'Roboto Mono',monospace;transition:0.3s; }
69        .custom-input:focus { border-color:var(--accent);box-shadow:0 0 15px rgba(212,175,55,0.2); }
70        .start-btn { background:var(--accent);color:#000;border:none;padding:16px 60px;font-size:1.2rem;letter-spacing:2px;text-transform:uppercase;font-weight:900;border-radius:50px;cursor:pointer;box-shadow:0 0 20px rgba(212, 175, 55, 0.3);transition:all 0.3s; }
71        .start-btn:hover { transform:scale(1.05);box-shadow:0 0 40px rgba(212, 175, 55, 0.6); }
72
73        /* HUD */
74        .hud { width:100%;height:64px;padding:0 16px;background:rgba(8,8,10,.98);border-bottom:1px solid rgba(212,175,55,.12);flex-shrink:0;z-index:200;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:0 12px; }
75        .hud-left { display:flex;align-items:center;gap:5px;justify-self:start; }
76        .level-info h1 { margin:0;font-size:11px;letter-spacing:3px;color:var(--accent);text-transform:uppercase;font-weight:700;white-space:nowrap;margin-right:15px; }
77        .icon-btn { background:none;border:none;color:#666;cursor:pointer;width:38px;height:38px;display:flex;justify-content:center;align-items:center;border-radius:8px;transition:all .2s ease;position:relative; }
78        .icon-btn:hover { color:#fff;background:rgba(255,255,255,.06); }
79        .icon-btn.active { color:#fff !important; }
80        .icon-btn svg { width:18px;height:18px;fill:currentColor; }
81
82        /* Kb popover */
83        .kb-menu-wrap { position:relative; }
84        #kb-popover { display:none;position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%);background:rgba(10,10,14,.98);border:1px solid rgba(212,175,55,.2);border-radius:14px;padding:10px;width:170px;z-index:500;box-shadow:0 20px 60px rgba(0,0,0,.8),0 0 0 1px rgba(212,175,55,.05);backdrop-filter:blur(20px); }
85        #kb-popover::before { content:'';position:absolute;top:-6px;left:50%;width:10px;height:10px;background:rgba(10,10,14,.98);border-left:1px solid rgba(212,175,55,.2);border-top:1px solid rgba(212,175,55,.2);transform:translateX(-50%) rotate(45deg); }
86        #kb-popover.open { display:block;animation:popoverIn .18s ease-out; }
87        @keyframes popoverIn { from{opacity:0;transform:translateX(-50%) translateY(-6px);} to{opacity:1;transform:translateX(-50%) translateY(0);} }
88        .kb-menu-title { font-size:9px;letter-spacing:3px;color:#444;text-transform:uppercase;margin-bottom:8px;font-weight:700;padding:0 4px; }
89        .kb-menu-row { display:flex;align-items:center;justify-content:space-between;padding:9px 12px;border-radius:8px;
89cursor:pointer;transition:all .15s;margin-bottom:3px;border:1px solid transparent; }
90        .kb-menu-row:hover { background:rgba(255,255,255,.06); }
91        .kb-menu-row.kb-active { background:rgba(212,175,55,.15);border-color:rgba(212,175,55,.3); }
92        .kb-menu-row.kb-active .kb-menu-row-label { color:var(--accent);font-weight:700; }
93        .kb-menu-row.kb-active .kb-menu-row-check { color:var(--accent); }
94        .kb-menu-row-label { font-size:11px;color:#888;letter-spacing:1px;transition:color .15s; }
95        .kb-menu-row-check { font-size:12px;color:transparent;transition:color .15s; }
96
97        /* live WPM/ACC & TIMER */
98        .hud-center { display:flex;align-items:center;justify-content:center; }
99        .hud-live { display:flex;align-items:center;gap:35px; }
100        .hud-live-stat { display:flex;align-items:baseline;gap:6px; }
101        .hud-live-val { font-size:24px;font-weight:800;color:var(--blue-glow);text-shadow:0 0 14px rgba(0,255,255,.4); font-variant-numeric: tabular-nums; }
102        .hud-live-label { font-size:10px;letter-spacing:2px;color:#666;text-transform:uppercase;font-weight:700; }
103        .timer-accent { color:var(--accent); text-shadow:0 0 14px rgba(212,175,55,.4); font-size: 28px; }
104
105        .phase-indicator { justify-self:end;font-size:11px;letter-spacing:3px;color:rgba(255,255,255,0.4);text-transform:uppercase;font-weight:bold;white-space:nowrap; }
106        .size-controls { display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.04);padding:3px;border-radius:8px;border:1px solid rgba(255,255,255,.1);height:36px;margin-left:6px; }
107        .size-btn { background:none;border:none;color:#666;font-size:12px;font-weight:bold;cursor:pointer;width:30px;height:100%;display:flex;justify-content:center;align-items:center;transition:.2s;border-radius:5px; }
108        .size-btn:hover { background:rgba(255,255,255,.1);color:#fff; }
109
110        #typing-progress-bar { width:0%;height:3px;background:var(--accent);flex-shrink:0;transition:width 1s linear;z-index:199; }
111
112        /* ── STAGE ── */
113        .stage { flex:1;display:flex;flex-direction:column;width:100%;overflow:hidden;position:relative;justify-content:center;align-items:center;opacity:0;transition:opacity .8s ease-in;min-height:0; }
114        .stanza-scroll { flex:1; width:100%; display:flex; flex-direction:column; align-items:center; justify-content:flex-start; overflow-y:auto; overflow-x:hidden; -ms-overflow-style:none; scrollbar-width:none; min-height:0; }
115        .stanza-scroll::-webkit-scrollbar { display:none; }
116        .stanza-scroll::before, .stanza-scroll::after { content:''; margin:auto; }
117        
118        #stanza-container { display:flex; justify-content:center; width:100%; padding:20px 40px 60px 40px; flex-shrink:0; }
119        
120        .paragraph-wrapper { display:flex; flex-wrap:wrap; justify-content:flex-start; align-content:flex-start; max-width:1300px; padding:1em 30px; font-family:'OuChan2',serif; font-size:var(--dynamic-font-size); line-height:1.8; }
121        .tib-word { display:flex; flex-wrap:nowrap; }
122
123        .char { opacity:.4;transition:all .15s ease;position:relative;display:flex;justify-content:center;align-items:center;text-align:center;padding:0 1px;margin:0;border-radius:4px;border:1px solid transparent;min-height:1.2em; }
124        .char.tsek { padding:0;margin:0; }
125        .sentence-break { margin-right:1.2em; }
126        .char.space-char { width: 0.8em; }
127
128        .char .ghost-text { position:absolute;top:0;left:0;right:0;bottom:0;display:flex;justify-content:center;align-items:center;color:var(--blue-glow);font-family:'OuChan2',serif;font-size:inherit;white-space:nowrap;pointer-events:none;z-index:5; }
129        .char .ghost-text.error { color:var(--error); }
130        .char.current { opacity:1;color:#fff;transform:scale(1.1);z-index:10;background:transparent;border-color:transparent;box-shadow:none;text-shadow:0 0 15px rgba(255,255,255,.8); }
131        .char.correct { opacity:.7;color:#fff;text-shadow:none; }
132        .char.wrong { opacity:1;color:var(--error);animation:shake .3s; }
133        @keyframes shake { 0%,100%{transform:translateX(0);} 25%{transform:translateX(-5px);} 75%{transform:translateX(5px);} }
134
135        /* ── KEYBOARD ── */
136        #keyboard-wrapper { width:100%;max-width:900px;aspect-ratio:950/400;margin:0 auto;flex-shrink:0;position:relative;z-index:20;transition:all .5s; }
137        body.layout-medium #keyboard-wrapper { height:32vh;opacity:1;margin-bottom:16px; }
138        body.layout-small  #keyboard-wrapper { height:24vh;opacity:1;margin-bottom:16px; }
139        body.layout-clean  #keyboard-wrapper { height:0;opacity:0;pointer-events:none;margin-bottom:0; }
140        body.hands-hidden #layer-hands-container { display:none; }
141        #layer-keyboard { position:absolute;top:0;left:0;width:100%;height:100%;display:block;filter:drop-shadow(0 -20px 40px rgba(0,0,0,.8));z-index:1; }
142        #layer-hands-container { position:absolute;top:0;left:0;width:100%;height:100%;z-index:2;pointer-events:none; }
143        #layer-hands-container svg { width:100%;height:100%;display:block; }
144        #active-finger { fill:url(#finger-gradient) !important;stroke:#00ffff !important;stroke-width:.5px !important;filter:drop-shadow(0 0 10px #00ffff) drop-shadow(0 0 20px #00ffff) !important;opacity:1 !important; }
145        #layer-hands-container svg g:not(#active-finger),#layer-hands-container svg path:not(#active-finger) { opacity:.5; }
146        .st0 { fill:#d4af37;font-family:Arial,sans-serif;font-size:14px;font-weight:700;pointer-events:none; }
147        .st1 { fill:none;opacity:.5;
147stroke:#d4af37;stroke-width:1.5px;transition:all .1s; }
148        .st2 { fill:transparent; }
149        .key-guide { stroke:var(--blue-glow) !important;stroke-width:3px !important;fill:rgba(0,255,255,.15) !important;opacity:1 !important;filter:drop-shadow(0 0 15px var(--blue-glow)); }
150        .key-hit { fill:var(--success) !important;stroke:var(--success) !important;filter:drop-shadow(0 0 15px var(--success));transition:0s; }
151
152        /* ── RESULT MODAL ── */
153        #result-modal { display:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:10000;justify-content:center;align-items:center;overflow:hidden;background:#000; }
154        #result-aurora { position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;overflow:hidden; }
155        .aurora-blob { position:absolute;border-radius:50%;filter:blur(80px);animation:auroraFloat 8s ease-in-out infinite alternate; }
156        .ab1{width:60vw;height:60vw;background:radial-gradient(circle,rgba(0,230,118,.18),transparent 70%);top:-15%;left:-10%;animation-delay:0s;}
157        .ab2{width:50vw;height:50vw;background:radial-gradient(circle,rgba(0,200,255,.15),transparent 70%);top:20%;right:-5%;animation-delay:-2s;}
158        .ab3{width:45vw;height:45vw;background:radial-gradient(circle,rgba(212,175,55,.12),transparent 70%);bottom:-10%;left:20%;animation-delay:-4s;}
159        .ab4{width:40vw;height:40vw;background:radial-gradient(circle,rgba(196,113,237,.10),transparent 70%);bottom:5%;right:10%;animation-delay:-6s;}
160        @keyframes auroraFloat{0%{transform:scale(1) translate(0,0);}100%{transform:scale(1.2) translate(3%,5%);}}
161        #result-canvas { position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2; }
162        
163        .victory-container { display:flex;flex-direction:column;align-items:center;position:relative;z-index:3;padding:0 20px;gap:20px; width:min(480px, 90vw); text-align:center;}
164        .res-title { font-size:20px;letter-spacing:6px;text-transform:uppercase;font-weight:900;color:var(--accent);text-shadow:0 0 30px rgba(212,175,55,.8),0 0 60px rgba(212,175,55,.3);margin-bottom:10px;animation:titleGlow 2s ease-in-out infinite alternate; }
165        @keyframes titleGlow{from{letter-spacing:6px;}to{letter-spacing:8px;text-shadow:0 0 40px rgba(212,175,55,1),0 0 80px rgba(212,175,55,.5);}}
166        .res-grid { display:flex;gap:15px;width:100%; justify-content:center;}
167        .res-stat { flex:1;padding:25px 10px 20px;border-radius:16px;border:1px solid transparent;text-align:center;position:relative;overflow:hidden;opacity:0;transform:translateY(30px) scale(.9);transition:opacity .5s ease,transform .5s cubic-bezier(.175,.885,.32,1.275),box-shadow .3s; }
168        .res-stat.visible { opacity:1;transform:translateY(0) scale(1); }
169        .res-stat::before { content:'';position:absolute;top:0;left:0;right:0;bottom:0;border-radius:16px;opacity:.06; }
170        .res-stat.stat-wpm  { background:linear-gradient(160deg,rgba(0,40,50,.9),rgba(0,20,28,.95));border-color:rgba(0,255,255,.25);box-shadow:0 0 30px rgba(0,255,255,.08),inset 0 1px 0 rgba(0,255,255,.1); }
171        .res-stat.stat-wpm::before { background:var(--blue-glow); }
172        .res-stat.stat-acc  { background:linear-gradient(160deg,rgba(0,40,20,.9),rgba(0,20,10,.95));border-color:rgba(0,230,118,.25);box-shadow:0 0 30px rgba(0,230,118,.08),inset 0 1px 0 rgba(0,230,118,.1); }
173        .res-stat.stat-acc::before { background:var(--success); }
174        
175        .res-val { font-size:clamp(32px,5vw,56px);font-weight:900;font-variant-numeric:tabular-nums;line-height:1;margin-bottom:8px;display:block;white-space:nowrap; }
176        .stat-wpm  .res-val { color:var(--blue-glow);text-shadow:0 0 20px rgba(0,255,255,.7),0 0 40px rgba(0,255,255,.3); }
177        .stat-acc  .res-val { color:var(--success);text-shadow:0 0 20px rgba(0,230,118,.7),0 0 40px rgba(0,230,118,.3); }
178        .res-icon  { font-size:22px;margin-bottom:8px;display:block;opacity:.7; }
179        .res-label { font-size:9px;color:rgba(255,255,255,.3);letter-spacing:3px;text-transform:uppercase;font-weight:700;margin-top:4px; }
180        @keyframes numBoom{0%{transform:scale(0) rotate(-5deg);opacity:0;}60%{transform:scale(1.2) rotate(1deg);}80%{transform:scale(.95);}100%{transform:scale(1);opacity:1;}}
181        .res-val.boom { animation:numBoom .6s cubic-bezier(.175,.885,.32,1.275) both; }
182        
183        .timer-recap { font-size: 14px; color: #aaa; letter-spacing: 2px; text-transform: uppercase; margin-top: 10px; margin-bottom: 20px; }
184        .timer-recap span { color: var(--accent); font-weight: bold; }
185
186        .prompt-container { display:flex; justify-content:center; gap:30px; width:100%; animation:pulse 2s infinite; margin-top: 15px; }
187        .prompt-group { display:flex; flex-direction:column; align-items:center; gap:8px; }
188        .prompt-key { color:#fff;font-weight:800;font-size:11px;letter-spacing:2px;text-transform:uppercase;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);padding:8px 16px;border-radius:8px;min-width:80px;text-align:center;cursor:pointer;transition:all .2s;pointer-events:auto;backdrop-filter:blur(10px); white-space:nowrap; }
189        .prompt-key:hover { border-color:var(--accent);color:var(--accent);background:rgba(212,175,55,.1);transform:translateY(-2px); }
190        .prompt-key:active { transform:scale(.95); }
191        .shining-btn { border-color:var(--accent);color:var(--accent);background:rgba(212,175,55,.15);animation:pulseGlow 2s infinite; }
192        @keyframes pulseGlow{0%{box-shadow:0 0 10px rgba(212,175,55,.4);}50%{box-shadow:0 0 25px rgba(212,175,55,.8);}100%{box-shadow:0 0 10px rgba(212,175,55,.4);}}
193        .prompt-action { color:rgba(255,255,255,.2);font-size:8px;letter-spacing:2px;text-transform:uppercase;text-align:center; }
194        @keyframes pulse { 0%{opacity:.55;} 50%{opacity:1;} 100%{opacity:.55;} }
195
196        .back-btn { position:fixed;bottom:20px;left:30px;color:#666;text-decoration:none;font-size:11px;letter-spacing:2px;text-transform:uppercase;transition:.3s;z-index:50;display:flex;align-items:center;gap:8px;pointer-events:auto; }
197        .back-btn:hover { color:var(--accent); }
198    </style>
199</head>
200<body class="layout-small">
201
202<div id="setup-overlay">
203    <div id="setup-panel">
204        <h2 class="setup-title">Set Timer</h2>
205        <div class="time-presets">
206            <button class="time-btn" onclick="selectTime(30, this)">30s</button>
207            <button class="time-btn" onclick="selectTime(60, this)">1m</button>
208            <button class="time-btn selected" onclick="selectTime(120, this)">2m</button>
209            <button class="time-btn" onclick="selectTime(300, this)">5m</button>
210            <button class="time-btn" onclick="selectTime(600, this)">10m</button>
211        </div>
212        <div class="custom-time-box">
213            <input type="number" id="cust-min" class="custom-input" placeholder="00" min="0" max="60" onin
213put="clearPresets()"> 
214            <span>m :</span>
215            <input type="number" id="cust-sec" class="custom-input" placeholder="00" min="0" max="59" oninput="clearPresets()">
216            <span>s</span>
217        </div>
218        <button class="start-btn" onclick="initGame()">ENTER ARENA</button>
219    </div>
220</div>
221
222<div id="preloader"><div class="loader-dots"><div class="dot"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div></div></div>
223<div id="glitch-container"></div>
224
225<svg style="width:0;height:0;position:absolute;" aria-hidden="true" focusable="false">
226    <defs><linearGradient id="finger-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
227        <stop offset="0%"  style="stop-color:#00ffff;stop-opacity:0.6"/>
228        <stop offset="50%" style="stop-color:#00ccff;stop-opacity:0.4"/>
229        <stop offset="90%" style="stop-color:#00ffff;stop-opacity:0"/>
230    </linearGradient></defs>
231</svg>
232
233<div class="hud">
234    <div class="hud-left">
235        <div class="level-info"><h1>Speed Test</h1></div>
236        <button class="icon-btn" onclick="window.location.reload()" title="Restart"><svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg></button>
237        <button class="icon-btn active" id="btn-hand" onclick="toggleHands()"><svg viewBox="0 0 24 24"><path d="M23 5.5V20c0 2.2-1.8 4-4 4h-7.3c-1.08 0-2.1-.43-2.85-1.19L1 14.83s1.26-1.23 1.3-1.25c.22-.19.49-.29.79-.29.22 0 .42.06.6.16.04.01 4.31 2.46 4.31 2.46V4c0-.83.67-1.5 1.5-1.5S11 3.17 11 4v7h1V1.5c0-.83.67-1.5 1.5-1.5S15 .67 15 1.5V11h1V2.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5V11h1V5.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5V11h1V5.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5z"/></svg></button>
238        
239        <div class="kb-menu-wrap">
240            <button class="icon-btn" id="btn-keyboard" onclick="toggleKbMenu()" title="Keyboard Size"><svg viewBox="0 0 24 24"><path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z"/></svg></button>
241            <div id="kb-popover">
242                <div class="kb-menu-title">Keyboard Size</div>
243                <div class="kb-menu-row" id="kb-row-medium" onclick="setLayout('layout-medium',true);closeKbMenu()"><span class="kb-menu-row-label">Medium</span><span class="kb-menu-row-check">✓</span></div>
244                <div class="kb-menu-row" id="kb-row-small"  onclick="setLayout('layout-small',true);closeKbMenu()"><span class="kb-menu-row-label">Small</span><span class="kb-menu-row-check">✓</span></div>
245                <div class="kb-menu-row" id="kb-row-clean"  onclick="setLayout('layout-clean',true);closeKbMenu()"><span class="kb-menu-row-label">Off</span><span class="kb-menu-row-check">✓</span></div>
246            </div>
247        </div>
248        <button id="sound-btn" class="icon-btn" onclick="sfx.toggle()"><svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg></button>
249        <div class="size-controls">
250            <button class="size-btn" onclick="adjustFontSize(-5)">A-</button>
251            <button class="size-btn" onclick="adjustFontSize(5)">A+</button>
252        </div>
253    </div>
254    
255    <div class="hud-center">
256        <div class="hud-live">
257            <div class="hud-live-stat"><span class="hud-live-val timer-accent" id="live-timer">00:00</span></div>
258            <div class="hud-live-stat"><span class="hud-live-val" id="live-wpm">0</span><span class="hud-live-label">WPM</span></div>
259            <div class="hud-live-stat"><span class="hud-live-val" id="live-acc">100%</span><span class="hud-live-label">ACC</span></div>
260        </div>
261    </div>
262    <div class="phase-indicator">Begin typing to start</div>
263</div>
264
265<div id="typing-progress-bar"></div>
266
267<div class="stage">
268    <div class="stanza-scroll" id="stanza-scroll">
269        <div id="stanza-container"></div>
270    </div>
271
272    <div id="keyboard-wrapper">
273        <svg id="layer-keyboard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 950 400" preserveAspectRatio="xMidYMid meet">
274            <rect class="st2" width="950" height="400"/>
275            <g id="row-1"><g><rect class="st1" x="20" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(43.79 52)">~</text></g><g id="key-1"><rect class="st1" x="80" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(103.94 52)">1</text></g><g id="key-2"><rect class="st1" x="140" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(163.94 52)">2</text></g><g id="key-3"><rect class="st1" x="200" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(223.94 52)">3</text></g><g id="key-4"><rect class="st1" x="260" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(283.94 52)">4</text></g><g id="key-5"><rect class="st1" x="320" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(343.94 52)">5</text></g><g id="key-6"><rect class="st1" x="380" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(403.94 52)">6</text></g><g id="key-7"><rect class="st1" x="440" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(463.94 52)">7</text></g><g id="key-8"><rect class="st1" x="500" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(523.94 52)">8</text></g><g id="key-9"><rect class="st1" x="560" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(583.94 52)">9</text></g><g id="key-0"><rect class="st1" x="620" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(643.94 52)">0</text></g><g><rect class="st1" x="680" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(705.17 52)">-</text></g><g><rect class="st1" x="740" y="20" width="55" height="55" rx="6"/><text class="st0" transform="translate(763.79 52)">=</text></g><g><rect class="st1" x="800" y="20" width="115" height="55" rx="6"/><text class="st0" transform="translate(822.57 52)">BACKSPACE</text></g></g>
276            <g id="row-2"><g><rect class="st1" x="20" y="85" width="85" height="55" rx="6"/><text class="st0" transform="translate(51.1 117)">TAB</text></g><g id="key-Q"><rect class="st1" x="110" y="85" width="55" height="55" rx="6"/>
276<text class="st0" transform="translate(132.72 117)">Q</text></g><g id="key-W"><rect class="st1" x="170" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(191.81 117)">W</text></g><g id="key-E"><rect class="st1" x="230" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(253.33 117)">E</text></g><g id="key-R"><rect class="st1" x="290" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(313.03 117)">R</text></g><g id="key-T"><rect class="st1" x="350" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(373.64 117)">T</text></g><g id="key-Y"><rect class="st1" x="410" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(433.33 117)">Y</text></g><g id="key-U"><rect class="st1" x="470" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(493.03 117)">U</text></g><g id="key-I"><rect class="st1" x="530" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(555.47 117)">I</text></g><g id="key-O"><rect class="st1" x="590" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(612.72 117)">O</text></g><g id="key-P"><rect class="st1" x="650" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(673.33 117)">P</text></g><g id="key-LBRACK"><rect class="st1" x="710" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(735.17 117)">[</text></g><g id="key-RBRACK"><rect class="st1" x="770" y="85" width="55" height="55" rx="6"/><text class="st0" transform="translate(795.17 117)">]</text></g><g><rect class="st1" x="830" y="85" width="85" height="55" rx="6"/><text class="st0" transform="translate(870.47 117)">\</text></g></g>
277            <g id="row-3"><g><rect class="st1" x="20" y="150" width="105" height="55" rx="6"/><text class="st0" transform="translate(56.72 182)">CAPS</text></g><g id="key-A"><rect class="st1" x="130" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(153.03 182)">A</text></g><g id="key-S"><rect class="st1" x="190" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(213.33 182)">S</text></g><g id="key-D"><rect class="st1" x="250" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(273.03 182)">D</text></g><g id="key-F"><rect class="st1" x="310" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(333.64 182)">F</text><rect class="bump" x="327" y="190" width="20" height="2" rx="1"/></g><g id="key-G"><rect class="st1" x="370" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(392.72 182)">G</text></g><g id="key-H"><rect class="st1" x="430" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(453.03 182)">H</text></g><g id="key-J"><rect class="st1" x="490" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(513.94 182)">J</text><rect class="bump" x="507" y="190" width="20" height="2" rx="1"/></g><g id="key-K"><rect class="st1" x="550" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(573.03 182)">K</text></g><g id="key-L"><rect class="st1" x="610" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(633.64 182)">L</text></g><g id="key-SEMI"><rect class="st1" x="670" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(695.17 182)">;</text></g><g id="key-QUOTE"><rect class="st1" x="730" y="150" width="55" height="55" rx="6"/><text class="st0" transform="translate(755.69 182)">'</text></g><g><rect class="st1" x="790" y="150" width="125" height="55" rx="6"/><text class="st0" transform="translate(833.36 182)">ENTER</text></g></g>
278            <g id="row-4"><g id="key-LSHIFT"><rect class="st1" x="20" y="215" width="135" height="55" rx="6"/><text class="st0" transform="translate(71.11 247)">SHIFT</text></g><g id="key-Z"><rect class="st1" x="160" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(183.64 247)">Z</text></g><g id="key-X"><rect class="st1" x="220" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(243.33 247)">X</text></g><g id="key-C"><rect class="st1" x="280" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(303.03 247)">C</text></g><g id="key-V"><rect class="st1" x="340" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(363.33 247)">V</text></g><g id="key-B"><rect class="st1" x="400" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(423.03 247)">B</text></g><g id="key-N"><rect class="st1" x="460" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(483.03 247)">N</text></g><g id="key-M"><rect class="st1" x="520" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(542.42 247)">M</text></g><g id="key-COMMA"><rect class="st1" x="580" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(605.47 247)">,</text></g><g id="key-DOT"><rect class="st1" x="640" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(665.47 247)">.</text></g><g id="key-SLASH"><rect class="st1" x="700" y="215" width="55" height="55" rx="6"/><text class="st0" transform="translate(725.47 247)">/</text></g><g id="key-RSHIFT"><rect class="st1" x="760" y="215" width="155" height="55" rx="6"/><text class="st0" transform="translate(821.11 247)">SHIFT</text></g></g>
279            <g id="row-5"><g id="key-SPACE"><rect class="st1" x="270" y="280" width="400" height="55" rx="6"/><text class="st0" transform="translate(439.54 312)">SPACEBAR</text></g></g>
280        </svg>
281        <div id="layer-hands-container"></div>
282    </div>
283</div>
284
285<div id="result-modal">
286    <div id="result-aurora"><div class="aurora-blob ab1"></div><div class="aurora-blob ab2"></div><div class="aurora-blob ab3"></div><div class="aurora-blob ab4"></div></div>
287    <canvas id="result-canvas"></canvas>
288    <div id="victory-container" class="victory-container">
289        <div class="res-title">TIME'S UP!</div>
290        <div class="timer-recap">Session Duration: <span id="res-duration">02:00</span></div>
291        <div class="res-grid">
292            <div class="res-stat stat-wpm" ><span class="res-icon">⚡</span><div class="res-val" id="res-wpm">0</div><div class="res-label">WPM</div></div>
293            <div class="res-stat stat-acc" ><span class="res-icon">🎯</span><div class="res-val" id="res-acc">0%</div><div class="res-label">ACCURACY</div></div>
294        </div>
295        <div class="prompt-container" id="prompt-cont">
296            <div class="prompt-group">
297                <div class="prompt-key" onclick="window.location.href='index.html'">SPACE</div>
298                <div class="prompt-action">Main Menu</div>
299            </div>
300            <div class="prompt-group">
301                <div class="prompt-key shining-btn" onclick="window.location.reload()">SHIFT</div>
302                <div class="prompt-action" style="color:var(--accent);font-weight:bold;text-shadow:0 0 5px var(--accent);">Try Again</div>
303            </div>
304        </div>
305    </div>
306</div>
307
308<a href="index.html" class="back-btn">← Main Menu</a>
309
310<script>
311// ═══════════════════════════════════════════════
312// SETUP AND TIMER STATE
313// ═══════════════════════════════════════════════
314let selectedSeconds = 120;
315let timeRemaining = 0;
316let timerInterval = null;
317let gameStarted = false; // Becomes true on first keystroke
318let targetStacks = [], currentStackIndex = 0, currentTyped = '', isStackingMode = false;
319let startTime = null, errorsCount = 0, charCount = 0;
320let resultState = 'hidden', currentLayout = 'layout-small';
321let errorTimeout = null, liveStatsInterval = null;
322
323const container   = document.getElementById('stanza-container');
324const handContainer = document.getElementById('layer-hands-container');
325const preloader   = document.getElementById('preloader');
326
327// Ensure Fallback Text if timer.js fails to load
328if (!window.TK_TIMER_TEXTS || window.TK_TIMER_TEXTS.length === 0) {
329    window.TK_TIMER_TEXTS = ["འཛམ་གླིང་གི་ཡང་ཐོག་ཏུ་ཆགས་པའི་ང་ཚོའི་ཕ་ཡུལ་བོད་ལྗོངས་ནི་རི་མཐོ་ས་གཙང་དང་གངས་རི་ར་བས་བསྐོར་བའི་ཡུལ་གྲུ་ཞིག་ཡིན།"];
330}
331
332function selectTime(sec, btnElement) {
333    selectedSeconds = sec;
334    document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('selected'));
335    if (btnElement) btnElement.classList.add('selected');
336    document.getElementById('cust-min').value = "";
337    document.getElementById('cust-sec').value = "";
338}
339
340function clearPresets() {
341    document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('selected'));
342    selectedSeconds = 0; 
343}
344
345function initGame() {
346    const cMin = parseInt(document.getElementById('cust-min').value) || 0;
347    const cSec = parseInt(document.getElementById('cust-sec').value) || 0;
348    if (cMin > 0 || cSec > 0) selectedSeconds = (cMin * 60) + cSec;
349    if (selectedSeconds <= 0) { alert("Please select a valid time."); return; }
350
351    document.getElementById('setup-overlay').style.display = 'none';
352    
353    // Show preloader while processing text and images
354    preloader.style.display = 'flex';
355    preloader.style.opacity = '1';
356
357    timeRemaining = selectedSeconds;
358    updateTimerDisplay();
359    
360    // Clear DOM and Arrays
361    container.innerHTML = '';
362    targetStacks = [];
363    currentStackIndex = 0;
364    
365    appendMoreText();
366    preloadAssets();
367}
368
369// ═══════════════════════════════════════════════
370// PREFS
371// ═══════════════════════════════════════════════
372const PREF_HANDS = 'tk_pref_hands', PREF_SOUND = 'tk_pref_sound', PREF_TEXT_SIZE = 'tk_pref_text_size';
373let currentFontSize = 50;
374
375function toggleHands(save = true) { document.body.classList.toggle('hands-hidden'); document.getElementById('btn-hand').classList.toggle('active'); if (save) localStorage.setItem(PREF_HANDS, document.body.classList.contains('hands-hidden')); }
376function adjustFontSize(d) { currentFontSize = Math.max(15, Math.min(100, currentFontSize + d)); localStorage.setItem(PREF_TEXT_SIZE, currentFontSize); document.body.style.setProperty('--dynamic-font-size', currentFontSize + 'px'); }
377
378function setLayout(mode, save = true) {
379    document.body.classList.remove('layout-medium','layout-small','layout-clean');
380    document.body.classList.add(mode); currentLayout = mode;
381    if (save) localStorage.setItem('tk_pref_layout_paragraph', mode);
382    updateKbMenuState();
383}
384function toggleKbMenu() { const p = document.getElementById('kb-popover'); p.classList.toggle('open'); if (p.classList.contains('open')) updateKbMenuState(); }
385function closeKbMenu()  { document.getElementById('kb-popover').classList.remove('open'); }
386function updateKbMenuState() {
387    ['medium','small','clean'].forEach(k => {
388        const r = document.getElementById('kb-row-'+k);
389        if (r) r.classList.toggle('kb-active', currentLayout === 'layout-'+k);
390    });
391}
392document.addEventListener('click', e => { if (!document.querySelector('.kb-menu-wrap')?.contains(e.target)) closeKbMenu(); });
393
394// ═══════════════════════════════════════════════
395// GLITCH & SOUND
396// ═══════════════════════════════════════════════
397const glitchSystem = {
398    container: null,
399    init() { this.container = document.getElementById('glitch-container'); },
400    trigger() {
401        this.container.innerHTML = '';
402        for (let i = 0; i < 3 + Math.floor(Math.random() * 6); i++) {
403            const b = document.createElement('div'); b.className = 'glitch-band';
404            const rgb = Math.random() < .55 ? '255,30,30' : '0,255,255';
405            b.style.cssText = `top:${Math.random()*96}%;height:${1+Math.random()*18}px;background:rgba(${rgb},${.10+Math.random()*.22});transform:translateX(${(Math.random()-.5)*50}px);animation-delay:${Math.random()*.1}s;animation-duration:${.12+Math.random()*.2}s;`;
406            this.container.appendChild(b);
407        }
408        document.body.classList.add('chromatic-glitch');
409        setTimeout(() => { this.container.innerHTML = ''; document.body.classList.remove('chromatic-glitch'); }, 450);
410    }
411};
412function triggerErrorGlitch() { glitchSystem.trigger(); }
413
414const sfx = {
415    ctx: null, muted: false,
416    init()  { if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)(); if (this.ctx.state === 'suspended') this.ctx.resume(); },
417    toggle() {
418        this.muted = !this.muted; localStorage.setItem(PREF_SOUND, this.muted);
419        const btn = document.getElementById('sound-btn');
420        if (this.muted) { btn.classList.add('muted'); btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73 4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>'; }
421        else { this.init(); btn.classList.remove('muted'); btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>'; }
422        btn.blur();
423    },
424    playClick() { if (this.muted || !this.ctx) return; const o = this.ctx.createOscillator(), g = this.ctx.createGain(); o.type='sine'; o.frequency.setValueAtTime(800,this.ctx.currentTime); g.gain.setValueAtTime(.3,this.ctx.currentTime); g.gain.exponentialRampToValueAtTime(.01,this.ctx.currentTime+.08); o.connect(g); g.connect(this.ctx.destination); o.start(); o.stop(this.ctx.currentTime+.08); },
425    playTick()  { if (this.muted || !this.ctx) return; const o = this.ctx.createOscillator(), g = this.ctx.createGain(); o.type='sine'; o.frequency.setValueAtTime(1200,this.ctx.currentTime); g.gain.setValueAtTime(.1,this.ctx.currentTime); g.gain.exponentialRampToValueAtTime(.01,this.ctx.currentTime+.05); o.connect(g); g.connect(this.ctx.destination); o.start(); o.stop(this.ctx.currentTime+.05); },
426    playError() { if (this.muted || !this.ctx) return; const o = this.ctx.createOscillator(), g = this.ctx.createGain(); o.type='sawtooth'; o.frequency.setValueAtTime(150,this.ctx.currentTime); o.frequency.linearRampToValueAtTime(100,this.ctx.currentTime+.1); g.gain.setValueAtTime(.1,this.ctx.currentTime); g.gain.linearRampToValueAtTime(0,this.ctx.currentTime+.1); o.connect(g); g.connect(this.ctx.destination); o.start(); o.stop(this.ctx.currentTime+.1); }
427};
428
429// ═══════════════════════════════════════════════
430// KEY MAPS
431// ═══════════════════════════════════════════════
432const NORMAL_MAP={'k':'ཀ','t':'ཏ','p':'པ','s':'ས','d':'ད','f':'ང','g':'ག','h':'ཧ','j':'ཇ','l':'ལ','z':'ཟ','x':'ཙ','c':'ཅ','v':'ཌ','b':'བ','n':'ན','m':'མ','q':'ཊ','w':'ཝ','e':'ེ','r':'ར','y':'ཡ','u':'ུ','i':'ི','o':'ོ','[':'༷',']':'྄','\\':'༔',';':'ཌྷ',"'":'འ',',':'ྱ','.':'ྲ','/':'།','`':'༌','1':'༡','2':'༢','3':'༣','4':'༤','5':'༥','6':'༦','7':'༧','8':'༨','9':'༩','0':'༠','-':'ྀ','=':'ཾ',' ':'་'};
433const SHIFT_MAP ={'k':'ཁ','t':'ཐ','p':'ཕ','a':'ཨ','s':'ཤ','d':'ཛ','f':'དྷ','g':'གྷ','h':'ྷ','j':'ཛྷ','z':'ཞ','x':'ཚ','c':'ཆ','v':'ཎ','b':'བྷ','n':'ཉ','m':'ཥ','q':'ཋ','w':'ྭ','e':'ཻ','r':'ཪ','u':'ཱུ','i':'ྀ','o':'ཽ','[':'ྂ',']':'༵','\\':'྅',';':'ཿ',"'":'ཱ','/':'„','`':'༸','1':'༑','2':'༄༅','3':'༅','4':'\u201C','5':'\u201D','6':'༈','7':'༼','8':'༽','9':'(','0':')','-':'ཱྀ','=':'ྃ',' ':' '};
434const SUBJOINED_MAP      ={'k':'ྐ','g':'ྒ','b':'ྦ','t':'ྟ','d':'ྡ','p':'ྤ','m':'ྨ','n':'ྣ','l':'ླ','s':'ྶ','h':'ྷ','j':'ྗ','c':'ྕ','x':'ྩ','f':'ྔ','q':'ྚ','v':'ྜ','w':'ྭ','r':'ྲ','y':'ྱ',"'":'ཱ'};
435const SUBJOINED_SHIFT_MAP={'n':'ྙ','d':'ྫ','k':'ྑ','t':'ྠ','p':'ྥ','c':'ྪ','x':'ཚ'};
436const LEFT_HAND_KEYS  = ['`','1','2','3','4','5','q','w','e','r','t','a','s','d','f','g','z','x','c','v','b'];
437const AVAILABLE_SHIFTS = ['sa','sc','sd','sk','sm','sn','sp','sq','ss','st','sv','sw','sx','sz'];
438
439const poseMap={'default':'hands/rest.svg','་':'hands/space.svg',' ':'hands/sspace.svg','a':'hands/a.svg'};
440function generatePoseMap(){
441    const hasSvg = (k) => /^[a-z]$/i.test(k) || ['.', '/', ',', "'"].includes(k);
442
443    for(let[k,v]of Object.entries(NORMAL_MAP)){
444        if(v==='་'||v===' ')continue;
445        if(!hasSvg(k)) continue; 
446        let fn=`hands/${k}.svg`;
447        if(k==='.')fn='hands/rata.svg';
448        if(k==='/')fn='hands/shea.svg';
449        poseMap[v]=fn;
450    }
451    for(let[k,v]of Object.entries(SHIFT_MAP)){
452        if(v===' ')continue;
453        if(!hasSvg(k)) continue;
454        let sn='s'+k,fn=`hands/${sn}.svg`;
455        if(!AVAILABLE_SHIFTS.includes(sn)){
456            fn=`hands/${k}.svg`;
457            if(k==='.')fn='hands/rata.svg';
458            if(k==='/')fn='hands/shea.svg';
459        }
460        poseMap[v]=fn;
461    }
462    for(let[k,v]of Object.entries(SUBJOINED_MAP)){
463        if(!hasSvg(k)) continue;
464        let fn=`hands/${k}.svg`;
465        if(k==='.')fn='hands/rata.svg';
466        if(k==='/')fn='hands/shea.svg';
467        if(!poseMap[v])poseMap[v]=fn;
468    }
469    for(let[k,v]of Object.entries(SUBJOINED_SHIFT_MAP)){
470        if(!hasSvg(k)) continue;
471        let sn='s'+k,fn=`hands/${sn}.svg`;
472        if(!AVAILABLE_SHIFTS.includes(sn)){
473            fn=`hands/${k}.svg`;
474            if(k==='.')fn='hands/rata.svg';
475            if(k==='/')fn='hands/shea.svg';
476        }
477        if(!poseMap[v])poseMap[v]=fn;
478    }
479    poseMap['ྱ']='hands/,.svg';
480    poseMap['ྲ']='hands/rata.svg';
481    poseMap['ྭ']='hands/sw.svg';
482    poseMap['ཱ']="hands/s'.svg";
483    poseMap['།']='hands/shea.svg';
484}
485generatePoseMap();
486const svgCache={};
487
488// ═══════════════════════════════════════════════
489// ATOMIC UNITS  (FIX)
490// Single keystrokes that output 2+ NON-combining codepoints (e.g. Shift+2 → ༄༅).
491// The default segmenter would wrongly split these into separate stacks, breaking
492// both the keyboard guide and the hit-detection. We protect them as one stack.
493// Derived from the keymaps so any future "one key → multiple base glyphs" entry
494// is covered automatically.
495// ═══════════════════════════════════════════════
496const ATOMIC_UNITS = (() => {
497    const set = new Set();
498    const isMod = cc => (cc>=0x0F71&&cc<=0x0F8D)||(cc>=0x0F90&&cc<=0x0FBC)||cc===0x0F35||cc===0x0F37;
499    [NORMAL_MAP, SHIFT_MAP, SUBJOINED_MAP, SUBJOINED_SHIFT_MAP].forEach(map => {
500        Object.values(map).forEach(v => {
501            if (!v || v.length < 2) return;
502            let bases = 0;
503            for (const ch of v) if (!isMod(ch.charCodeAt(0))) bases++;
504            if (bases >= 2) set.add(v); // 2+ base glyphs = not a normal stack
505        });
506    });
507    return set;
508})();
509
510function shouldEnterStacking(nextChar,currentTyped){
511    if(currentTyped==='')return false;
512    const DIRECT=new Set(['ྱ','ྲ','ྭ','ཱ','།','་','ྃ','ཿ','ཾ','ྀ','ཱྀ']);
513    if(DIRECT.has(nextChar))return false;
514    return Object.values(SUBJOINED_MAP).includes(nextChar)||Object.values(SUBJOINED_SHIFT_MAP).includes(nextChar);
515}
516
517// ═══════════════════════════════════════════════
518// SEGMENTER  (FIX — now respects ATOMIC_UNITS)
519// ═══════════════════════════════════════════════
520function segmentWordIntoStacks(word){
521    let stacks=[], cur='', i=0;
522    while(i < word.length){
523        // Keep atomic multi-glyph units (like ༄༅) together as ONE stack.
524        let unit = null;
525        for(const u of ATOMIC_UNITS){
526            if(word.startsWith(u, i) && (!unit || u.length > unit.length)) unit = u;
527        }
528        if(unit){
529            if(cur!==''){ stacks.push(cur); cur=''; }
530            stacks.push(unit);
531            i += unit.length;
532            continue;
533        }
534        const cc = word.charCodeAt(i);
535        const isMod=(cc>=0x0F71&&cc<=0x0F8D)||(cc>=0x0F90&&cc<=0x0FBC)||cc===0x0F35||cc===0x0F37;
536        if(!isMod){ if(cur!=='') stacks.push(cur); cur=word[i]; }
537        else { cur+=word[i]; }
538        i++;
539    }
540    if(cur!=='') stacks.push(cur);
541    return stacks;
542}
543
544// ═══════════════════════════════════════════════
545// DYNAMIC TEXT ENGINE (FIXED APPEND BEHAVIOR)
546// ═══════════════════════════════════════════════
547function appendMoreText() {
548    let para = window.TK_TIMER_TEXTS[Math.floor(Math.random() * window.TK_TIMER_TEXTS.length)];
549    para = para.replace(/[\s\r\n]+/g,' ').trim();
550    para = para.replace(/་+/g,'་');
551    if(!para.endsWith('་') && !para.endsWith('།')) para += '་';
552
553    // If we are appending to existing text, prepend a space so it flows naturally
554    if (targetStacks.length > 0 && !para.startsWith(' ')) {
555        para = ' ' + para;
556    }
557
558    let stackCounter = targetStacks.length; // Start from where we left off
559    
560    // REUSE existing wrapper instead of creating a new one to prevent split layouts
561    let pWrapper = document.querySelector('.paragraph-wrapper');
562    if (!pWrapper) {
563        pWrapper = document.createElement('div');
564        pWrapper.className = 'paragraph-wrapper';
565        container.appendChild(pWrapper);
566    }
567
568    const stacks = segmentWordIntoStacks(para);
569    let currentWordDiv = document.createElement('div');
570    currentWordDiv.className = 'tib-word';
571
572    stacks.forEach(stack => {
573        targetStacks.push({text:stack,type:'char'});
574        const span = document.createElement('span');
575        span.innerText = stack; span.className = 'char';
576        
577        if(stack.trim()==='་'||stack.trim()==='།') span.classList.add('tsek');
578        if(stack.includes('།')) span.classList.add('sentence-break');
579        if(stack === ' ') span.classList.add('space-char');
580        
581        span.id=`stack-${stackCounter}`;
582        currentWordDiv.appendChild(span);
583        stackCounter++;
584
585        if (stack.includes('་') || stack.includes('།') || stack === ' ') {
586            pWrapper.appendChild(currentWordDiv);
587            currentWordDiv = document.createElement('div');
588            currentWordDiv.className = 'tib-word';
589        }
590    });
591    
592    if (currentWordDiv.childNodes.length > 0) {
593        pWrapper.appendChild(currentWordDiv);
594    }
595    
596    // If game has already started, ensure the new elements format correctly
597    if (gameStarted) {
598        renderState();
599    }
600}
601
602function preloadAssets(){
603    const unique=new Set();
604    targetStacks.forEach(s=>{if(s.type==='char'){for(let c of s.text)unique.add(c);}});
605    const assets=[poseMap['default'],poseMap['་'],poseMap[' '],poseMap['a']];
606    unique.forEach(c=>{if(c==='ལ'){assets.push('hands/a.svg');assets.push('hands/l.svg');}else if(poseMap[c])assets.push(poseMap[c]);});
607    const files=[...new Set(assets)].filter(Boolean);
608    Promise.all(files.map(src=>
609        fetch(src).then(r=>r.ok?r.text():Promise.reject()).then(t=>{
610            svgCache[src]=t.replace(/<svg([^>]*)>/,'<svg$1 width="100%" height="100%" preserveAspectRatio="xMidYMid meet">');
611        }).catch(()=>{svgCache[src]='';})
612    )).then(hidePreloader);
613    setTimeout(hidePreloader,2000);
614}
615
616let preloaderHidden=false;
617let doHideRan = false;
618function hidePreloader(){
619    if(preloaderHidden)return;preloaderHidden=true;
620    glitchSystem.init();
621    
622    const doHide=()=>{
623        if(doHideRan) return;
624        doHideRan = true;
625        setTimeout(()=>{
626            preloader.style.opacity='0';
627            setTimeout(()=>{
628                preloader.style.display='none';
629                renderState();
630                document.querySelector('.stage').style.opacity='1';
631                setTimeout(checkAutoScroll,300);
632            },500);
633        },200);
634    };
635
636    if(document.fonts && document.fonts.ready) {
637        document.fonts.ready.then(doHide);
638    } else {
639        doHide();
640    }
641    setTimeout(doHide,1500); // Fallback
642}
643
644// ═══════════════════════════════════════════════
645// TIMER & LIVE STATS
646// ═══════════════════════════════════════════════
647function startLiveStats(){
648    if(liveStatsInterval) clearInterval(liveStatsInterval);
649    liveStatsInterval = setInterval(()=>{
650        if(!startTime||resultState!=='hidden')return;
651        const el=(new Date()-startTime)/60000;
652        const wpm=el>0?Math.round((charCount/5)/el):0;
653        const acc=charCount>0?Math.max(0,Math.min(100,Math.round(((charCount-errorsCount)/charCount)*100))):100;
654        document.getElementById('live-wpm').innerText=wpm;
655        document.getElementById('live-acc').innerText=acc+'%';
656    },500);
657}
658
659function gameLoop() {
660    timeRemaining--;
661    updateTimerDisplay();
662    updateProgressBar();
663    if (timeRemaining <= 0) {
664        finishLevel();
665    }
666}
667
668function updateTimerDisplay() {
669    const m = Math.floor(timeRemaining / 60).toString().padStart(2,'0');
670    const s = (timeRemaining % 60).toString().padStart(2,'0');
671    document.getElementById('live-timer').innerText = `${m}:${s}`;
672}
673
674function updateProgressBar(){
675    const b = document.getElementById('typing-progress-bar');
676    if(selectedSeconds > 0) {
677        const percentage = 100 - ((timeRemaining / selectedSeconds) * 100);
678        b.style.width = percentage + '%';
679    }
680}
681
682function renderState(){
683    document.querySelectorAll('.char').forEach((span,i)=>{
684        span.className='char';
685        const s=targetStacks[i];
686        if(s.type==='enter'){span.classList.add('return-char');}
687        else{
688            if(s.text.trim()==='་'||s.text.trim()==='།')span.classList.add('tsek');
689            if(s.text.includes('།'))span.classList.add('sentence-break');
690            if(s.text === ' ')span.classList.add('space-char');
691        }
692        if(i<currentStackIndex)span.classList.add('correct');
693        if(i===currentStackIndex){
694            span.classList.add('current');
695            if(s.type==='char'){
696                document.getElementById('current-ghost')?.remove();
697                const g=document.createElement('span');g.className='ghost-text';g.id='current-ghost';g.innerText=currentTyped;span.appendChild(g);
698            }
699        }
700    });
701    updateGuide();
702}
703
704// ═══════════════════════════════════════════════
705// FIXED UPDATE GUIDE (MULTI-CHAR & MISSING SVGS)
706// ═══════════════════════════════════════════════
707function updateGuide(){
708    document.querySelectorAll('.key-guide').forEach(el=>el.classList.remove('key-guide'));
709    handContainer.innerHTML='';
710    if(currentStackIndex>=targetStacks.length)return;
711    
712    const targetObj=targetStacks[currentStackIndex];
713    if(targetObj.type==='enter'){
714        handContainer.innerHTML = svgCache[poseMap['enter']] || svgCache[poseMap['default']] || '';
715        highlightSVGKey('ENTER','key-guide');
716        return;
717    }
718    
719    const targetStack=targetObj.text;
720    if(!targetStack.startsWith(currentTyped))return;
721    
722    const remaining = targetStack.slice(currentTyped.length);
723    if(!remaining)return;
724    
725    const DIRECT=new Set(['ྱ','ྲ','ྭ','ཱ','།','་','ྃ','ཿ','ཾ','ྀ','ཱྀ']);
726    let key='', shift=false, requiresA=false, matchedVal='';
727    
728    const findKey = (map, isShift) => {
729        for(let [k, v] of Object.entries(map)) {
730            if(remaining.startsWith(v)) {
731                if (v.length > matchedVal.length) {
732                    key = k;
733                    shift = isShift;
734                    matchedVal = v;
735                }
736            }
737        }
738    };
739    
740    if(isStackingMode){
741        findKey(SUBJOINED_MAP, false);
742        findKey(SUBJOINED_SHIFT_MAP, true);
743        if(!key) findKey(NORMAL_MAP, false);
744        if(!key) findKey(SHIFT_MAP, true);
745    } else if(DIRECT.has(remaining[0])){
746        findKey(NORMAL_MAP, false);
747        findKey(SHIFT_MAP, true);
748    } else {
749        findKey(NORMAL_MAP, false);
750        findKey(SHIFT_MAP, true);
751        if(!key) requiresA = true;
752    }
753    
754    if(requiresA&&!isStackingMode){key='a';shift=false;}
755    else if(requiresA&&isStackingMode){
756        findKey(SUBJOINED_MAP, false);
757        if(!key) findKey(SUBJOINED_SHIFT_MAP, true);
758    }
759    
760    let poseFile=poseMap[matchedVal || remaining[0]];
761    if(key==='a')poseFile=poseMap['a'];
762    
763    if(key&&!poseFile){
764        if(isStackingMode&&SUBJOINED_MAP[key]) poseFile=poseMap[SUBJOINED_MAP[key]];
765        else if(NORMAL_MAP[key]) poseFile=poseMap[NORMAL_MAP[key]];
766    }
767
768    if(key) {
769        if (poseFile && svgCache[poseFile]) {
770            handContainer.innerHTML = svgCache[poseFile];
771        } else {
772            // No dedicated finger SVG (e.g. number/symbol keys like Shift+2) → show neutral rest pose
773            handContainer.innerHTML = svgCache[poseMap['default']] || '';
774        }
775        highlightSVGKey(key,'key-guide');
776        if(shift) highlightShiftKey(LEFT_HAND_KEYS.includes(key)?'right':'left','key-guide');
777    } else {
778        handContainer.innerHTML = svgCache[poseMap['default']] || '';
779    }
780}
781
782function highlightSVGKey(k,cls){
783    const t=(k===' ')?'SPACEBAR':k.toUpperCase();
784    document.querySelectorAll('text.st0').forEach(txt=>{if(txt.textContent.trim()===t)txt.parentElement.querySelector('rect.st1')?.classList.add(cls);});
785}
786function highlightShiftKey(side,cls){
787    const shifts=Array.from(document.querySelectorAll('text.st0')).filter(t=>t.textContent.trim()==='SHIFT');
788    if(shifts.length>=2)(side==='left'?shifts[0]:shifts[1]).parentElement.querySelector('rect.st1')?.classList.add(cls);
789}
790
791function checkAutoScroll(){
792    const active = document.querySelector('.char.current');
793    if(active) { active.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
794}
795
796// ═══════════════════════════════════════════════
797// KEYDOWN
798// ═══════════════════════════════════════════════
799window.addEventListener('keydown',e=>{
800    if (document.getElementById('setup-overlay').style.display !== 'none') return;
801    if (resultState !== 'hidden') {
802        if(e.key==='Shift'||e.code==='ShiftLeft'||e.code==='ShiftRight') window.location.reload();
803        if(e.code==='Space') window.location.href='index.html';
804        return;
805    }
806    if (e.ctrlKey||e.metaKey||e.key==='Tab'||e.key==='Enter') { e.preventDefault(); return; }
807
808    // Start Timer on FIRST keystroke
809    if (!gameStarted && (e.key !== 'Shift' && e.key !== 'Alt' && e.key !== 'Control')) {
810        gameStarted = true;
811        document.querySelector('.phase-indicator').innerText = "Session Active";
812        startTime = new Date();
813        startLiveStats();
814        timerInterval = setInterval(gameLoop, 1000);
815    }
816
817    // BACKSPACE
818    if(e.key==='Backspace'){
819        e.preventDefault();sfx.init();
820        if(isStackingMode){isStackingMode=false;updateGuide();return;}
821        if(currentTyped.length>0){
822            currentTyped=currentTyped.slice(0,-1);
823            const span=document.getElementById(`stack-${currentStackIndex}`);
824            const ghost=document.getElementById('current-ghost');
825            if(ghost){ghost.innerText=currentTyped;span?.classList.remove('wrong');ghost.classList.remove('error');updateGuide();}
826        }
827        return;
828    }
829
830    const targetStack=targetStacks[currentStackIndex].text;
831    if(!targetStack.startsWith(currentTyped)){sfx.init();sfx.playError();triggerErrorGlitch();return;}
832
833    // 'a' STACKING GATE
834    if(e.key==='a'&&!e.shiftKey&&!isStackingMode){
835        e.preventDefault();sfx.init();
836        const nextChar=targetStack[currentTyped.length];
837        if(!shouldEnterStacking(nextChar,currentTyped)){
838            errorsCount++;sfx.playError();triggerErrorGlitch();
839            const span=document.getElementById(`stack-${currentStackIndex}`);
840            span?.classList.add('wrong');setTimeout(()=>span?.classList.remove('wrong'),200);
841            const ghost=document.getElementById('current-ghost');
842            if(ghost){ghost.classList.add('error');if(errorTimeout)clearTimeout(errorTimeout);
843                errorTimeout=setTimeout(()=>{if(ghost&&ghost.isConnected)ghost.classList.remove('error');},300);}
844            return;
845        }
846        isStackingMode=true;sfx.playTick();updateGuide();return;
847    }
848
849    // REGULAR KEY
850    const unshift={'~':'`','!':'1','@':'2','#':'3','$':'4','%':'5','^':'6','&':'7','*':'8','(':'9',')':'0','_':'-','+':'=','{':'[','}':']','|':'\\',':':';','"':"'",'<':',','>':'.','?':'/'};
851    let pk=e.key.toLowerCase();
852    if(e.shiftKey&&unshift[e.key])pk=unshift[e.key];
853    const attemptedStack=isStackingMode;
854    let typed='';
855    if(isStackingMode){typed=e.shiftKey?SUBJOINED_SHIFT_MAP[pk]:SUBJOINED_MAP[pk];}
856    else{typed=e.shiftKey?SHIFT_MAP[pk]:NORMAL_MAP[pk];}
857    if(e.code==='Space'){typed=e.shiftKey?' ':'་';}
858    if(!typed)return;
859    
860    e.preventDefault();sfx.init();
861    highlightSVGKey(pk===' '?'SPACEBAR':pk,'key-hit');
862    setTimeout(()=>document.querySelectorAll('.key-hit').forEach(el=>el.classList.remove('key-hit')),100);
863    
864    if(currentStackIndex>=targetStacks.length) return;
865    
866    const prospective=currentTyped+typed;
867    if(targetStack.startsWith(prospective)){
868        if(attemptedStack)isStackingMode=false;
869        sfx.playClick();currentTyped=prospective;charCount++;
870        const ghost=document.getElementById('current-ghost');
871        if(ghost){ghost.innerText=currentTyped;ghost.classList.remove('error');if(errorTimeout){clearTimeout(errorTimeout);errorTimeout=null;}}
872        
873        if(currentTyped===targetStack){
874            const cur=document.getElementById(`stack-${currentStackIndex}`);
875            cur?.classList.remove('current','wrong');cur?.classList.add('correct');ghost?.remove();
876            currentStackIndex++;currentTyped='';
877            
878            // IF we run out of text, append more!
879            if (currentStackIndex >= targetStacks.length - 1) {
880                appendMoreText(); 
881            }
882
883            if(currentStackIndex<targetStacks.length){
884                const next=document.getElementById(`stack-${currentStackIndex}`);
885                next?.classList.add('current');
886                document.getElementById('current-ghost')?.remove();
887                const ng=document.createElement('span');ng.className='ghost-text';ng.id='current-ghost';next?.appendChild(ng);
888                updateGuide();checkAutoScroll();
889            }
890        }else{updateGuide();}
891    }else{
892        errorsCount++;sfx.playError();triggerErrorGlitch();
893        const span=document.getElementById(`stack-${currentStackIndex}`);
894        span?.classList.add('wrong');setTimeout(()=>span?.classList.remove('wrong'),200);
895        const ghost=document.getElementById('current-ghost');
896        if(ghost){ghost.innerText=prospective;ghost.classList.add('error');
897            if(errorTimeout)clearTimeout(errorTimeout);
898            errorTimeout=setTimeout(()=>{if(ghost&&ghost.isConnected){ghost.classList.remove('error');ghost.innerText=currentTyped;}},300);}
899    }
900});
901
902// ═══════════════════════════════════════════════
903// RESULT SYSTEM
904// ═══════════════════════════════════════════════
905const resultParticles={
906    canvas:null,ctx:null,particles:[],active:false,W:0,H:0,
907    GC:['#FFD700','#00FFFF','#00E676','#FF6B9D','#C471ED','#74B9FF','#FF9F1C','#FFB7C5','#FFFFFF','#d4af37'],
908    GL:['ཀ','ཁ','ག','ང','ཅ','ཆ','ཇ','ཉ','ཏ','ཐ','ད','ན','པ','ཕ','བ','མ','ར','ལ','ས','ཨ','་','།'],
909    rg(){return this.GL[Math.floor(Math.random()*this.GL.length)];},
910    rc(){return this.GC[Math.floor(Math.random()*this.GC.length)];},
911    init(){this.canvas=document.getElementById('result-canvas');if(!this.canvas)return;this.W=this.canvas.width=window.innerWidth;this.H=this.canvas.height=window.innerHeight;this.ctx=this.canvas.getContext('2d');},
912    statBurst(x,y,color){for(let i=0;i<12;i++){const a=Math.random()*Math.PI*2,sp=1.5+Math.random()*7;this.particles.push({type:'burst',x,y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-1,gravity:.1,alpha:1,decay:.024+Math.random()*.016,size:10+Math.random()*16,color,char:this.rg(),rot:Math.random()*Math.PI*2,rotSpd:(Math.random()-.5)*.15});}if(!this.active){this.active=true;this._loop();}},
913    _loop(){
914        if(!this.active||!this.ctx)return;
915        const{canvas,ctx,W,H}=this; ctx.clearRect(0,0,W,H);
916        this.particles=this.particles.filter(p=>p.alpha>.015);
917        for(const p of this.particles){
918            if(p.type==='burst'){p.x+=p.vx;
918p.y+=p.vy;p.vy+=p.gravity;p.vx*=.97;p.alpha-=p.decay;p.rot+=p.rotSpd;ctx.save();ctx.globalAlpha=Math.max(0,p.alpha);ctx.font=`${p.size}px OuChan2,serif`;ctx.fillStyle=p.color;ctx.textAlign='center';ctx.textBaseline='middle';ctx.shadowColor=p.color;ctx.shadowBlur=14;ctx.translate(p.x,p.y);ctx.rotate(p.rot);ctx.fillText(p.char,0,0);ctx.restore();}
919        }
920        if(this.particles.length>0)requestAnimationFrame(()=>this._loop());else{this.active=false;ctx.clearRect(0,0,W,H);}
921    }
922};
923
924function animateCounter(el,target,delay=0,onDone){
925    setTimeout(()=>{
926        el.classList.add('boom');setTimeout(()=>el.classList.remove('boom'),700);
927        if(typeof target==='string'){el.textContent=target;if(onDone)onDone();return;}
928        const dur=900,start=performance.now();
929        function step(now){const t=Math.min((now-start)/dur,1);const ease=1-Math.pow(1-t,4);el.textContent=Math.round(ease*target);if(t<1)requestAnimationFrame(step);else{el.textContent=target;if(onDone)onDone();}}
930        requestAnimationFrame(step);
931    },delay);
932}
933
934function finishLevel(){
935    if(timerInterval) clearInterval(timerInterval);
936    if(liveStatsInterval) clearInterval(liveStatsInterval);
937    
938    // Exact mathematical output. We use selectedSeconds here.
939    const minutes = selectedSeconds / 60;
940    const wpm = minutes > 0 ? Math.round((charCount/5)/minutes) : 0;
941    const acc = charCount > 0 ? Math.max(0, Math.min(100, Math.round(((charCount-errorsCount)/charCount)*100))) : 0;
942
943    const mF = Math.floor(selectedSeconds / 60).toString().padStart(2,'0');
944    const sF = (selectedSeconds % 60).toString().padStart(2,'0');
945    document.getElementById('res-duration').innerText = `${mF}:${sF}`;
946
947    document.querySelectorAll('.res-stat').forEach(s=>s.classList.remove('visible'));
948    document.getElementById('res-wpm').innerText=0;
949    document.getElementById('res-acc').innerText=0;
950
951    document.getElementById('result-modal').style.display='flex';
952    resultState='animating';
953
954    resultParticles.init();
955    
956    const we=document.getElementById('res-wpm'),ae=document.getElementById('res-acc');
957    const st=document.querySelectorAll('.res-stat');
958    setTimeout(()=>{st[0]?.classList.add('visible');animateCounter(we,wpm,0,()=>{const r=we.getBoundingClientRect();resultParticles.statBurst(r.left+r.width/2,r.top+r.height/2,'#00FFFF');});},500);
959    setTimeout(()=>{st[1]?.classList.add('visible');animateCounter(ae,acc,0,()=>{const r=ae.getBoundingClientRect();resultParticles.statBurst(r.left+r.width/2,r.top+r.height/2,'#00E676');});},900);
960    setTimeout(()=>{resultState='finished';}, 1500);
961}
962
963// ═══════════════════════════════════════════════
964// INITIAL LOAD
965// ═══════════════════════════════════════════════
966window.addEventListener('load',()=>{
967    const savedLayout = localStorage.getItem('tk_pref_layout_paragraph');
968    setLayout(savedLayout || 'layout-small', false);
969
970    if(localStorage.getItem(PREF_HANDS)==='true') toggleHands(false);
971    if(localStorage.getItem(PREF_SOUND)==='true'){ sfx.muted=true; document.getElementById('sound-btn').classList.add('muted'); }
972
973    const savedSize = localStorage.getItem(PREF_TEXT_SIZE);
974    if(savedSize){ currentFontSize = parseInt(savedSize); document.body.style.setProperty('--dynamic-font-size',currentFontSize+'px'); }
975});
976</script>
976
977</body>
978</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.