Fu01978 commited on
Commit
2961f29
·
verified ·
1 Parent(s): 44bf64f

Upload index (7).html

Browse files
Files changed (1) hide show
  1. templates/index (7).html +1551 -0
templates/index (7).html ADDED
@@ -0,0 +1,1551 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
+ <title>Chess Analyzer</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Pro:ital,wght@0,300;0,400;0,600;1,300;1,400&display=swap" rel="stylesheet">
9
+
10
+ <style>
11
+ /* ── Root Variables ─────────────────────────────────────────────── */
12
+ :root {
13
+ --bg: #0c0c0e;
14
+ --surface: #141416;
15
+ --card: #1c1c20;
16
+ --card2: #222228;
17
+ --border: #2e2e36;
18
+ --border-soft: #252530;
19
+ --text: #e4dfd6;
20
+ --text-dim: #7a7068;
21
+ --text-muted: #4a4848;
22
+ --gold: #c8a84c;
23
+ --gold-dim: #8a7030;
24
+
25
+ --sq-light: #f0d9b5;
26
+ --sq-dark: #b58863;
27
+ --sq-hi-light: #cdd26a;
28
+ --sq-hi-dark: #aaa23a;
29
+ --sq-hi-best: rgba(0, 160, 80, 0.5);
30
+
31
+ --cl-brilliant: #00b5ad;
32
+ --cl-great: #1a4fc4;
33
+ --cl-best: #7ec820;
34
+ --cl-excellent: #7ec820;
35
+ --cl-good: #4db896;
36
+ --cl-book: #a0754a;
37
+ --cl-inaccuracy:#d4b030;
38
+ --cl-mistake: #d07020;
39
+ --cl-blunder: #c83030;
40
+ --cl-omission: #e05050;
41
+
42
+ --board-size: 480px;
43
+ --sq-size: 60px;
44
+ }
45
+
46
+ * { box-sizing: border-box; margin: 0; padding: 0; }
47
+
48
+ html, body {
49
+ height: 100%;
50
+ background: var(--bg);
51
+ color: var(--text);
52
+ font-family: 'Crimson Pro', Georgia, serif;
53
+ font-size: 16px;
54
+ line-height: 1.5;
55
+ }
56
+
57
+ /* ─────────────────────────────────────────────────────────────────
58
+ SCREENS
59
+ ───────────────────────────────────────────────────────────────── */
60
+ .screen { display: none; min-height: 100vh; flex-direction: column; }
61
+ .screen.active { display: flex; }
62
+
63
+ /* ── Header ──────────────────────────────────────────────────── */
64
+ header {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 14px;
68
+ padding: 18px 32px;
69
+ border-bottom: 1px solid var(--border);
70
+ background: var(--surface);
71
+ flex-shrink: 0;
72
+ }
73
+
74
+ .logo {
75
+ font-family: 'Cinzel', serif;
76
+ font-size: 1.3rem;
77
+ font-weight: 700;
78
+ color: var(--gold);
79
+ letter-spacing: 0.04em;
80
+ }
81
+
82
+ .logo-sub {
83
+ font-family: 'Cinzel', serif;
84
+ font-size: 0.7rem;
85
+ color: var(--text-dim);
86
+ letter-spacing: 0.12em;
87
+ text-transform: uppercase;
88
+ }
89
+
90
+ /* ── Upload Screen ────────────────────────────────────────────── */
91
+ #upload-screen {
92
+ align-items: center;
93
+ justify-content: center;
94
+ gap: 0;
95
+ }
96
+
97
+ .upload-box {
98
+ width: 560px;
99
+ max-width: 95vw;
100
+ }
101
+
102
+ .upload-title {
103
+ font-family: 'Cinzel', serif;
104
+ font-size: 2rem;
105
+ font-weight: 600;
106
+ color: var(--text);
107
+ margin-bottom: 6px;
108
+ }
109
+
110
+ .upload-sub {
111
+ color: var(--text-dim);
112
+ font-size: 1rem;
113
+ font-style: italic;
114
+ margin-bottom: 32px;
115
+ }
116
+
117
+ .drop-zone {
118
+ border: 1.5px dashed var(--border);
119
+ border-radius: 10px;
120
+ padding: 24px;
121
+ background: var(--card);
122
+ transition: border-color 0.2s, background 0.2s;
123
+ cursor: pointer;
124
+ }
125
+
126
+ .drop-zone:hover, .drop-zone.dragover {
127
+ border-color: var(--gold);
128
+ background: var(--card2);
129
+ }
130
+
131
+ .drop-zone-label {
132
+ font-size: 0.85rem;
133
+ color: var(--text-dim);
134
+ text-align: center;
135
+ margin-bottom: 12px;
136
+ }
137
+
138
+ .drop-zone-label span { color: var(--gold); cursor: pointer; }
139
+
140
+ #pgn-textarea {
141
+ width: 100%;
142
+ height: 180px;
143
+ background: var(--card2);
144
+ border: 1px solid var(--border-soft);
145
+ border-radius: 6px;
146
+ color: var(--text);
147
+ font-family: 'Crimson Pro', monospace;
148
+ font-size: 0.88rem;
149
+ padding: 12px;
150
+ resize: vertical;
151
+ outline: none;
152
+ transition: border-color 0.2s;
153
+ }
154
+ #pgn-textarea:focus { border-color: var(--gold-dim); }
155
+ #pgn-textarea::placeholder { color: var(--text-muted); }
156
+
157
+ .upload-row {
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: space-between;
161
+ margin-top: 16px;
162
+ gap: 12px;
163
+ }
164
+
165
+ .depth-row {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 10px;
169
+ font-size: 0.88rem;
170
+ color: var(--text-dim);
171
+ }
172
+
173
+ .depth-row input[type=range] {
174
+ width: 100px;
175
+ accent-color: var(--gold);
176
+ }
177
+
178
+ #depth-val { color: var(--text); font-weight: 600; }
179
+
180
+ .btn-primary {
181
+ background: var(--gold);
182
+ color: #1a1200;
183
+ font-family: 'Cinzel', serif;
184
+ font-size: 0.85rem;
185
+ font-weight: 700;
186
+ letter-spacing: 0.06em;
187
+ border: none;
188
+ border-radius: 6px;
189
+ padding: 10px 28px;
190
+ cursor: pointer;
191
+ transition: background 0.15s, transform 0.1s;
192
+ }
193
+ .btn-primary:hover { background: #dfc060; }
194
+ .btn-primary:active { transform: scale(0.97); }
195
+
196
+ #file-input { display: none; }
197
+
198
+ /* ── Loading Screen ───────────────────────────────────────────── */
199
+ #loading-screen {
200
+ align-items: center;
201
+ justify-content: center;
202
+ gap: 32px;
203
+ }
204
+
205
+ .loading-chess-icon {
206
+ font-size: 3.5rem;
207
+ animation: float 2s ease-in-out infinite;
208
+ }
209
+
210
+ @keyframes float {
211
+ 0%, 100% { transform: translateY(0); }
212
+ 50% { transform: translateY(-10px); }
213
+ }
214
+
215
+ .loading-title {
216
+ font-family: 'Cinzel', serif;
217
+ font-size: 1.1rem;
218
+ color: var(--gold);
219
+ letter-spacing: 0.08em;
220
+ }
221
+
222
+ .loading-info {
223
+ font-size: 0.95rem;
224
+ color: var(--text-dim);
225
+ font-style: italic;
226
+ }
227
+
228
+ .progress-wrap {
229
+ width: 360px;
230
+ background: var(--card);
231
+ border-radius: 100px;
232
+ height: 6px;
233
+ overflow: hidden;
234
+ border: 1px solid var(--border);
235
+ }
236
+
237
+ .progress-bar {
238
+ height: 100%;
239
+ background: linear-gradient(90deg, var(--gold-dim), var(--gold));
240
+ border-radius: 100px;
241
+ transition: width 0.3s ease;
242
+ width: 0%;
243
+ }
244
+
245
+ #loading-move-label {
246
+ font-size: 0.9rem;
247
+ color: var(--text-dim);
248
+ font-family: 'Cinzel', serif;
249
+ letter-spacing: 0.06em;
250
+ min-height: 1.4em;
251
+ }
252
+
253
+ /* ── Game Screen ──────────────────────────────────────────────── */
254
+ #game-screen {
255
+ flex: 1;
256
+ }
257
+
258
+ .game-header {
259
+ display: flex;
260
+ align-items: center;
261
+ justify-content: space-between;
262
+ padding: 14px 28px;
263
+ border-bottom: 1px solid var(--border);
264
+ background: var(--surface);
265
+ }
266
+
267
+ .game-players {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 12px;
271
+ font-family: 'Cinzel', serif;
272
+ }
273
+
274
+ .player-white { color: #e8e0d0; font-size: 1rem; font-weight: 600; }
275
+ .player-black { color: #888; font-size: 1rem; font-weight: 600; }
276
+ .vs-sep { color: var(--text-muted); font-size: 0.8rem; }
277
+ .game-meta { font-size: 0.82rem; color: var(--text-dim); font-style: italic; }
278
+
279
+ .btn-new-game {
280
+ background: transparent;
281
+ border: 1px solid var(--border);
282
+ color: var(--text-dim);
283
+ font-family: 'Crimson Pro', serif;
284
+ font-size: 0.85rem;
285
+ padding: 6px 16px;
286
+ border-radius: 5px;
287
+ cursor: pointer;
288
+ transition: border-color 0.2s, color 0.2s;
289
+ }
290
+ .btn-new-game:hover { border-color: var(--gold-dim); color: var(--gold); }
291
+
292
+ /* ── Main layout ─────────────────────────────────────────────── */
293
+ .game-body {
294
+ display: flex;
295
+ flex: 1;
296
+ gap: 0;
297
+ padding: 24px 24px 16px;
298
+ align-items: flex-start;
299
+ justify-content: center;
300
+ gap: 20px;
301
+ }
302
+
303
+ /* ── Eval bar ─────────────────────────────────────────────────── */
304
+ .eval-col {
305
+ display: flex;
306
+ flex-direction: column;
307
+ align-items: center;
308
+ gap: 6px;
309
+ padding-top: 4px;
310
+ }
311
+
312
+ .eval-bar-wrap {
313
+ width: 16px;
314
+ height: var(--board-size);
315
+ background: #1a1010;
316
+ border-radius: 5px;
317
+ overflow: hidden;
318
+ position: relative;
319
+ border: 1px solid var(--border);
320
+ }
321
+
322
+ .eval-bar-white {
323
+ position: absolute;
324
+ bottom: 0;
325
+ left: 0;
326
+ right: 0;
327
+ background: #ece8e0;
328
+ transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1);
329
+ height: 50%;
330
+ }
331
+
332
+ .eval-label {
333
+ font-family: 'Cinzel', serif;
334
+ font-size: 0.72rem;
335
+ color: var(--text-dim);
336
+ letter-spacing: 0.04em;
337
+ text-align: center;
338
+ min-height: 1.2em;
339
+ }
340
+
341
+ /* ── Board col ────────────────────────────────────────────────── */
342
+ .board-col {
343
+ display: flex;
344
+ flex-direction: column;
345
+ gap: 0;
346
+ }
347
+
348
+ .board-outer {
349
+ display: flex;
350
+ align-items: flex-start;
351
+ }
352
+
353
+ /* rank labels */
354
+ .rank-labels {
355
+ display: flex;
356
+ flex-direction: column;
357
+ justify-content: space-around;
358
+ width: 18px;
359
+ height: var(--board-size);
360
+ padding: 2px 0;
361
+ }
362
+
363
+ .rank-label {
364
+ font-size: 0.7rem;
365
+ color: var(--text-dim);
366
+ text-align: center;
367
+ height: var(--sq-size);
368
+ line-height: var(--sq-size);
369
+ font-family: 'Cinzel', serif;
370
+ }
371
+
372
+ /* file labels */
373
+ .file-labels {
374
+ display: flex;
375
+ width: var(--board-size);
376
+ margin-left: 18px;
377
+ padding-top: 4px;
378
+ }
379
+
380
+ .file-label {
381
+ width: var(--sq-size);
382
+ text-align: center;
383
+ font-size: 0.7rem;
384
+ color: var(--text-dim);
385
+ font-family: 'Cinzel', serif;
386
+ }
387
+
388
+ /* board grid */
389
+ #board {
390
+ display: grid;
391
+ grid-template-columns: repeat(8, var(--sq-size));
392
+ grid-template-rows: repeat(8, var(--sq-size));
393
+ width: var(--board-size);
394
+ height: var(--board-size);
395
+ position: relative;
396
+ }
397
+
398
+ .sq {
399
+ width: var(--sq-size);
400
+ height: var(--sq-size);
401
+ position: relative;
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: center;
405
+ cursor: default;
406
+ }
407
+
408
+ .sq.light { background: var(--sq-light); }
409
+ .sq.dark { background: var(--sq-dark); }
410
+
411
+ .sq.hi-from.light { background: var(--sq-hi-light); }
412
+ .sq.hi-from.dark { background: var(--sq-hi-dark); }
413
+ .sq.hi-to.light { background: var(--sq-hi-light); }
414
+ .sq.hi-to.dark { background: var(--sq-hi-dark); }
415
+
416
+ /* piece image */
417
+ .piece-img {
418
+ width: 54px;
419
+ height: 54px;
420
+ object-fit: contain;
421
+ user-select: none;
422
+ pointer-events: none;
423
+ display: block;
424
+ }
425
+
426
+ /* Unicode fallback */
427
+ .piece-uni {
428
+ font-size: 44px;
429
+ line-height: 1;
430
+ user-select: none;
431
+ pointer-events: none;
432
+ }
433
+
434
+ /* ── Classification badge ─────────────────────────────────────── */
435
+ .badge {
436
+ position: absolute;
437
+ top: 1px;
438
+ right: 1px;
439
+ width: 22px;
440
+ height: 22px;
441
+ border-radius: 50%;
442
+ display: flex;
443
+ align-items: center;
444
+ justify-content: center;
445
+ font-size: 9px;
446
+ font-weight: 800;
447
+ font-family: Arial, sans-serif;
448
+ color: white;
449
+ z-index: 20;
450
+ border: 2px solid rgba(0,0,0,0.35);
451
+ box-shadow: 0 2px 6px rgba(0,0,0,0.6);
452
+ letter-spacing: -0.5px;
453
+ pointer-events: none;
454
+ }
455
+
456
+ .badge-BRILLIANT { background: var(--cl-brilliant); }
457
+ .badge-GREAT { background: var(--cl-great); }
458
+ .badge-BEST { background: var(--cl-best); }
459
+ .badge-EXCELLENT { background: var(--cl-excellent); }
460
+ .badge-GOOD { background: var(--cl-good); }
461
+ .badge-BOOK { background: var(--cl-book); }
462
+ .badge-INACCURACY { background: var(--cl-inaccuracy);}
463
+ .badge-MISTAKE { background: var(--cl-mistake); }
464
+ .badge-BLUNDER { background: var(--cl-blunder); }
465
+ .badge-OMISSION { background: var(--cl-omission); }
466
+
467
+ /* ── Info panel below board ───────────────────────────────────── */
468
+ .board-info {
469
+ margin-top: 10px;
470
+ margin-left: 18px;
471
+ width: var(--board-size);
472
+ background: var(--card);
473
+ border: 1px solid var(--border-soft);
474
+ border-radius: 8px;
475
+ padding: 14px 18px;
476
+ display: flex;
477
+ flex-direction: column;
478
+ gap: 6px;
479
+ }
480
+
481
+ .move-label {
482
+ font-family: 'Cinzel', serif;
483
+ font-size: 1.05rem;
484
+ color: var(--text);
485
+ letter-spacing: 0.04em;
486
+ }
487
+
488
+ .move-class-row {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 10px;
492
+ }
493
+
494
+ .move-class-dot {
495
+ width: 12px;
496
+ height: 12px;
497
+ border-radius: 50%;
498
+ flex-shrink: 0;
499
+ }
500
+
501
+ .move-class-text {
502
+ font-size: 1rem;
503
+ font-weight: 600;
504
+ }
505
+
506
+ .move-notes {
507
+ font-size: 0.88rem;
508
+ color: var(--text-dim);
509
+ font-style: italic;
510
+ }
511
+
512
+ .move-cp {
513
+ font-size: 0.82rem;
514
+ color: var(--text-muted);
515
+ }
516
+
517
+ /* ── Navigation ──────────────────────────────────────────────── */
518
+ .nav-row {
519
+ display: flex;
520
+ align-items: center;
521
+ justify-content: center;
522
+ gap: 10px;
523
+ margin-top: 12px;
524
+ }
525
+
526
+ .nav-btn {
527
+ background: var(--card);
528
+ border: 1px solid var(--border);
529
+ color: var(--text-dim);
530
+ width: 44px;
531
+ height: 44px;
532
+ border-radius: 8px;
533
+ font-size: 1.2rem;
534
+ cursor: pointer;
535
+ display: flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
539
+ flex-shrink: 0;
540
+ }
541
+
542
+ .nav-btn:hover:not(:disabled) {
543
+ background: var(--card2);
544
+ border-color: var(--gold-dim);
545
+ color: var(--gold);
546
+ }
547
+
548
+ .nav-btn:disabled { opacity: 0.3; cursor: default; }
549
+
550
+ .nav-counter {
551
+ font-family: 'Cinzel', serif;
552
+ font-size: 0.78rem;
553
+ color: var(--text-muted);
554
+ letter-spacing: 0.06em;
555
+ min-width: 80px;
556
+ text-align: center;
557
+ }
558
+
559
+ /* ── Move list (right panel) ─────────────────────────────────── */
560
+ .move-list-panel {
561
+ width: 240px;
562
+ flex-shrink: 0;
563
+ background: var(--card);
564
+ border: 1px solid var(--border-soft);
565
+ border-radius: 10px;
566
+ display: flex;
567
+ flex-direction: column;
568
+ overflow: hidden;
569
+ max-height: calc(var(--board-size) + 50px);
570
+ }
571
+
572
+ .move-list-header {
573
+ padding: 12px 14px;
574
+ font-family: 'Cinzel', serif;
575
+ font-size: 0.78rem;
576
+ letter-spacing: 0.1em;
577
+ color: var(--text-dim);
578
+ border-bottom: 1px solid var(--border-soft);
579
+ text-transform: uppercase;
580
+ }
581
+
582
+ .move-list-body {
583
+ overflow-y: auto;
584
+ flex: 1;
585
+ padding: 6px 0;
586
+ scrollbar-width: thin;
587
+ scrollbar-color: var(--border) transparent;
588
+ }
589
+
590
+ .move-pair {
591
+ display: flex;
592
+ align-items: stretch;
593
+ }
594
+
595
+ .move-num {
596
+ width: 32px;
597
+ text-align: right;
598
+ padding: 4px 6px;
599
+ font-size: 0.78rem;
600
+ color: var(--text-muted);
601
+ font-family: 'Cinzel', serif;
602
+ flex-shrink: 0;
603
+ line-height: 26px;
604
+ }
605
+
606
+ .move-cell {
607
+ flex: 1;
608
+ padding: 4px 6px;
609
+ font-size: 0.9rem;
610
+ cursor: pointer;
611
+ border-radius: 4px;
612
+ display: flex;
613
+ align-items: center;
614
+ gap: 5px;
615
+ transition: background 0.12s;
616
+ min-height: 28px;
617
+ margin: 1px 2px;
618
+ }
619
+
620
+ .move-cell:hover { background: var(--card2); }
621
+ .move-cell.current { background: var(--card2); outline: 1px solid var(--gold-dim); }
622
+
623
+ .move-cell .cl-dot {
624
+ width: 7px;
625
+ height: 7px;
626
+ border-radius: 50%;
627
+ flex-shrink: 0;
628
+ }
629
+
630
+ .move-cell .san-text {
631
+ font-family: 'Crimson Pro', serif;
632
+ font-size: 0.95rem;
633
+ }
634
+
635
+ /* ── Eval summary bars ───────────────────────────────────────── */
636
+ .eval-summary {
637
+ padding: 12px 14px;
638
+ border-top: 1px solid var(--border-soft);
639
+ }
640
+
641
+ .eval-summary-row {
642
+ display: flex;
643
+ align-items: center;
644
+ gap: 8px;
645
+ margin-bottom: 4px;
646
+ }
647
+
648
+ .eval-summary-name {
649
+ font-size: 0.78rem;
650
+ color: var(--text-dim);
651
+ width: 36px;
652
+ font-family: 'Cinzel', serif;
653
+ }
654
+
655
+ .eval-summary-bar-wrap {
656
+ flex: 1;
657
+ height: 4px;
658
+ background: var(--bg);
659
+ border-radius: 2px;
660
+ overflow: hidden;
661
+ }
662
+
663
+ .eval-summary-bar { height: 100%; border-radius: 2px; }
664
+
665
+ .eval-accuracy {
666
+ font-size: 0.82rem;
667
+ color: var(--text-dim);
668
+ text-align: center;
669
+ font-style: italic;
670
+ margin-top: 6px;
671
+ }
672
+
673
+ /* ── Flip button ─────────────────────────────────────────────── */
674
+ .flip-btn {
675
+ background: transparent;
676
+ border: 1px solid var(--border);
677
+ color: var(--text-muted);
678
+ border-radius: 5px;
679
+ font-size: 0.75rem;
680
+ padding: 3px 10px;
681
+ cursor: pointer;
682
+ font-family: 'Crimson Pro', serif;
683
+ transition: border-color 0.15s, color 0.15s;
684
+ }
685
+ .flip-btn:hover { border-color: var(--gold-dim); color: var(--gold); }
686
+
687
+ /* ── Classification color helper ─────────────────────────────── */
688
+ .cl-BRILLIANT { color: var(--cl-brilliant); }
689
+ .cl-GREAT { color: var(--cl-great); }
690
+ .cl-BEST { color: var(--cl-best); }
691
+ .cl-EXCELLENT { color: var(--cl-excellent); }
692
+ .cl-GOOD { color: var(--cl-good); }
693
+ .cl-BOOK { color: var(--cl-book); }
694
+ .cl-INACCURACY { color: var(--cl-inaccuracy);}
695
+ .cl-MISTAKE { color: var(--cl-mistake); }
696
+ .cl-BLUNDER { color: var(--cl-blunder); }
697
+ .cl-OMISSION { color: var(--cl-omission); }
698
+
699
+ /* ── Scrollbar style ─────────────────────────────────────────── */
700
+ ::-webkit-scrollbar { width: 5px; }
701
+ ::-webkit-scrollbar-track { background: transparent; }
702
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
703
+
704
+
705
+ /* ── Continuations ───────────────────────────────────────────── */
706
+ .continuations {
707
+ margin-top: 8px;
708
+ display: flex;
709
+ flex-direction: column;
710
+ gap: 6px;
711
+ }
712
+
713
+ .cont-block {
714
+ background: var(--bg);
715
+ border: 1px solid var(--border-soft);
716
+ border-radius: 6px;
717
+ padding: 8px 12px;
718
+ }
719
+
720
+ .cont-label {
721
+ font-size: 0.72rem;
722
+ font-family: 'Cinzel', serif;
723
+ letter-spacing: 0.08em;
724
+ color: var(--text-muted);
725
+ text-transform: uppercase;
726
+ margin-bottom: 4px;
727
+ }
728
+
729
+ .cont-moves {
730
+ font-family: 'Crimson Pro', serif;
731
+ font-size: 0.92rem;
732
+ color: var(--text-dim);
733
+ line-height: 1.5;
734
+ word-spacing: 4px;
735
+ }
736
+
737
+ .cont-moves .cont-best-move {
738
+ color: var(--cl-best);
739
+ font-weight: 600;
740
+ }
741
+
742
+ .cont-moves .cont-player-move {
743
+ color: var(--text);
744
+ font-weight: 600;
745
+ }
746
+
747
+ /* ══════════════════════════════════════════════════════════════
748
+ MOBILE RESPONSIVE
749
+ ══════════════════════════════════════════════════════════════ */
750
+
751
+ /* Fluid board: fill the viewport minus eval bar and padding */
752
+ @media (max-width: 700px) {
753
+ :root {
754
+ --board-size: min(92vw, 420px);
755
+ --sq-size: calc(min(92vw, 420px) / 8);
756
+ }
757
+
758
+ .game-body {
759
+ flex-direction: column;
760
+ align-items: center;
761
+ padding: 10px 8px 8px;
762
+ gap: 10px;
763
+ }
764
+
765
+ /* Eval bar goes horizontal above the board on mobile */
766
+ .eval-col {
767
+ flex-direction: row;
768
+ align-items: center;
769
+ width: var(--board-size);
770
+ margin-left: 18px; /* align with board (rank label width) */
771
+ gap: 6px;
772
+ }
773
+
774
+ .eval-bar-wrap {
775
+ width: 100%;
776
+ height: 10px;
777
+ flex: 1;
778
+ }
779
+
780
+ .eval-bar-white {
781
+ /* horizontal bar: width instead of height */
782
+ height: 100%;
783
+ width: 50%;
784
+ top: 0;
785
+ left: 0;
786
+ transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
787
+ }
788
+
789
+ /* hide the W / B labels on mobile */
790
+ .eval-col > div:first-child,
791
+ .eval-col > div:last-child { display: none; }
792
+
793
+ .eval-label { font-size: 0.65rem; }
794
+
795
+ /* board info strip */
796
+ .board-info { margin-left: 18px; padding: 10px 12px; }
797
+
798
+ /* nav row tighter */
799
+ .nav-btn { width: 38px; height: 38px; font-size: 1rem; }
800
+ .nav-row { gap: 6px; }
801
+
802
+ /* Move list panel goes below the board, full width */
803
+ .move-list-panel {
804
+ width: var(--board-size);
805
+ max-height: 220px;
806
+ margin-left: 18px;
807
+ }
808
+
809
+ /* game header: stack players */
810
+ .game-header {
811
+ flex-wrap: wrap;
812
+ gap: 6px;
813
+ padding: 10px 14px;
814
+ }
815
+
816
+ .game-players { font-size: 0.9rem; }
817
+ .game-meta { font-size: 0.72rem; }
818
+ .btn-new-game { padding: 5px 10px; font-size: 0.78rem; }
819
+
820
+ /* upload box full width */
821
+ .upload-box { width: 95vw; }
822
+ .upload-title { font-size: 1.5rem; }
823
+
824
+ /* badge slightly smaller */
825
+ .badge { width: 18px; height: 18px; font-size: 8px; }
826
+
827
+ /* piece images */
828
+ .piece-img { width: calc(var(--sq-size) * 0.9); height: calc(var(--sq-size) * 0.9); }
829
+ .piece-uni { font-size: calc(var(--sq-size) * 0.75); }
830
+ }
831
+
832
+ @media (max-width: 400px) {
833
+ :root {
834
+ --board-size: 98vw;
835
+ --sq-size: calc(98vw / 8);
836
+ }
837
+
838
+ .rank-labels { width: 14px; }
839
+ .rank-label { font-size: 0.6rem; }
840
+ .file-label { font-size: 0.6rem; }
841
+ .eval-col { margin-left: 14px; }
842
+ .board-info,
843
+ .move-list-panel { margin-left: 14px; width: calc(98vw - 14px); }
844
+ }
845
+ </style>
846
+ </head>
847
+ <body>
848
+
849
+ <!-- ═══════════════════════════════════════════════════════════════
850
+ UPLOAD SCREEN
851
+ ════════════════════════════════════════════════════════════════ -->
852
+ <div id="upload-screen" class="screen active">
853
+ <header>
854
+ <div>
855
+ <div class="logo">♟ ANALYSIS</div>
856
+ <div class="logo-sub">Stockfish · Deep Engine</div>
857
+ </div>
858
+ </header>
859
+
860
+ <div style="flex:1; display:flex; align-items:center; justify-content:center; padding:40px 20px;">
861
+ <div class="upload-box">
862
+ <h1 class="upload-title">Analyse a Game</h1>
863
+ <p class="upload-sub">Paste PGN notation or drop a file below</p>
864
+
865
+ <div class="drop-zone" id="drop-zone">
866
+ <div class="drop-zone-label">
867
+ Drop a <span onclick="document.getElementById('file-input').click()">PGN file</span> here, or paste below
868
+ </div>
869
+ <textarea id="pgn-textarea"
870
+ placeholder='[Event "World Championship"]&#10;[White "Kasparov"]&#10;[Black "Karpov"]&#10;&#10;1. e4 e5 2. Nf3 Nc6 ...'></textarea>
871
+ </div>
872
+
873
+ <input type="file" id="file-input" accept=".pgn,.txt">
874
+
875
+ <div class="upload-row">
876
+ <div class="depth-row">
877
+ <select id="mode-select" onchange="updateModeLabel()"
878
+ style="background:var(--card2);border:1px solid var(--border);color:var(--text-dim);
879
+ border-radius:4px;padding:3px 7px;font-family:'Crimson Pro',serif;font-size:0.88rem;outline:none;cursor:pointer;">
880
+ <option value="depth">Depth</option>
881
+ <option value="time">Time/move</option>
882
+ </select>
883
+ <input type="range" id="depth-slider" min="6" max="24" value="14"
884
+ oninput="updateModeLabel()">
885
+ <span id="depth-val" style="min-width:44px;display:inline-block;">14</span>
886
+ </div>
887
+ <button class="btn-primary" onclick="startUpload()">Analyse →</button>
888
+ </div>
889
+ <div style="font-size:0.78rem;color:var(--text-muted);margin-top:8px;font-style:italic;" id="speed-hint">
890
+ Depth 14 ≈ fast · 18 ≈ balanced · 22+ ≈ slow
891
+ </div>
892
+
893
+ <div id="upload-error" style="color:var(--cl-blunder); font-size:0.85rem; margin-top:12px; display:none;"></div>
894
+ </div>
895
+ </div>
896
+ </div>
897
+
898
+ <!-- ═══════════════════════════════════════════════════════════════
899
+ LOADING SCREEN
900
+ ════════════════════════════════════════════════════════════════ -->
901
+ <div id="loading-screen" class="screen">
902
+ <header>
903
+ <div>
904
+ <div class="logo">♟ ANALYSIS</div>
905
+ <div class="logo-sub">Stockfish · Deep Engine</div>
906
+ </div>
907
+ </header>
908
+ <div style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:20px;">
909
+ <div class="loading-chess-icon">♞</div>
910
+ <div class="loading-title">ANALYSING GAME</div>
911
+ <div class="progress-wrap">
912
+ <div class="progress-bar" id="progress-bar"></div>
913
+ </div>
914
+ <div id="loading-move-label">Preparing engine…</div>
915
+ <div class="loading-info" id="loading-info">Running Stockfish at depth <span id="loading-depth">—</span></div>
916
+ </div>
917
+ </div>
918
+
919
+ <!-- ═══════════════════════════════════════════════════════════════
920
+ GAME SCREEN
921
+ ════════════════════════════════════════════════════════════════ -->
922
+ <div id="game-screen" class="screen">
923
+ <header>
924
+ <div>
925
+ <div class="logo">♟ ANALYSIS</div>
926
+ <div class="logo-sub">Stockfish · Deep Engine</div>
927
+ </div>
928
+ <div style="display:flex; align-items:center; gap:24px; flex:1; justify-content:center;">
929
+ <div class="game-players">
930
+ <span class="player-white" id="hdr-white">White</span>
931
+ <span class="vs-sep">vs</span>
932
+ <span class="player-black" id="hdr-black">Black</span>
933
+ </div>
934
+ <div class="game-meta" id="hdr-meta"></div>
935
+ </div>
936
+ <button class="btn-new-game" onclick="newGame()">← New Game</button>
937
+ </header>
938
+
939
+ <div class="game-body">
940
+
941
+ <!-- Eval bar -->
942
+ <div class="eval-col">
943
+ <div style="font-size:0.7rem; color:var(--text-muted); font-family:'Cinzel',serif; margin-bottom:4px; letter-spacing:0.06em;">W</div>
944
+ <div class="eval-bar-wrap">
945
+ <div class="eval-bar-white" id="eval-bar-white"></div>
946
+ </div>
947
+ <div style="font-size:0.7rem; color:var(--text-muted); font-family:'Cinzel',serif; margin-top:4px; letter-spacing:0.06em;">B</div>
948
+ </div>
949
+
950
+ <!-- Board + info -->
951
+ <div class="board-col">
952
+ <div class="board-outer">
953
+ <!-- Rank labels -->
954
+ <div class="rank-labels" id="rank-labels">
955
+ <div class="rank-label">8</div>
956
+ <div class="rank-label">7</div>
957
+ <div class="rank-label">6</div>
958
+ <div class="rank-label">5</div>
959
+ <div class="rank-label">4</div>
960
+ <div class="rank-label">3</div>
961
+ <div class="rank-label">2</div>
962
+ <div class="rank-label">1</div>
963
+ </div>
964
+ <!-- Board -->
965
+ <div id="board"></div>
966
+ </div>
967
+
968
+ <!-- File labels -->
969
+ <div class="file-labels">
970
+ <div class="file-label">a</div>
971
+ <div class="file-label">b</div>
972
+ <div class="file-label">c</div>
973
+ <div class="file-label">d</div>
974
+ <div class="file-label">e</div>
975
+ <div class="file-label">f</div>
976
+ <div class="file-label">g</div>
977
+ <div class="file-label">h</div>
978
+ </div>
979
+
980
+ <!-- Navigation -->
981
+ <div class="nav-row">
982
+ <button type="button" class="nav-btn" id="btn-start" onclick="navigateTo(0)" title="Start (Home)">⇤</button>
983
+ <button type="button" class="nav-btn" id="btn-prev" onclick="navigateTo(currentIdx - 1)" title="Previous (←)">‹</button>
984
+ <div class="nav-counter" id="nav-counter">— / —</div>
985
+ <button type="button" class="nav-btn" id="btn-next" onclick="navigateTo(currentIdx + 1)" title="Next (→)">›</button>
986
+ <button type="button" class="nav-btn" id="btn-end" onclick="navigateTo(gameData.positions.length - 1)" title="End (End)">⇥</button>
987
+ <button type="button" class="flip-btn" onclick="flipBoard()" title="Flip board">⇅ Flip</button>
988
+ </div>
989
+
990
+ <!-- Move info -->
991
+ <div class="board-info">
992
+ <div style="display:flex; align-items:center; justify-content:space-between;">
993
+ <div class="move-label" id="info-move-label">Starting Position</div>
994
+ <div class="eval-label" id="info-eval">0.00</div>
995
+ </div>
996
+ <div class="move-class-row" id="info-class-row" style="display:none;">
997
+ <div class="move-class-dot" id="info-class-dot"></div>
998
+ <div class="move-class-text" id="info-class-text"></div>
999
+ <div class="move-cp" id="info-cp"></div>
1000
+ </div>
1001
+ <div class="move-notes" id="info-notes"></div>
1002
+
1003
+ <!-- Continuations -->
1004
+ <div class="continuations" id="continuations" style="display:none;">
1005
+ <div class="cont-block" id="cont-player-block">
1006
+ <div class="cont-label">Continuation after this move</div>
1007
+ <div class="cont-moves" id="cont-player"></div>
1008
+ </div>
1009
+ <div class="cont-block" id="cont-best-block" style="display:none;">
1010
+ <div class="cont-label">Best move was <span id="cont-best-move-name" class="cont-best-move"></span></div>
1011
+ <div class="cont-label" style="margin-top:4px;">Continuation after best move</div>
1012
+ <div class="cont-moves" id="cont-best"></div>
1013
+ </div>
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+
1018
+ <!-- Move list panel -->
1019
+ <div class="move-list-panel">
1020
+ <div class="move-list-header">Move List</div>
1021
+ <div class="move-list-body" id="move-list"></div>
1022
+ <div class="eval-summary" id="eval-summary" style="display:none;">
1023
+ <div class="eval-summary-row">
1024
+ <div class="eval-summary-name">White</div>
1025
+ <div class="eval-summary-bar-wrap">
1026
+ <div class="eval-summary-bar" id="white-acc-bar" style="background:var(--cl-excellent);"></div>
1027
+ </div>
1028
+ </div>
1029
+ <div class="eval-summary-row">
1030
+ <div class="eval-summary-name">Black</div>
1031
+ <div class="eval-summary-bar-wrap">
1032
+ <div class="eval-summary-bar" id="black-acc-bar" style="background:var(--cl-excellent);"></div>
1033
+ </div>
1034
+ </div>
1035
+ <div class="eval-accuracy" id="accuracy-text"></div>
1036
+ </div>
1037
+ </div>
1038
+
1039
+ </div><!-- /game-body -->
1040
+ </div><!-- /game-screen -->
1041
+
1042
+ <!-- ═══════════════════════════════════════════════════════════════
1043
+ JAVASCRIPT
1044
+ ════════════════════════════════════════════════════════════════ -->
1045
+ <script>
1046
+ /* ── State ─────────────────────────────────────────────────── */
1047
+ let gameData = null;
1048
+ let currentIdx = 0;
1049
+ let flipped = false;
1050
+ let currentGameId = null;
1051
+ let progressEvt = null;
1052
+
1053
+ /* ── Classification config ─────────────────────────────────── */
1054
+ const CL = {
1055
+ BRILLIANT: { color: '#00b5ad', badge: '!!', label: '!! Brilliant' },
1056
+ GREAT: { color: '#1a4fc4', badge: '!', label: '! Great' },
1057
+ BEST: { color: '#7ec820', badge: '★', label: '★ Best' },
1058
+ EXCELLENT: { color: '#7ec820', badge: '👍', label: '👍 Excellent' },
1059
+ GOOD: { color: '#4db896', badge: '✓', label: '✓ Good' },
1060
+ BOOK: { color: '#a0754a', badge: '📖', label: '📖 Book' },
1061
+ INACCURACY: { color: '#d4b030', badge: '?!', label: '?! Inaccuracy' },
1062
+ MISTAKE: { color: '#d07020', badge: '?', label: '? Mistake' },
1063
+ BLUNDER: { color: '#c83030', badge: '??', label: '?? Blunder' },
1064
+ OMISSION: { color: '#e05050', badge: '✖', label: '✖ Missed Tactic' },
1065
+ };
1066
+
1067
+ /* ── Piece maps ────────────────────────────────────────────── */
1068
+ const PIECE_CODE = {
1069
+ K:'wK', Q:'wQ', R:'wR', B:'wB', N:'wN', P:'wP',
1070
+ k:'bK', q:'bQ', r:'bR', b:'bB', n:'bN', p:'bP',
1071
+ };
1072
+ const PIECE_UNI = {
1073
+ K:'♔', Q:'♕', R:'♖', B:'♗', N:'♘', P:'♙',
1074
+ k:'♚', q:'♛', r:'♜', b:'♝', n:'♞', p:'♟',
1075
+ };
1076
+ /* Track which piece images failed to load (use unicode instead) */
1077
+ const imgFailed = {};
1078
+
1079
+ /* ── Screens ───────────────────────────────────────────────── */
1080
+ function showScreen(id) {
1081
+ document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
1082
+ document.getElementById(id).classList.add('active');
1083
+ }
1084
+
1085
+ /* ── Upload ────────────────────────────────────────────────── */
1086
+ function updateModeLabel() {
1087
+ const mode = document.getElementById('mode-select').value;
1088
+ const val = +document.getElementById('depth-slider').value;
1089
+ const hint = document.getElementById('speed-hint');
1090
+ if (mode === 'depth') {
1091
+ document.getElementById('depth-val').textContent = val;
1092
+ hint.textContent = `Depth ${val} ≈ ${val<=10?'very fast':val<=14?'fast':val<=18?'balanced':val<=21?'slow':'very slow'}`;
1093
+ } else {
1094
+ // remap slider 6-24 → 100ms-3000ms
1095
+ const ms = Math.round(((val - 6) / 18) * 2900 + 100);
1096
+ document.getElementById('depth-val').textContent = ms + ' ms';
1097
+ hint.textContent = `${ms}ms per move ≈ ${ms<=300?'fast':ms<=800?'balanced':'thorough'}`;
1098
+ }
1099
+ }
1100
+ updateModeLabel();
1101
+
1102
+ function startUpload() {
1103
+ const pgn = document.getElementById('pgn-textarea').value.trim();
1104
+ const mode = document.getElementById('mode-select').value;
1105
+ const sliderVal = +document.getElementById('depth-slider').value;
1106
+ const errEl = document.getElementById('upload-error');
1107
+
1108
+ if (!pgn) { errEl.textContent = 'Please paste a PGN first.'; errEl.style.display='block'; return; }
1109
+ errEl.style.display = 'none';
1110
+
1111
+ const fd = new FormData();
1112
+ fd.append('pgn_text', pgn);
1113
+ if (mode === 'depth') {
1114
+ fd.append('depth', sliderVal);
1115
+ } else {
1116
+ const ms = Math.round(((sliderVal - 6) / 18) * 2900 + 100);
1117
+ fd.append('time_ms', ms);
1118
+ }
1119
+
1120
+ fetch('/upload', { method: 'POST', body: fd })
1121
+ .then(r => {
1122
+ const ct = r.headers.get('content-type') || '';
1123
+ if (!ct.includes('application/json')) {
1124
+ throw new Error('Server returned non-JSON (status ' + r.status + '). ' +
1125
+ 'If using localtunnel, open the tunnel URL directly in a new tab and click Continue first.');
1126
+ }
1127
+ return r.json();
1128
+ })
1129
+ .then(d => {
1130
+ if (d.error) { showError(d.error); return; }
1131
+ currentGameId = d.game_id;
1132
+ document.getElementById('loading-depth').textContent =
1133
+ mode === 'depth' ? 'depth ' + sliderVal : Math.round(((sliderVal-6)/18)*2900+100) + 'ms/move';
1134
+ showScreen('loading-screen');
1135
+ pollProgress(d.game_id);
1136
+ })
1137
+ .catch(e => showError('Upload failed: ' + e.message));
1138
+ }
1139
+
1140
+ function showError(msg) {
1141
+ const el = document.getElementById('upload-error');
1142
+ if (el) { el.textContent = msg; el.style.display = 'block'; }
1143
+ else { alert(msg); }
1144
+ showScreen('upload-screen');
1145
+ }
1146
+
1147
+ function pollProgress(gameId) {
1148
+ if (progressEvt) progressEvt.close();
1149
+ const bar = document.getElementById('progress-bar');
1150
+ const label = document.getElementById('loading-move-label');
1151
+
1152
+ progressEvt = new EventSource(`/progress/${gameId}`);
1153
+ progressEvt.onmessage = (e) => {
1154
+ const p = JSON.parse(e.data);
1155
+
1156
+ if (p.error) {
1157
+ progressEvt.close();
1158
+ showError('Analysis error: ' + p.error);
1159
+ return;
1160
+ }
1161
+
1162
+ const pct = p.total > 0 ? Math.round((p.done / p.total) * 100) : 0;
1163
+ bar.style.width = pct + '%';
1164
+ label.textContent = p.done > 0
1165
+ ? `Move ${p.done} of ${p.total} analysed`
1166
+ : 'Initialising engine…';
1167
+
1168
+ if (p.complete && !p.error) {
1169
+ progressEvt.close();
1170
+ loadGame(gameId);
1171
+ }
1172
+ };
1173
+ }
1174
+
1175
+ function loadGame(gameId) {
1176
+ fetch(`/game/${gameId}`)
1177
+ .then(r => r.json())
1178
+ .then(d => {
1179
+ if (d.error) { alert(d.error); showScreen('upload-screen'); return; }
1180
+ gameData = d;
1181
+ setupGameScreen();
1182
+ showScreen('game-screen');
1183
+ });
1184
+ }
1185
+
1186
+ /* ── Game screen setup ─────────────────────────────────────── */
1187
+ function setupGameScreen() {
1188
+ const h = gameData.headers;
1189
+ document.getElementById('hdr-white').textContent = h.White || 'White';
1190
+ document.getElementById('hdr-black').textContent = h.Black || 'Black';
1191
+ document.getElementById('hdr-meta').textContent = [h.Event, h.Date, h.Result].filter(Boolean).join(' · ');
1192
+
1193
+ buildMoveList();
1194
+ computeAccuracy();
1195
+ navigateTo(0);
1196
+ }
1197
+
1198
+ function newGame() {
1199
+ gameData = null;
1200
+ currentIdx = 0;
1201
+ flipped = false;
1202
+ document.getElementById('pgn-textarea').value = '';
1203
+ showScreen('upload-screen');
1204
+ }
1205
+
1206
+ /* ── FEN parser ────────────────────────────────────────────── */
1207
+ function parseFEN(fen) {
1208
+ const [placement] = fen.split(' ');
1209
+ const ranks = placement.split('/');
1210
+ const grid = []; // grid[rank][file], rank 0 = rank 8
1211
+ for (const row of ranks) {
1212
+ const r = [];
1213
+ for (const ch of row) {
1214
+ if (/\d/.test(ch)) { for (let i=0; i<+ch; i++) r.push(null); }
1215
+ else { r.push(ch); }
1216
+ }
1217
+ grid.push(r);
1218
+ }
1219
+ return grid;
1220
+ }
1221
+
1222
+ /* ── Board render ──────────────────────────────────────────── */
1223
+ function renderBoard(pos) {
1224
+ const grid = parseFEN(pos.fen);
1225
+ const fromSq = pos.from_sq;
1226
+ const toSq = pos.to_sq;
1227
+ const board = document.getElementById('board');
1228
+ board.innerHTML = '';
1229
+
1230
+ // Update rank/file labels for flipped
1231
+ const rankLabels = document.getElementById('rank-labels');
1232
+ rankLabels.innerHTML = '';
1233
+ for (let i=0; i<8; i++) {
1234
+ const d = document.createElement('div');
1235
+ d.className = 'rank-label';
1236
+ d.textContent = flipped ? (i + 1) : (8 - i);
1237
+ rankLabels.appendChild(d);
1238
+ }
1239
+
1240
+ const fileLabels = document.querySelector('.file-labels');
1241
+ fileLabels.innerHTML = '';
1242
+ for (let i=0; i<8; i++) {
1243
+ const d = document.createElement('div');
1244
+ d.className = 'file-label';
1245
+ d.textContent = flipped
1246
+ ? String.fromCharCode(104 - i) // h...a
1247
+ : String.fromCharCode(97 + i); // a...h
1248
+ fileLabels.appendChild(d);
1249
+ }
1250
+
1251
+ for (let displayRank=0; displayRank<8; displayRank++) {
1252
+ for (let displayFile=0; displayFile<8; displayFile++) {
1253
+ const gridRank = flipped ? (7 - displayRank) : displayRank;
1254
+ const gridFile = flipped ? (7 - displayFile) : displayFile;
1255
+
1256
+ const squareName = String.fromCharCode(97 + gridFile) + (8 - gridRank);
1257
+ const piece = grid[gridRank][gridFile];
1258
+ const isLight = (gridRank + gridFile) % 2 === 0;
1259
+
1260
+ const sq = document.createElement('div');
1261
+ sq.className = 'sq ' + (isLight ? 'light' : 'dark');
1262
+ sq.dataset.sq = squareName;
1263
+
1264
+ if (squareName === fromSq) sq.classList.add('hi-from');
1265
+ if (squareName === toSq) sq.classList.add('hi-to');
1266
+
1267
+ if (piece) {
1268
+ if (imgFailed[PIECE_CODE[piece]]) {
1269
+ // unicode fallback
1270
+ const span = document.createElement('span');
1271
+ span.className = 'piece-uni';
1272
+ span.textContent = PIECE_UNI[piece];
1273
+ sq.appendChild(span);
1274
+ } else {
1275
+ const img = document.createElement('img');
1276
+ img.className = 'piece-img';
1277
+ img.src = `https://lichess1.org/assets/piece/cburnett/${PIECE_CODE[piece]}.svg`;
1278
+ img.alt = piece;
1279
+ img.onerror = function() {
1280
+ imgFailed[PIECE_CODE[piece]] = true;
1281
+ const span = document.createElement('span');
1282
+ span.className = 'piece-uni';
1283
+ span.textContent = PIECE_UNI[piece];
1284
+ this.parentNode.replaceChild(span, this);
1285
+ };
1286
+ sq.appendChild(img);
1287
+ }
1288
+ }
1289
+
1290
+ // Classification badge on the destination square
1291
+ if (squareName === toSq && pos.classification && CL[pos.classification]) {
1292
+ const cl = CL[pos.classification];
1293
+ const bdg = document.createElement('div');
1294
+ bdg.className = `badge badge-${pos.classification}`;
1295
+ bdg.textContent = cl.badge;
1296
+ sq.appendChild(bdg);
1297
+ }
1298
+
1299
+ sq.addEventListener('click', () => { /* future: select piece */ });
1300
+ board.appendChild(sq);
1301
+ }
1302
+ }
1303
+ }
1304
+
1305
+ /* ── Eval bar ──────────────────────────────────────────────── */
1306
+ function updateEvalBar(evalWhite) {
1307
+ const pct = 50 + 50 * (2 / Math.PI) * Math.atan(evalWhite / 400);
1308
+ const bar = document.getElementById('eval-bar-white');
1309
+ const val = (flipped ? (100 - pct) : pct) + '%';
1310
+ bar.style.height = val;
1311
+ bar.style.width = val; // used by horizontal mobile bar
1312
+ }
1313
+
1314
+ /* ── Move info panel ───────────────────────────────────────── */
1315
+ function updateInfo(pos) {
1316
+ const evalDisp = pos.eval_display;
1317
+ document.getElementById('info-eval').textContent = evalDisp;
1318
+ document.getElementById('info-eval').className = 'eval-label';
1319
+
1320
+ const clRow = document.getElementById('info-class-row');
1321
+ const clDot = document.getElementById('info-class-dot');
1322
+ const clText = document.getElementById('info-class-text');
1323
+ const cpEl = document.getElementById('info-cp');
1324
+ const notes = document.getElementById('info-notes');
1325
+ const label = document.getElementById('info-move-label');
1326
+
1327
+ if (!pos.san) {
1328
+ label.textContent = 'Starting Position';
1329
+ clRow.style.display = 'none';
1330
+ notes.textContent = '';
1331
+ } else {
1332
+ const colLabel = pos.color === 'white' ? 'White' : 'Black';
1333
+ label.textContent = `${pos.move_number}${pos.color === 'white' ? '.' : '…'} ${pos.san} — ${colLabel}`;
1334
+
1335
+ clRow.style.display = 'flex';
1336
+ if (pos.classification && CL[pos.classification]) {
1337
+ const cl = CL[pos.classification];
1338
+ clDot.style.background = cl.color;
1339
+ clText.textContent = cl.label;
1340
+ clText.className = `move-class-text cl-${pos.classification}`;
1341
+ const eStr = pos.e_loss > 0.005
1342
+ ? `(−${pos.e_loss.toFixed(3)} EP)`
1343
+ : '';
1344
+ cpEl.textContent = eStr;
1345
+ notes.textContent = pos.notes && pos.notes.length ? pos.notes.join(' · ') : '';
1346
+ } else {
1347
+ clRow.style.display = 'none';
1348
+ }
1349
+ }
1350
+
1351
+ // ── Continuations ────────────────────────────────────────────
1352
+ const contSection = document.getElementById('continuations');
1353
+ const contPlayer = document.getElementById('cont-player');
1354
+ const contBest = document.getElementById('cont-best');
1355
+ const contBestBlock = document.getElementById('cont-best-block');
1356
+ const contBestName = document.getElementById('cont-best-move-name');
1357
+
1358
+ if (!pos.san) {
1359
+ contSection.style.display = 'none';
1360
+ } else {
1361
+ contSection.style.display = 'flex';
1362
+
1363
+ // Player continuation
1364
+ if (pos.player_pv && pos.player_pv.length) {
1365
+ const pMoves = pos.player_pv.map((m, i) => {
1366
+ const cls = i === 0 ? 'cont-player-move' : '';
1367
+ return cls ? `<span class="${cls}">${m}</span>` : m;
1368
+ });
1369
+ contPlayer.innerHTML = pMoves.join(' ');
1370
+ } else {
1371
+ contPlayer.textContent = '—';
1372
+ }
1373
+
1374
+ // Best move & continuation (only show when best != played move)
1375
+ if (pos.best_san && pos.best_san !== pos.san) {
1376
+ contBestBlock.style.display = 'block';
1377
+ contBestName.textContent = pos.best_san;
1378
+ if (pos.best_pv && pos.best_pv.length) {
1379
+ const bMoves = pos.best_pv.map((m, i) => {
1380
+ const cls = i === 0 ? 'cont-best-move' : '';
1381
+ return cls ? `<span class="${cls}">${m}</span>` : m;
1382
+ });
1383
+ contBest.innerHTML = bMoves.join(' ');
1384
+ } else {
1385
+ contBest.textContent = '—';
1386
+ }
1387
+ } else {
1388
+ contBestBlock.style.display = 'none';
1389
+ }
1390
+ }
1391
+ }
1392
+
1393
+ /* ── Navigate ──────────────────────────────────────────────── */
1394
+ function navigateTo(idx) {
1395
+ if (!gameData) return;
1396
+ idx = Math.max(0, Math.min(idx, gameData.positions.length - 1));
1397
+ currentIdx = idx;
1398
+ const pos = gameData.positions[idx];
1399
+
1400
+ renderBoard(pos);
1401
+ updateEvalBar(pos.eval_white);
1402
+ updateInfo(pos);
1403
+
1404
+ document.getElementById('btn-start').disabled = idx === 0;
1405
+ document.getElementById('btn-prev').disabled = idx === 0;
1406
+ document.getElementById('btn-next').disabled = idx === gameData.positions.length - 1;
1407
+ document.getElementById('btn-end').disabled = idx === gameData.positions.length - 1;
1408
+
1409
+ const total = gameData.positions.length - 1;
1410
+ document.getElementById('nav-counter').textContent = idx === 0
1411
+ ? `Start / ${total}`
1412
+ : `${idx} / ${total}`;
1413
+
1414
+ // Highlight move list
1415
+ document.querySelectorAll('.move-cell').forEach(c => c.classList.remove('current'));
1416
+ if (idx > 0) {
1417
+ const cell = document.querySelector(`[data-move-idx="${idx}"]`);
1418
+ if (cell) {
1419
+ cell.classList.add('current');
1420
+ // Scroll the move-list container, not the whole page
1421
+ const listBody = document.getElementById('move-list');
1422
+ const cellTop = cell.offsetTop;
1423
+ const cellBottom = cellTop + cell.offsetHeight;
1424
+ const bodyTop = listBody.scrollTop;
1425
+ const bodyBottom = bodyTop + listBody.clientHeight;
1426
+ if (cellTop < bodyTop) {
1427
+ listBody.scrollTop = cellTop - 8;
1428
+ } else if (cellBottom > bodyBottom) {
1429
+ listBody.scrollTop = cellBottom - listBody.clientHeight + 8;
1430
+ }
1431
+ }
1432
+ }
1433
+ }
1434
+
1435
+ function flipBoard() {
1436
+ flipped = !flipped;
1437
+ navigateTo(currentIdx);
1438
+ }
1439
+
1440
+ /* ── Keyboard ──────────────────────────────────────────────── */
1441
+ document.addEventListener('keydown', (e) => {
1442
+ if (!gameData) return;
1443
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); navigateTo(currentIdx - 1); }
1444
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); navigateTo(currentIdx + 1); }
1445
+ if (e.key === 'Home') { e.preventDefault(); navigateTo(0); }
1446
+ if (e.key === 'End') { e.preventDefault(); navigateTo(gameData.positions.length - 1); }
1447
+ });
1448
+
1449
+ /* ── Move list ─────────────────────────────────────────────── */
1450
+ function buildMoveList() {
1451
+ const list = document.getElementById('move-list');
1452
+ list.innerHTML = '';
1453
+ const positions = gameData.positions;
1454
+
1455
+ // positions[0] = start, positions[1] = white's first move, positions[2] = black's...
1456
+ let pairDiv = null;
1457
+ for (let i = 1; i < positions.length; i++) {
1458
+ const pos = positions[i];
1459
+ const isWhite = (i % 2 === 1);
1460
+
1461
+ if (isWhite) {
1462
+ pairDiv = document.createElement('div');
1463
+ pairDiv.className = 'move-pair';
1464
+
1465
+ const numEl = document.createElement('div');
1466
+ numEl.className = 'move-num';
1467
+ numEl.textContent = pos.move_number + '.';
1468
+ pairDiv.appendChild(numEl);
1469
+ list.appendChild(pairDiv);
1470
+ }
1471
+
1472
+ const cell = document.createElement('div');
1473
+ cell.className = 'move-cell';
1474
+ cell.dataset.moveIdx = i;
1475
+
1476
+ const cl = pos.classification && CL[pos.classification] ? CL[pos.classification] : null;
1477
+
1478
+ if (cl) {
1479
+ const dot = document.createElement('div');
1480
+ dot.className = 'cl-dot';
1481
+ dot.style.background = cl.color;
1482
+ cell.appendChild(dot);
1483
+ } else {
1484
+ // placeholder dot
1485
+ const dot = document.createElement('div');
1486
+ dot.className = 'cl-dot';
1487
+ dot.style.background = 'transparent';
1488
+ cell.appendChild(dot);
1489
+ }
1490
+
1491
+ const san = document.createElement('span');
1492
+ san.className = 'san-text';
1493
+ san.textContent = pos.san || '';
1494
+ cell.appendChild(san);
1495
+
1496
+ cell.addEventListener('click', () => navigateTo(i));
1497
+ if (pairDiv) pairDiv.appendChild(cell);
1498
+ }
1499
+ }
1500
+
1501
+ /* ── Accuracy summary ──────────────────────────────────────── */
1502
+ function computeAccuracy() {
1503
+ const positions = gameData.positions;
1504
+ const WHITE_CL = []; const BLACK_CL = [];
1505
+
1506
+ for (let i = 1; i < positions.length; i++) {
1507
+ const pos = positions[i];
1508
+ const arr = pos.color === 'white' ? WHITE_CL : BLACK_CL;
1509
+ arr.push(pos.classification);
1510
+ }
1511
+
1512
+ function score(arr) {
1513
+ const weights = {
1514
+ BRILLIANT: 100, GREAT: 97, BEST: 95, EXCELLENT: 88, GOOD: 78,
1515
+ BOOK: 90, INACCURACY: 50, MISTAKE: 20, BLUNDER: 0, OMISSION: 10,
1516
+ };
1517
+ if (!arr.length) return 0;
1518
+ return arr.reduce((s, c) => s + (weights[c] || 50), 0) / arr.length;
1519
+ }
1520
+
1521
+ const wScore = score(WHITE_CL);
1522
+ const bScore = score(BLACK_CL);
1523
+
1524
+ document.getElementById('white-acc-bar').style.width = wScore + '%';
1525
+ document.getElementById('black-acc-bar').style.width = bScore + '%';
1526
+ document.getElementById('accuracy-text').textContent =
1527
+ `White ${wScore.toFixed(0)}% · Black ${bScore.toFixed(0)}%`;
1528
+ document.getElementById('eval-summary').style.display = 'block';
1529
+ }
1530
+
1531
+ /* ── Drag-and-drop ─────────────────────────────────────────── */
1532
+ const dropZone = document.getElementById('drop-zone');
1533
+ dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
1534
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
1535
+ dropZone.addEventListener('drop', (e) => {
1536
+ e.preventDefault();
1537
+ dropZone.classList.remove('dragover');
1538
+ const file = e.dataTransfer.files[0];
1539
+ if (file) { const r = new FileReader(); r.onload = (ev) => { document.getElementById('pgn-textarea').value = ev.target.result; }; r.readAsText(file); }
1540
+ });
1541
+
1542
+ document.getElementById('file-input').addEventListener('change', (e) => {
1543
+ const file = e.target.files[0];
1544
+ if (file) { const r = new FileReader(); r.onload = (ev) => { document.getElementById('pgn-textarea').value = ev.target.result; }; r.readAsText(file); }
1545
+ });
1546
+
1547
+ /* ── Enter key on textarea ─────────────────────────────────── */
1548
+ // Don't submit on Enter (user is typing PGN)
1549
+ </script>
1550
+ </body>
1551
+ </html>