Fu01978 commited on
Commit
c588584
·
verified ·
1 Parent(s): d3998c0

Upload index (9).html

Browse files
Files changed (1) hide show
  1. templates/index (9).html +943 -0
templates/index (9).html ADDED
@@ -0,0 +1,943 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>Chess Analysis</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=Crimson+Pro:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <style>
11
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
+
13
+ :root {
14
+ --bg: #07070e;
15
+ --bg-card: #0f0f1d;
16
+ --bg-elevated: #16162a;
17
+ --bg-deepest: #050509;
18
+ --gold: #c9a84c;
19
+ --gold-light: #e8c97a;
20
+ --gold-dim: #7a6430;
21
+ --gold-glow: rgba(201,168,76,0.15);
22
+ --text: #ede8dc;
23
+ --text-sub: #9a9282;
24
+ --text-muted: #5a5650;
25
+ --border: #1e1e36;
26
+ --border-bright:#2e2e4e;
27
+ --sq: 66px;
28
+ }
29
+
30
+ html, body {
31
+ height: 100%;
32
+ font-family: 'Crimson Pro', Georgia, serif;
33
+ background: var(--bg);
34
+ color: var(--text);
35
+ overflow: hidden;
36
+ }
37
+
38
+ body::before {
39
+ content: '';
40
+ position: fixed; inset: 0; z-index: 0;
41
+ background:
42
+ radial-gradient(ellipse 80% 60% at 15% 50%, rgba(20,18,60,0.5) 0%, transparent 70%),
43
+ radial-gradient(ellipse 60% 80% at 85% 10%, rgba(10,25,60,0.4) 0%, transparent 60%);
44
+ pointer-events: none;
45
+ }
46
+
47
+ /* ── Screen system ───────────────────────────────────────────────────── */
48
+ .screen {
49
+ position: fixed; inset: 0; z-index: 1;
50
+ display: flex; flex-direction: column; align-items: center;
51
+ overflow-y: auto;
52
+ opacity: 0;
53
+ pointer-events: none;
54
+ transition: opacity 0.35s ease;
55
+ }
56
+ .screen.active {
57
+ opacity: 1;
58
+ pointer-events: all;
59
+ }
60
+
61
+ /* ── Shared ──────────────────────────────────────────────────────────── */
62
+ .mono { font-family: 'JetBrains Mono', 'Consolas', monospace; }
63
+
64
+ .btn {
65
+ display: inline-flex; align-items: center; gap: 8px;
66
+ border: none; cursor: pointer;
67
+ font-family: 'Crimson Pro', serif;
68
+ font-weight: 500;
69
+ letter-spacing: 0.05em;
70
+ transition: all 0.2s;
71
+ }
72
+ .btn:disabled { opacity: 0.3; cursor: default; }
73
+
74
+ .btn-gold {
75
+ background: linear-gradient(135deg, #b8922a 0%, #c9a84c 40%, #e8c97a 100%);
76
+ color: #07070e;
77
+ font-size: 17px;
78
+ padding: 13px 40px;
79
+ border-radius: 3px;
80
+ box-shadow: 0 4px 20px rgba(201,168,76,0.25), inset 0 1px 0 rgba(255,255,255,0.15);
81
+ }
82
+ .btn-gold:hover {
83
+ background: linear-gradient(135deg, #c9a84c 0%, #e8c97a 60%, #f5dca0 100%);
84
+ box-shadow: 0 6px 30px rgba(201,168,76,0.4), inset 0 1px 0 rgba(255,255,255,0.2);
85
+ transform: translateY(-1px);
86
+ }
87
+ .btn-ghost {
88
+ background: transparent;
89
+ color: var(--text-sub);
90
+ font-size: 14px;
91
+ padding: 7px 14px;
92
+ border: 1px solid var(--border-bright);
93
+ border-radius: 3px;
94
+ }
95
+ .btn-ghost:hover { color: var(--text); border-color: var(--gold-dim); background: rgba(201,168,76,0.05); }
96
+
97
+ /* ── Screen 1: Input ─────────────────────────────────────────────────── */
98
+ #s-input {
99
+ justify-content: center;
100
+ padding: 40px 20px;
101
+ }
102
+
103
+ .brand {
104
+ text-align: center;
105
+ margin-bottom: 36px;
106
+ }
107
+ .brand-name {
108
+ font-family: 'Playfair Display', serif;
109
+ font-size: 13px;
110
+ font-weight: 400;
111
+ letter-spacing: 0.35em;
112
+ text-transform: uppercase;
113
+ color: var(--gold);
114
+ }
115
+ .brand-line {
116
+ display: inline-block;
117
+ width: 40px; height: 1px;
118
+ background: var(--gold-dim);
119
+ vertical-align: middle;
120
+ margin: 0 12px;
121
+ opacity: 0.5;
122
+ }
123
+
124
+ .input-card {
125
+ background: var(--bg-card);
126
+ border: 1px solid var(--border);
127
+ border-radius: 4px;
128
+ padding: 48px 52px;
129
+ width: 100%; max-width: 640px;
130
+ box-shadow:
131
+ 0 40px 100px rgba(0,0,0,0.6),
132
+ 0 0 0 1px rgba(201,168,76,0.06),
133
+ inset 0 1px 0 rgba(255,255,255,0.03);
134
+ }
135
+
136
+ .input-card h1 {
137
+ font-family: 'Playfair Display', serif;
138
+ font-size: 32px; font-weight: 600;
139
+ color: var(--text);
140
+ margin-bottom: 6px;
141
+ }
142
+ .input-card .subtitle {
143
+ font-size: 16px; font-weight: 300;
144
+ color: var(--text-sub);
145
+ margin-bottom: 32px;
146
+ }
147
+
148
+ .field-label {
149
+ display: block;
150
+ font-size: 11px; font-weight: 500;
151
+ letter-spacing: 0.2em; text-transform: uppercase;
152
+ color: var(--gold-dim);
153
+ margin-bottom: 8px;
154
+ }
155
+
156
+ #pgn {
157
+ width: 100%; height: 200px;
158
+ background: var(--bg-deepest);
159
+ border: 1px solid var(--border);
160
+ border-radius: 3px;
161
+ color: var(--text);
162
+ font-family: 'JetBrains Mono', monospace;
163
+ font-size: 13px;
164
+ line-height: 1.6;
165
+ padding: 16px;
166
+ resize: vertical;
167
+ outline: none;
168
+ transition: border-color 0.2s;
169
+ margin-bottom: 28px;
170
+ }
171
+ #pgn::placeholder { color: var(--text-muted); }
172
+ #pgn:focus { border-color: var(--gold-dim); }
173
+
174
+ .depth-group { margin-bottom: 36px; }
175
+ .depth-row {
176
+ display: flex; align-items: center; gap: 16px;
177
+ }
178
+ #depth {
179
+ flex: 1;
180
+ -webkit-appearance: none;
181
+ height: 3px;
182
+ background: var(--border-bright);
183
+ border-radius: 2px;
184
+ outline: none;
185
+ }
186
+ #depth::-webkit-slider-thumb {
187
+ -webkit-appearance: none;
188
+ width: 16px; height: 16px;
189
+ border-radius: 50%;
190
+ background: var(--gold);
191
+ cursor: pointer;
192
+ box-shadow: 0 0 8px rgba(201,168,76,0.4);
193
+ }
194
+ #depth::-moz-range-thumb {
195
+ width: 16px; height: 16px;
196
+ border-radius: 50%;
197
+ background: var(--gold);
198
+ cursor: pointer; border: none;
199
+ }
200
+ #depth-val {
201
+ font-family: 'JetBrains Mono', monospace;
202
+ font-size: 20px; font-weight: 500;
203
+ color: var(--gold);
204
+ min-width: 28px; text-align: right;
205
+ }
206
+ .depth-hint {
207
+ display: block;
208
+ font-size: 13px; font-weight: 300;
209
+ color: var(--text-muted);
210
+ margin-top: 8px;
211
+ }
212
+
213
+ /* ── Screen 2: Loading ───────────────────────────────────────────────── */
214
+ #s-loading { justify-content: center; align-items: center; }
215
+
216
+ .loading-inner {
217
+ text-align: center;
218
+ width: 480px; max-width: 90vw;
219
+ }
220
+ .loading-inner h2 {
221
+ font-family: 'Playfair Display', serif;
222
+ font-size: 22px; font-weight: 400;
223
+ letter-spacing: 0.15em;
224
+ color: var(--text-sub);
225
+ margin-bottom: 36px;
226
+ text-transform: uppercase;
227
+ }
228
+
229
+ .progress-track {
230
+ width: 100%; height: 3px;
231
+ background: var(--border);
232
+ border-radius: 2px;
233
+ overflow: hidden;
234
+ margin-bottom: 20px;
235
+ }
236
+ #progress-bar {
237
+ height: 100%;
238
+ background: linear-gradient(90deg, var(--gold-dim), var(--gold), var(--gold-light));
239
+ border-radius: 2px;
240
+ width: 0%;
241
+ transition: width 0.45s cubic-bezier(0.4,0,0.2,1);
242
+ box-shadow: 0 0 12px rgba(201,168,76,0.5);
243
+ }
244
+
245
+ #progress-msg {
246
+ font-size: 14px; font-weight: 300;
247
+ color: var(--text-sub);
248
+ letter-spacing: 0.05em;
249
+ }
250
+ #progress-pct {
251
+ font-family: 'JetBrains Mono', monospace;
252
+ font-size: 12px;
253
+ color: var(--gold-dim);
254
+ margin-top: 10px;
255
+ }
256
+
257
+ /* ── Screen 3: Analysis ──────────────────────────────────────────────── */
258
+ #s-analysis {
259
+ align-items: center;
260
+ padding: 0 20px 40px;
261
+ }
262
+
263
+ .analysis-header {
264
+ display: flex; align-items: center; justify-content: space-between;
265
+ width: 100%; max-width: 620px;
266
+ padding: 20px 0 18px;
267
+ border-bottom: 1px solid var(--border);
268
+ margin-bottom: 24px;
269
+ gap: 12px;
270
+ }
271
+
272
+ .players {
273
+ text-align: center; flex: 1;
274
+ display: flex; align-items: center; justify-content: center; gap: 14px;
275
+ }
276
+ .player-name {
277
+ font-family: 'Playfair Display', serif;
278
+ font-size: 18px; font-weight: 500;
279
+ }
280
+ .white-player { color: #f0ede4; }
281
+ .black-player { color: #a09880; }
282
+ .vs {
283
+ font-size: 12px; font-weight: 300;
284
+ letter-spacing: 0.2em; text-transform: uppercase;
285
+ color: var(--text-muted);
286
+ }
287
+
288
+ /* ── Board ───────────────────────────────────────────────────────────── */
289
+ .board-area { position: relative; }
290
+
291
+ .board-with-coords {
292
+ display: flex; align-items: stretch; gap: 0;
293
+ }
294
+
295
+ .rank-labels {
296
+ display: flex; flex-direction: column;
297
+ justify-content: space-around;
298
+ padding-right: 8px;
299
+ padding-bottom: calc(var(--sq) * 0); /* align with board */
300
+ }
301
+ .coord {
302
+ font-family: 'JetBrains Mono', monospace;
303
+ font-size: 11px;
304
+ color: var(--text-muted);
305
+ width: var(--sq); height: var(--sq);
306
+ display: flex; align-items: center; justify-content: center;
307
+ user-select: none;
308
+ }
309
+ .rank-labels .coord { width: auto; }
310
+
311
+ .board-column { display: flex; flex-direction: column; }
312
+
313
+ #board {
314
+ display: grid;
315
+ grid-template-columns: repeat(8, var(--sq));
316
+ grid-template-rows: repeat(8, var(--sq));
317
+ box-shadow:
318
+ 0 0 0 2px var(--border),
319
+ 0 0 0 3px var(--border-bright),
320
+ 0 24px 80px rgba(0,0,0,0.8),
321
+ 0 4px 16px rgba(0,0,0,0.5);
322
+ }
323
+
324
+ .file-labels {
325
+ display: flex;
326
+ margin-top: 4px;
327
+ }
328
+ .file-labels .coord { height: auto; padding: 4px 0; }
329
+
330
+ .sq {
331
+ width: var(--sq); height: var(--sq);
332
+ position: relative;
333
+ display: flex; align-items: center; justify-content: center;
334
+ overflow: hidden;
335
+ }
336
+ .sq.light { background: #f0d9b5; }
337
+ .sq.dark { background: #b58863; }
338
+
339
+ .overlay {
340
+ position: absolute; inset: 0; z-index: 1;
341
+ pointer-events: none;
342
+ }
343
+ .from-ov { background: rgba(255, 220, 60, 0.35); }
344
+ .to-ov { opacity: 0.72; }
345
+
346
+ .cls-badge {
347
+ position: absolute; top: 2px; right: 2px; z-index: 4;
348
+ width: 20px; height: 20px;
349
+ border-radius: 50%;
350
+ display: flex; align-items: center; justify-content: center;
351
+ font-size: 10px; font-weight: 700;
352
+ color: #fff;
353
+ font-family: 'JetBrains Mono', monospace;
354
+ pointer-events: none;
355
+ box-shadow: 0 1px 4px rgba(0,0,0,0.6);
356
+ }
357
+
358
+ .piece {
359
+ position: relative; z-index: 2;
360
+ font-size: 42px; line-height: 1;
361
+ display: flex; align-items: center; justify-content: center;
362
+ width: 100%; height: 100%;
363
+ pointer-events: none; user-select: none;
364
+ }
365
+ .wp {
366
+ color: #fff;
367
+ text-shadow:
368
+ 0 0 2px #000, 0 0 3px #000,
369
+ 1px 1px 0 #000, -1px -1px 0 #000,
370
+ 1px -1px 0 #000, -1px 1px 0 #000;
371
+ }
372
+ .bp {
373
+ color: #1c1a18;
374
+ text-shadow:
375
+ 0 0 2px rgba(255,255,255,0.55),
376
+ 1px 1px 0 rgba(255,255,255,0.3),
377
+ -1px -1px 0 rgba(255,255,255,0.3);
378
+ }
379
+
380
+ /* ── Navigation ──────────────────────────────────────────────────────── */
381
+ .nav-row {
382
+ display: flex; align-items: center; gap: 20px;
383
+ margin-top: 20px;
384
+ }
385
+ .nav-btn {
386
+ background: var(--bg-card);
387
+ border: 1px solid var(--border-bright);
388
+ color: var(--text-sub);
389
+ width: 40px; height: 40px;
390
+ border-radius: 3px;
391
+ font-size: 16px; cursor: pointer;
392
+ display: flex; align-items: center; justify-content: center;
393
+ transition: all 0.15s;
394
+ }
395
+ .nav-btn:hover:not(:disabled) { border-color: var(--gold-dim); color: var(--text); background: var(--bg-elevated); }
396
+ .nav-btn:disabled { opacity: 0.25; cursor: default; }
397
+
398
+ #move-indicator {
399
+ font-family: 'JetBrains Mono', monospace;
400
+ font-size: 13px; font-weight: 500;
401
+ color: var(--text-sub);
402
+ min-width: 120px; text-align: center;
403
+ }
404
+
405
+ /* ── Move timeline ───────────────────────────────────────────────────── */
406
+ #move-timeline {
407
+ display: flex; gap: 3px;
408
+ margin-top: 14px;
409
+ max-width: calc(var(--sq) * 8 + 28px);
410
+ overflow-x: auto;
411
+ padding-bottom: 4px;
412
+ scrollbar-width: thin;
413
+ scrollbar-color: var(--border-bright) transparent;
414
+ }
415
+ .tl-dot {
416
+ flex-shrink: 0;
417
+ width: 10px; height: 10px;
418
+ border-radius: 50%;
419
+ cursor: pointer;
420
+ transition: transform 0.15s, opacity 0.15s;
421
+ opacity: 0.65;
422
+ }
423
+ .tl-dot:hover { opacity: 1; transform: scale(1.3); }
424
+ .tl-dot.tl-active {
425
+ opacity: 1; transform: scale(1.25);
426
+ box-shadow: 0 0 6px currentColor;
427
+ }
428
+
429
+ /* ── Info box ────────────────────────────────────────────────────────── */
430
+ #info-box {
431
+ margin-top: 18px;
432
+ width: 100%;
433
+ max-width: calc(var(--sq) * 8 + 28px);
434
+ background: var(--bg-card);
435
+ border: 1px solid var(--border);
436
+ border-radius: 4px;
437
+ overflow: hidden;
438
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
439
+ }
440
+
441
+ .info-start {
442
+ padding: 28px 32px;
443
+ color: var(--text-muted);
444
+ font-size: 15px; font-weight: 300;
445
+ font-style: italic;
446
+ }
447
+
448
+ .info-header {
449
+ padding: 20px 24px 18px;
450
+ border-bottom: 1px solid var(--border);
451
+ display: grid;
452
+ grid-template-columns: auto 1fr auto;
453
+ align-items: center;
454
+ gap: 16px;
455
+ }
456
+
457
+ .cls-pill {
458
+ display: inline-flex; align-items: center; gap: 7px;
459
+ padding: 5px 12px 5px 9px;
460
+ border-radius: 99px;
461
+ border: 1px solid;
462
+ white-space: nowrap;
463
+ }
464
+ .cls-icon {
465
+ font-size: 12px;
466
+ font-family: 'JetBrains Mono', monospace;
467
+ }
468
+ .cls-label {
469
+ font-size: 11px; font-weight: 600;
470
+ letter-spacing: 0.15em;
471
+ font-family: 'Crimson Pro', serif;
472
+ }
473
+
474
+ .move-name {
475
+ font-family: 'Playfair Display', serif;
476
+ font-size: 20px; font-weight: 600;
477
+ color: var(--text);
478
+ }
479
+
480
+ .ep-badge {
481
+ font-family: 'JetBrains Mono', monospace;
482
+ font-size: 11px;
483
+ color: var(--text-muted);
484
+ white-space: nowrap;
485
+ }
486
+
487
+ .info-comment {
488
+ padding: 16px 24px;
489
+ font-size: 15px; font-weight: 300;
490
+ font-style: italic;
491
+ color: var(--text-sub);
492
+ border-bottom: 1px solid var(--border);
493
+ line-height: 1.5;
494
+ }
495
+
496
+ .info-details { padding: 4px 0 8px; }
497
+
498
+ .info-row {
499
+ display: grid;
500
+ grid-template-columns: 120px 1fr;
501
+ align-items: baseline;
502
+ gap: 12px;
503
+ padding: 10px 24px;
504
+ }
505
+ .info-row + .info-row { border-top: 1px solid var(--border); }
506
+
507
+ .info-label {
508
+ font-size: 11px; font-weight: 500;
509
+ letter-spacing: 0.18em; text-transform: uppercase;
510
+ color: var(--text-muted);
511
+ }
512
+ .info-val {
513
+ font-size: 15px;
514
+ color: var(--text);
515
+ }
516
+ .info-val.mono {
517
+ font-family: 'JetBrains Mono', monospace;
518
+ font-size: 13px;
519
+ }
520
+ .cont-text {
521
+ color: var(--text-sub);
522
+ line-height: 1.7;
523
+ word-break: break-word;
524
+ }
525
+
526
+ .best-row.found .info-val {
527
+ color: #6aa84f;
528
+ font-weight: 500;
529
+ }
530
+ </style>
531
+ </head>
532
+ <body>
533
+
534
+ <!-- ═══════════════════════════════════════════════════════════════════════
535
+ SCREEN 1 — PGN Input
536
+ ══════════════════════════════════════════════════════════════════════════ -->
537
+ <div id="s-input" class="screen active">
538
+ <div style="margin-top: auto; margin-bottom: auto; width: 100%; max-width: 640px; padding: 40px 20px;">
539
+ <div class="brand">
540
+ <span class="brand-line"></span>
541
+ <span class="brand-name">Chess Analysis</span>
542
+ <span class="brand-line"></span>
543
+ </div>
544
+
545
+ <div class="input-card">
546
+ <h1>Game Analysis</h1>
547
+ <p class="subtitle">Paste your PGN below and set analysis depth</p>
548
+
549
+ <label class="field-label" for="pgn">PGN Notation</label>
550
+ <textarea id="pgn" placeholder="[Event &quot;Chess Match&quot;]&#10;[White &quot;Player One&quot;]&#10;[Black &quot;Player Two&quot;]&#10;&#10;1. e4 e5 2. Nf3 Nc6 3. Bb5 ..."></textarea>
551
+
552
+ <div class="depth-group">
553
+ <label class="field-label" for="depth">Analysis Depth</label>
554
+ <div class="depth-row">
555
+ <input type="range" id="depth" min="6" max="22" value="12">
556
+ <span id="depth-val">12</span>
557
+ </div>
558
+ <span class="depth-hint">Deeper = stronger analysis, longer wait</span>
559
+ </div>
560
+
561
+ <div style="text-align: center;">
562
+ <button id="analyze-btn" class="btn btn-gold">Analyze Game</button>
563
+ </div>
564
+ </div>
565
+ </div>
566
+ </div>
567
+
568
+
569
+ <!-- ═══════════════════════════════════════════════════════════════════════
570
+ SCREEN 2 — Loading
571
+ ══════════════════════════════════════════════════════════════════════════ -->
572
+ <div id="s-loading" class="screen">
573
+ <div class="loading-inner" style="margin: auto;">
574
+ <h2>Analyzing</h2>
575
+ <div class="progress-track">
576
+ <div id="progress-bar"></div>
577
+ </div>
578
+ <p id="progress-msg">Initializing engine…</p>
579
+ <p id="progress-pct">0%</p>
580
+ </div>
581
+ </div>
582
+
583
+
584
+ <!-- ═══════════════════════════════════════════════════════════════════════
585
+ SCREEN 3 — Analysis
586
+ ══════════════════════════════════════════════════════════════════════════ -->
587
+ <div id="s-analysis" class="screen">
588
+
589
+ <div class="analysis-header">
590
+ <button id="back-btn" class="btn btn-ghost">← New Analysis</button>
591
+ <div class="players">
592
+ <span id="white-name" class="player-name white-player">White</span>
593
+ <span class="vs">vs</span>
594
+ <span id="black-name" class="player-name black-player">Black</span>
595
+ </div>
596
+ <button id="flip-btn" class="btn btn-ghost">⇅ Flip</button>
597
+ </div>
598
+
599
+ <div class="board-area">
600
+ <div class="board-with-coords">
601
+ <div class="rank-labels" id="rank-labels"></div>
602
+ <div class="board-column">
603
+ <div id="board"></div>
604
+ <div class="file-labels" id="file-labels"></div>
605
+ </div>
606
+ </div>
607
+ </div>
608
+
609
+ <div class="nav-row">
610
+ <button id="prev-btn" class="nav-btn" title="Previous move (←)">←</button>
611
+ <span id="move-indicator">Start</span>
612
+ <button id="next-btn" class="nav-btn" title="Next move (→)">→</button>
613
+ </div>
614
+
615
+ <div id="move-timeline"></div>
616
+
617
+ <div id="info-box">
618
+ <div class="info-start"><p>Press → to begin stepping through the game.</p></div>
619
+ </div>
620
+
621
+ </div>
622
+
623
+ <script>
624
+ // ── Constants ──────────────────────────────────────────────────────────
625
+ const PIECE_MAP = {
626
+ 'K':'♔','Q':'♕','R':'♖','B':'♗','N':'♘','P':'♙',
627
+ 'k':'♚','q':'♛','r':'♜','b':'♝','n':'♞','p':'♟'
628
+ };
629
+
630
+ const CLS_META = {
631
+ Brilliant: { color:'#1bada8', icon:'✦', label:'Brilliant' },
632
+ Great: { color:'#4c82e8', icon:'‼', label:'Great' },
633
+ Miss: { color:'#b52020', icon:'✕', label:'Miss' },
634
+ Best: { color:'#5a9e44', icon:'✓', label:'Best' },
635
+ Excellent: { color:'#82b83c', icon:'!', label:'Excellent' },
636
+ Good: { color:'#4891b8', icon:'⬤', label:'Good' },
637
+ Inaccuracy: { color:'#d4a820', icon:'△', label:'Inaccuracy' },
638
+ Mistake: { color:'#d96830', icon:'?', label:'Mistake' },
639
+ Blunder: { color:'#b82020', icon:'??', label:'Blunder' },
640
+ };
641
+
642
+ // ── State ──────────────────────────────────────────────────────────────
643
+ let gameData = null;
644
+ let curIdx = -1; // -1 = start, 0..n-1 = after that move
645
+ let flipped = false;
646
+
647
+ // ── Screen switching ───────────────────────────────────────────────────
648
+ function showScreen(id) {
649
+ document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
650
+ document.getElementById(id).classList.add('active');
651
+ }
652
+
653
+ // ── FEN parser ─────────────────────────────────────────────────────────
654
+ function parseFEN(fen) {
655
+ const rows = fen.split(' ')[0].split('/');
656
+ const board = {};
657
+ for (let r = 0; r < 8; r++) {
658
+ const rank = 8 - r;
659
+ let file = 0;
660
+ for (const ch of rows[r]) {
661
+ if (ch >= '1' && ch <= '8') { file += +ch; }
662
+ else { board['abcdefgh'[file] + rank] = ch; file++; }
663
+ }
664
+ }
665
+ return board;
666
+ }
667
+
668
+ // ── Board renderer ─────────────────────────────────────────────────────
669
+ function renderBoard(fen, fromSq, toSq, cls) {
670
+ const pieces = parseFEN(fen);
671
+ const boardEl = document.getElementById('board');
672
+ boardEl.innerHTML = '';
673
+
674
+ const rankOrder = flipped ? [1,2,3,4,5,6,7,8] : [8,7,6,5,4,3,2,1];
675
+ const fileOrder = flipped
676
+ ? ['h','g','f','e','d','c','b','a']
677
+ : ['a','b','c','d','e','f','g','h'];
678
+
679
+ const meta = cls ? (CLS_META[cls] || null) : null;
680
+
681
+ for (const rank of rankOrder) {
682
+ for (let fi = 0; fi < 8; fi++) {
683
+ const file = fileOrder[fi];
684
+ const sq = file + rank;
685
+ const isLight = (fi + rank) % 2 === 0;
686
+
687
+ const div = document.createElement('div');
688
+ div.className = `sq ${isLight ? 'light' : 'dark'}`;
689
+
690
+ // From-square highlight
691
+ if (sq === fromSq) {
692
+ const ov = document.createElement('div');
693
+ ov.className = 'overlay from-ov';
694
+ div.appendChild(ov);
695
+ }
696
+
697
+ // To-square highlight + badge
698
+ if (sq === toSq && meta) {
699
+ const ov = document.createElement('div');
700
+ ov.className = 'overlay to-ov';
701
+ ov.style.background = meta.color;
702
+ div.appendChild(ov);
703
+
704
+ const badge = document.createElement('div');
705
+ badge.className = 'cls-badge';
706
+ badge.textContent = meta.icon;
707
+ badge.style.background = meta.color;
708
+ div.appendChild(badge);
709
+ }
710
+
711
+ // Piece
712
+ const piece = pieces[sq];
713
+ if (piece) {
714
+ const span = document.createElement('span');
715
+ span.className = `piece ${piece === piece.toUpperCase() ? 'wp' : 'bp'}`;
716
+ span.textContent = PIECE_MAP[piece] || piece;
717
+ div.appendChild(span);
718
+ }
719
+
720
+ boardEl.appendChild(div);
721
+ }
722
+ }
723
+
724
+ updateCoords(rankOrder, fileOrder);
725
+ }
726
+
727
+ function updateCoords(rankOrder, fileOrder) {
728
+ const rl = document.getElementById('rank-labels');
729
+ rl.innerHTML = rankOrder.map(r =>
730
+ `<div class="coord" style="width:auto; padding-right:0">${r}</div>`
731
+ ).join('');
732
+
733
+ const fl = document.getElementById('file-labels');
734
+ fl.innerHTML = fileOrder.map(f =>
735
+ `<div class="coord" style="height:auto">${f}</div>`
736
+ ).join('');
737
+ }
738
+
739
+ // ── Timeline ───────────────────────────────────────────────────────────
740
+ function buildTimeline() {
741
+ const tl = document.getElementById('move-timeline');
742
+ tl.innerHTML = '';
743
+ gameData.moves.forEach((mv, i) => {
744
+ const dot = document.createElement('div');
745
+ dot.className = 'tl-dot';
746
+ dot.style.background = (CLS_META[mv.classification] || {color:'#555'}).color;
747
+ dot.title = `${mv.move_number}${mv.color==='white'?'.':'...'} ${mv.san} — ${mv.classification}`;
748
+ dot.addEventListener('click', () => navigateTo(i));
749
+ tl.appendChild(dot);
750
+ });
751
+ }
752
+
753
+ function refreshTimeline() {
754
+ const dots = document.querySelectorAll('.tl-dot');
755
+ dots.forEach((d, i) => {
756
+ d.classList.toggle('tl-active', i === curIdx);
757
+ });
758
+ // Scroll active dot into view
759
+ if (curIdx >= 0 && dots[curIdx]) {
760
+ dots[curIdx].scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: 'nearest' });
761
+ }
762
+ }
763
+
764
+ // ── Info box ───────────────────────────────────────────────────────────
765
+ function renderStartInfo() {
766
+ document.getElementById('info-box').innerHTML = `
767
+ <div class="info-start">
768
+ <p>Press <strong>→</strong> or the right arrow key to step through the game.</p>
769
+ </div>`;
770
+ }
771
+
772
+ function renderMoveInfo(mv) {
773
+ const meta = CLS_META[mv.classification] || { color:'#888', icon:'?', label: mv.classification };
774
+ const ep = mv.ep_loss > 0.001
775
+ ? `−${(mv.ep_loss * 100).toFixed(1)}% EP`
776
+ : 'No EP lost';
777
+
778
+ const moveLabel = mv.color === 'white'
779
+ ? `${mv.move_number}. ${mv.san}`
780
+ : `${mv.move_number}… ${mv.san}`;
781
+
782
+ const bestHtml = mv.best_move_san
783
+ ? `<div class="info-row best-row">
784
+ <span class="info-label">Best move</span>
785
+ <span class="info-val mono">${mv.color === 'white' ? mv.move_number + '.' : mv.move_number + '…'} ${mv.best_move_san}</span>
786
+ </div>`
787
+ : `<div class="info-row best-row found">
788
+ <span class="info-label">Best move</span>
789
+ <span class="info-val found">✓ You found the best move</span>
790
+ </div>`;
791
+
792
+ const contHtml = mv.continuation_fmt
793
+ ? `<div class="info-row cont-row">
794
+ <span class="info-label">Continuation</span>
795
+ <span class="info-val mono cont-text">${mv.continuation_fmt}</span>
796
+ </div>`
797
+ : '';
798
+
799
+ document.getElementById('info-box').innerHTML = `
800
+ <div class="info-header">
801
+ <div class="cls-pill" style="background:${meta.color}18; border-color:${meta.color}55; color:${meta.color}">
802
+ <span class="cls-icon">${meta.icon}</span>
803
+ <span class="cls-label">${meta.label.toUpperCase()}</span>
804
+ </div>
805
+ <div class="move-name">${moveLabel}</div>
806
+ <div class="ep-badge">${ep}</div>
807
+ </div>
808
+ <div class="info-comment">"${mv.comment}"</div>
809
+ <div class="info-details">
810
+ ${bestHtml}
811
+ ${contHtml}
812
+ </div>`;
813
+ }
814
+
815
+ // ── Navigation ─────────────────────────────────────────────────────────
816
+ function navigateTo(idx) {
817
+ if (!gameData) return;
818
+ const max = gameData.moves.length - 1;
819
+ curIdx = Math.max(-1, Math.min(idx, max));
820
+
821
+ document.getElementById('prev-btn').disabled = curIdx === -1;
822
+ document.getElementById('next-btn').disabled = curIdx === max;
823
+
824
+ if (curIdx === -1) {
825
+ renderBoard(gameData.initial_fen, null, null, null);
826
+ document.getElementById('move-indicator').textContent = 'Start';
827
+ renderStartInfo();
828
+ } else {
829
+ const mv = gameData.moves[curIdx];
830
+ renderBoard(mv.fen_after, mv.from_square, mv.to_square, mv.classification);
831
+ document.getElementById('move-indicator').textContent =
832
+ `Move ${curIdx + 1} / ${gameData.moves.length}`;
833
+ renderMoveInfo(mv);
834
+ }
835
+
836
+ refreshTimeline();
837
+ }
838
+
839
+ // ── Load completed analysis ────────────────────────────────────────────
840
+ function loadAnalysis(data) {
841
+ gameData = data;
842
+ curIdx = -1;
843
+ flipped = false;
844
+
845
+ document.getElementById('white-name').textContent = data.white;
846
+ document.getElementById('black-name').textContent = data.black;
847
+
848
+ buildTimeline();
849
+ navigateTo(-1);
850
+ }
851
+
852
+ // ── Progress bar ───────────────────────────────────────────────────────
853
+ function setProgress(pct, msg) {
854
+ document.getElementById('progress-bar').style.width = `${Math.round(pct * 100)}%`;
855
+ document.getElementById('progress-msg').textContent = msg;
856
+ document.getElementById('progress-pct').textContent = `${Math.round(pct * 100)}%`;
857
+ }
858
+
859
+ // ── Submit analysis ────────────────────────────────────────────────────
860
+ async function submitAnalysis() {
861
+ const pgn = document.getElementById('pgn').value.trim();
862
+ const depth = parseInt(document.getElementById('depth').value, 10);
863
+
864
+ if (!pgn) { alert('Please paste a PGN to analyze.'); return; }
865
+
866
+ showScreen('s-loading');
867
+ setProgress(0, 'Connecting to analysis engine…');
868
+
869
+ let jobId;
870
+ try {
871
+ const res = await fetch('/api/analyze', {
872
+ method: 'POST',
873
+ headers: { 'Content-Type': 'application/json' },
874
+ body: JSON.stringify({ pgn, depth })
875
+ });
876
+ const d = await res.json();
877
+ if (d.error) throw new Error(d.error);
878
+ jobId = d.job_id;
879
+ } catch (err) {
880
+ alert('Error: ' + err.message);
881
+ showScreen('s-input');
882
+ return;
883
+ }
884
+
885
+ const evtSrc = new EventSource(`/api/stream/${jobId}`);
886
+
887
+ evtSrc.onmessage = (e) => {
888
+ const data = JSON.parse(e.data);
889
+ if (data.type === 'progress') {
890
+ setProgress(data.progress, data.message);
891
+ } else if (data.type === 'complete') {
892
+ evtSrc.close();
893
+ loadAnalysis(data.data);
894
+ showScreen('s-analysis');
895
+ } else if (data.type === 'error') {
896
+ evtSrc.close();
897
+ alert('Analysis error: ' + data.message);
898
+ showScreen('s-input');
899
+ }
900
+ };
901
+
902
+ evtSrc.onerror = () => {
903
+ evtSrc.close();
904
+ alert('Connection lost. Please try again.');
905
+ showScreen('s-input');
906
+ };
907
+ }
908
+
909
+ // ── Wire up controls ───────────────────────────────────────────────────
910
+ document.getElementById('depth').addEventListener('input', e => {
911
+ document.getElementById('depth-val').textContent = e.target.value;
912
+ });
913
+
914
+ document.getElementById('analyze-btn').addEventListener('click', submitAnalysis);
915
+
916
+ document.getElementById('back-btn').addEventListener('click', () => {
917
+ showScreen('s-input');
918
+ });
919
+
920
+ document.getElementById('prev-btn').addEventListener('click', () => navigateTo(curIdx - 1));
921
+ document.getElementById('next-btn').addEventListener('click', () => navigateTo(curIdx + 1));
922
+
923
+ document.getElementById('flip-btn').addEventListener('click', () => {
924
+ flipped = !flipped;
925
+ if (!gameData) return;
926
+ if (curIdx === -1) {
927
+ renderBoard(gameData.initial_fen, null, null, null);
928
+ } else {
929
+ const mv = gameData.moves[curIdx];
930
+ renderBoard(mv.fen_after, mv.from_square, mv.to_square, mv.classification);
931
+ }
932
+ });
933
+
934
+ document.addEventListener('keydown', (e) => {
935
+ if (!gameData) return;
936
+ const active = document.querySelector('.screen.active');
937
+ if (!active || active.id !== 's-analysis') return;
938
+ if (e.key === 'ArrowRight') { e.preventDefault(); navigateTo(curIdx + 1); }
939
+ if (e.key === 'ArrowLeft') { e.preventDefault(); navigateTo(curIdx - 1); }
940
+ });
941
+ </script>
942
+ </body>
943
+ </html>