Fu01978 commited on
Commit
ac84159
·
verified ·
1 Parent(s): 209c391

Upload index (16).html

Browse files
Files changed (1) hide show
  1. templates/index (16).html +1360 -0
templates/index (16).html ADDED
@@ -0,0 +1,1360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ /* eval-bar 12px + gap 6px + rank-labels 20px + gap 8px + body-padding 20px = 66px */
28
+ --sq: min(66px, calc((100vw - 80px) / 8));
29
+ }
30
+
31
+ html, body {
32
+ height: 100%;
33
+ font-family: 'Crimson Pro', Georgia, serif;
34
+ background: var(--bg);
35
+ color: var(--text);
36
+ overflow: hidden;
37
+ }
38
+
39
+ body::before {
40
+ content: '';
41
+ position: fixed; inset: 0; z-index: 0;
42
+ background:
43
+ radial-gradient(ellipse 80% 60% at 15% 50%, rgba(20,18,60,0.5) 0%, transparent 70%),
44
+ radial-gradient(ellipse 60% 80% at 85% 10%, rgba(10,25,60,0.4) 0%, transparent 60%);
45
+ pointer-events: none;
46
+ }
47
+
48
+ /* ── Screen system ───────────────────────────────────────────────────── */
49
+ .screen {
50
+ position: fixed; inset: 0; z-index: 1;
51
+ display: flex; flex-direction: column; align-items: center;
52
+ overflow-y: auto;
53
+ -webkit-overflow-scrolling: touch;
54
+ opacity: 0;
55
+ pointer-events: none;
56
+ transition: opacity 0.35s ease;
57
+ }
58
+ .screen.active {
59
+ opacity: 1;
60
+ pointer-events: all;
61
+ }
62
+
63
+ /* ── Shared ──────────────────────────────────────────────────────────── */
64
+ .mono { font-family: 'JetBrains Mono', 'Consolas', monospace; }
65
+
66
+ .btn {
67
+ display: inline-flex; align-items: center; gap: 8px;
68
+ border: none; cursor: pointer;
69
+ font-family: 'Crimson Pro', serif;
70
+ font-weight: 500;
71
+ letter-spacing: 0.05em;
72
+ transition: all 0.2s;
73
+ }
74
+ .btn:disabled { opacity: 0.3; cursor: default; }
75
+
76
+ .btn-gold {
77
+ background: linear-gradient(135deg, #b8922a 0%, #c9a84c 40%, #e8c97a 100%);
78
+ color: #07070e;
79
+ font-size: 17px;
80
+ padding: 13px 40px;
81
+ border-radius: 3px;
82
+ box-shadow: 0 4px 20px rgba(201,168,76,0.25), inset 0 1px 0 rgba(255,255,255,0.15);
83
+ }
84
+ .btn-gold:hover {
85
+ background: linear-gradient(135deg, #c9a84c 0%, #e8c97a 60%, #f5dca0 100%);
86
+ box-shadow: 0 6px 30px rgba(201,168,76,0.4), inset 0 1px 0 rgba(255,255,255,0.2);
87
+ transform: translateY(-1px);
88
+ }
89
+ .btn-ghost {
90
+ background: transparent;
91
+ color: var(--text-sub);
92
+ font-size: 14px;
93
+ padding: 7px 14px;
94
+ border: 1px solid var(--border-bright);
95
+ border-radius: 3px;
96
+ }
97
+ .btn-ghost:hover { color: var(--text); border-color: var(--gold-dim); background: rgba(201,168,76,0.05); }
98
+
99
+ /* ── Screen 1: Input ─────────────────────────────────────────────────── */
100
+ #s-input {
101
+ justify-content: center;
102
+ padding: 40px 20px;
103
+ }
104
+
105
+ .brand {
106
+ text-align: center;
107
+ margin-bottom: 36px;
108
+ }
109
+ .brand-name {
110
+ font-family: 'Playfair Display', serif;
111
+ font-size: 13px;
112
+ font-weight: 400;
113
+ letter-spacing: 0.35em;
114
+ text-transform: uppercase;
115
+ color: var(--gold);
116
+ }
117
+ .brand-line {
118
+ display: inline-block;
119
+ width: 40px; height: 1px;
120
+ background: var(--gold-dim);
121
+ vertical-align: middle;
122
+ margin: 0 12px;
123
+ opacity: 0.5;
124
+ }
125
+
126
+ .input-card {
127
+ background: var(--bg-card);
128
+ border: 1px solid var(--border);
129
+ border-radius: 4px;
130
+ padding: 48px 52px;
131
+ width: 100%; max-width: 640px;
132
+ box-shadow:
133
+ 0 40px 100px rgba(0,0,0,0.6),
134
+ 0 0 0 1px rgba(201,168,76,0.06),
135
+ inset 0 1px 0 rgba(255,255,255,0.03);
136
+ }
137
+
138
+ @media (max-width: 520px) {
139
+ .input-card { padding: 28px 22px; }
140
+ .input-card h1 { font-size: 24px; }
141
+ #pgn { height: 160px; font-size: 12px; }
142
+ }
143
+
144
+ .input-card h1 {
145
+ font-family: 'Playfair Display', serif;
146
+ font-size: 32px; font-weight: 600;
147
+ color: var(--text);
148
+ margin-bottom: 6px;
149
+ }
150
+ .input-card .subtitle {
151
+ font-size: 16px; font-weight: 300;
152
+ color: var(--text-sub);
153
+ margin-bottom: 32px;
154
+ }
155
+
156
+ .field-label {
157
+ display: block;
158
+ font-size: 11px; font-weight: 500;
159
+ letter-spacing: 0.2em; text-transform: uppercase;
160
+ color: var(--gold-dim);
161
+ margin-bottom: 8px;
162
+ }
163
+
164
+ #pgn {
165
+ width: 100%; height: 200px;
166
+ background: var(--bg-deepest);
167
+ border: 1px solid var(--border);
168
+ border-radius: 3px;
169
+ color: var(--text);
170
+ font-family: 'JetBrains Mono', monospace;
171
+ font-size: 13px;
172
+ line-height: 1.6;
173
+ padding: 16px;
174
+ resize: vertical;
175
+ outline: none;
176
+ transition: border-color 0.2s;
177
+ margin-bottom: 28px;
178
+ }
179
+ #pgn::placeholder { color: var(--text-muted); }
180
+ #pgn:focus { border-color: var(--gold-dim); }
181
+
182
+ /* ── Speed picker ────────────────────────────────────────────────────── */
183
+ .speed-group { margin-bottom: 10px; }
184
+ .speed-btns {
185
+ display: flex; gap: 8px;
186
+ }
187
+ .speed-btn {
188
+ flex: 1;
189
+ background: var(--bg-deepest);
190
+ border: 1px solid var(--border-bright);
191
+ border-radius: 3px;
192
+ color: var(--text-sub);
193
+ font-family: 'Crimson Pro', serif;
194
+ font-size: 15px; font-weight: 500;
195
+ padding: 10px 0;
196
+ cursor: pointer;
197
+ transition: all 0.15s;
198
+ letter-spacing: 0.04em;
199
+ }
200
+ .speed-btn:hover { border-color: var(--gold-dim); color: var(--text); }
201
+ .speed-btn.active {
202
+ background: rgba(201,168,76,0.1);
203
+ border-color: var(--gold);
204
+ color: var(--gold);
205
+ }
206
+ .speed-btn .speed-sub {
207
+ display: block;
208
+ font-size: 11px; font-weight: 300;
209
+ color: var(--text-muted);
210
+ margin-top: 2px;
211
+ letter-spacing: 0.08em;
212
+ }
213
+ .speed-btn.active .speed-sub { color: var(--gold-dim); }
214
+
215
+ /* ── Advanced options toggle ─────────────────────────────────────────── */
216
+ .adv-toggle {
217
+ margin-top: 10px; margin-bottom: 0;
218
+ text-align: right;
219
+ }
220
+ #adv-btn {
221
+ background: none; border: none;
222
+ font-family: 'Crimson Pro', serif;
223
+ font-size: 12px; font-weight: 400;
224
+ letter-spacing: 0.15em; text-transform: uppercase;
225
+ color: var(--text-muted);
226
+ cursor: pointer;
227
+ padding: 4px 0;
228
+ transition: color 0.15s;
229
+ }
230
+ #adv-btn:hover { color: var(--gold-dim); }
231
+
232
+ .depth-group { margin-top: 16px; margin-bottom: 0; display: none; }
233
+ .depth-row {
234
+ display: flex; align-items: center; gap: 16px;
235
+ }
236
+ #depth {
237
+ flex: 1;
238
+ -webkit-appearance: none;
239
+ height: 3px;
240
+ background: var(--border-bright);
241
+ border-radius: 2px;
242
+ outline: none;
243
+ }
244
+ #depth::-webkit-slider-thumb {
245
+ -webkit-appearance: none;
246
+ width: 16px; height: 16px;
247
+ border-radius: 50%;
248
+ background: var(--gold);
249
+ cursor: pointer;
250
+ box-shadow: 0 0 8px rgba(201,168,76,0.4);
251
+ }
252
+ #depth::-moz-range-thumb {
253
+ width: 16px; height: 16px;
254
+ border-radius: 50%;
255
+ background: var(--gold);
256
+ cursor: pointer; border: none;
257
+ }
258
+ #depth-val {
259
+ font-family: 'JetBrains Mono', monospace;
260
+ font-size: 20px; font-weight: 500;
261
+ color: var(--gold);
262
+ min-width: 28px; text-align: right;
263
+ }
264
+ .depth-hint {
265
+ display: block;
266
+ font-size: 13px; font-weight: 300;
267
+ color: var(--text-muted);
268
+ margin-top: 8px;
269
+ margin-bottom: 24px;
270
+ }
271
+
272
+ /* ── Screen 2: Loading ───────────────────────────────────────────────── */
273
+ #s-loading { justify-content: center; align-items: center; }
274
+
275
+ .loading-inner {
276
+ text-align: center;
277
+ width: 480px; max-width: 90vw;
278
+ }
279
+ .loading-inner h2 {
280
+ font-family: 'Playfair Display', serif;
281
+ font-size: 22px; font-weight: 400;
282
+ letter-spacing: 0.15em;
283
+ color: var(--text-sub);
284
+ margin-bottom: 36px;
285
+ text-transform: uppercase;
286
+ }
287
+
288
+ .progress-track {
289
+ width: 100%; height: 3px;
290
+ background: var(--border);
291
+ border-radius: 2px;
292
+ overflow: hidden;
293
+ margin-bottom: 20px;
294
+ }
295
+ #progress-bar {
296
+ height: 100%;
297
+ background: linear-gradient(90deg, var(--gold-dim), var(--gold), var(--gold-light));
298
+ border-radius: 2px;
299
+ width: 0%;
300
+ transition: width 0.45s cubic-bezier(0.4,0,0.2,1);
301
+ box-shadow: 0 0 12px rgba(201,168,76,0.5);
302
+ }
303
+
304
+ #progress-msg {
305
+ font-size: 14px; font-weight: 300;
306
+ color: var(--text-sub);
307
+ letter-spacing: 0.05em;
308
+ }
309
+ #progress-pct {
310
+ font-family: 'JetBrains Mono', monospace;
311
+ font-size: 12px;
312
+ color: var(--gold-dim);
313
+ margin-top: 10px;
314
+ }
315
+
316
+ /* ── Screen 3: Analysis ──────────────────────────────────────────────── */
317
+ #s-analysis {
318
+ align-items: center;
319
+ padding: 0 10px 40px;
320
+ }
321
+
322
+ .analysis-header {
323
+ display: flex; align-items: center; justify-content: space-between;
324
+ width: 100%; max-width: 620px;
325
+ padding: 12px 0 12px;
326
+ border-bottom: 1px solid var(--border);
327
+ margin-bottom: 14px;
328
+ gap: 8px;
329
+ }
330
+
331
+ .players {
332
+ text-align: center; flex: 1;
333
+ display: flex; align-items: center; justify-content: center; gap: 10px;
334
+ }
335
+ .player-name {
336
+ font-family: 'Playfair Display', serif;
337
+ font-size: 18px; font-weight: 500;
338
+ }
339
+ .white-player { color: #f0ede4; }
340
+ .black-player { color: #a09880; }
341
+ .vs {
342
+ font-size: 12px; font-weight: 300;
343
+ letter-spacing: 0.2em; text-transform: uppercase;
344
+ color: var(--text-muted);
345
+ }
346
+
347
+ @media (max-width: 420px) {
348
+ .player-name { font-size: 13px; }
349
+ .vs { display: none; }
350
+ .players { gap: 6px; }
351
+ .btn-ghost { font-size: 12px; padding: 5px 8px; }
352
+ }
353
+
354
+ /* ── Board ───────────────────────────────────────────────────────────── */
355
+ .board-area { position: relative; }
356
+
357
+ .board-with-coords {
358
+ display: flex; align-items: flex-start; gap: 0;
359
+ }
360
+
361
+ /* ── Eval bar ────────────────────────────────────────────────────────── */
362
+ #eval-bar-wrap {
363
+ position: relative;
364
+ width: 12px;
365
+ height: calc(var(--sq) * 8);
366
+ border-radius: 3px;
367
+ overflow: hidden;
368
+ background: #1c1a18;
369
+ margin-right: 6px;
370
+ flex-shrink: 0;
371
+ box-shadow: 0 0 0 1px var(--border);
372
+ }
373
+ #eval-bar-fill {
374
+ position: absolute;
375
+ bottom: 0; left: 0; right: 0;
376
+ background: #f0ede4;
377
+ height: 50%;
378
+ transition: height 0.5s cubic-bezier(0.4, 0, 0.2, 1);
379
+ border-radius: 0 0 3px 3px;
380
+ }
381
+
382
+ .rank-labels {
383
+ display: flex; flex-direction: column;
384
+ justify-content: space-around;
385
+ padding-right: 8px;
386
+ padding-bottom: calc(var(--sq) * 0); /* align with board */
387
+ }
388
+ .coord {
389
+ font-family: 'JetBrains Mono', monospace;
390
+ font-size: 11px;
391
+ color: var(--text-muted);
392
+ width: var(--sq); height: var(--sq);
393
+ display: flex; align-items: center; justify-content: center;
394
+ user-select: none;
395
+ }
396
+ .rank-labels .coord { width: auto; }
397
+
398
+ .board-column { display: flex; flex-direction: column; }
399
+
400
+ #board {
401
+ display: grid;
402
+ grid-template-columns: repeat(8, var(--sq));
403
+ grid-template-rows: repeat(8, var(--sq));
404
+ box-shadow:
405
+ 0 0 0 2px var(--border),
406
+ 0 0 0 3px var(--border-bright),
407
+ 0 24px 80px rgba(0,0,0,0.8),
408
+ 0 4px 16px rgba(0,0,0,0.5);
409
+ }
410
+
411
+ .file-labels {
412
+ display: flex;
413
+ margin-top: 4px;
414
+ }
415
+ .file-labels .coord { height: auto; padding: 4px 0; }
416
+
417
+ .sq {
418
+ width: var(--sq); height: var(--sq);
419
+ position: relative;
420
+ display: flex; align-items: center; justify-content: center;
421
+ overflow: hidden;
422
+ }
423
+ .sq.light { background: #f0d9b5; }
424
+ .sq.dark { background: #b58863; }
425
+
426
+ .overlay {
427
+ position: absolute; inset: 0; z-index: 1;
428
+ pointer-events: none;
429
+ }
430
+ .from-ov { background: rgba(255, 220, 60, 0.35); }
431
+ .to-ov { opacity: 0.72; }
432
+
433
+ .cls-badge {
434
+ position: absolute; top: 2px; right: 2px; z-index: 4;
435
+ width: calc(var(--sq) * 0.31); height: calc(var(--sq) * 0.31);
436
+ min-width: 14px; min-height: 14px;
437
+ border-radius: 50%;
438
+ display: flex; align-items: center; justify-content: center;
439
+ font-size: calc(var(--sq) * 0.16); font-weight: 700;
440
+ color: #fff;
441
+ font-family: 'JetBrains Mono', monospace;
442
+ pointer-events: none;
443
+ box-shadow: 0 1px 4px rgba(0,0,0,0.6);
444
+ }
445
+
446
+ .piece {
447
+ position: relative; z-index: 2;
448
+ font-size: calc(var(--sq) * 0.64); line-height: 1;
449
+ display: flex; align-items: center; justify-content: center;
450
+ width: 100%; height: 100%;
451
+ pointer-events: none; user-select: none;
452
+ }
453
+ .wp {
454
+ color: #fff;
455
+ text-shadow:
456
+ 0 0 2px #000, 0 0 3px #000,
457
+ 1px 1px 0 #000, -1px -1px 0 #000,
458
+ 1px -1px 0 #000, -1px 1px 0 #000;
459
+ }
460
+ .bp {
461
+ color: #1c1a18;
462
+ text-shadow:
463
+ 0 0 2px rgba(255,255,255,0.55),
464
+ 1px 1px 0 rgba(255,255,255,0.3),
465
+ -1px -1px 0 rgba(255,255,255,0.3);
466
+ }
467
+
468
+ /* ── Best move arrow ───────────────────────────────────────���─────────── */
469
+ .board-wrap {
470
+ position: relative;
471
+ display: inline-block;
472
+ line-height: 0;
473
+ }
474
+ #best-arrow-svg {
475
+ position: absolute;
476
+ top: 0; left: 0;
477
+ width: 100%; height: 100%;
478
+ pointer-events: none;
479
+ z-index: 5;
480
+ }
481
+
482
+ /* ── Navigation ──────────────────────────────────────────────────────── */
483
+ .nav-row {
484
+ display: flex; align-items: center; gap: 20px;
485
+ margin-top: 12px;
486
+ }
487
+ .nav-btn {
488
+ background: var(--bg-card);
489
+ border: 1px solid var(--border-bright);
490
+ color: var(--text-sub);
491
+ width: 40px; height: 40px;
492
+ border-radius: 3px;
493
+ font-size: 16px; cursor: pointer;
494
+ display: flex; align-items: center; justify-content: center;
495
+ transition: all 0.15s;
496
+ }
497
+ .nav-btn:hover:not(:disabled) { border-color: var(--gold-dim); color: var(--text); background: var(--bg-elevated); }
498
+ .nav-btn:disabled { opacity: 0.25; cursor: default; }
499
+
500
+ #move-indicator {
501
+ font-family: 'JetBrains Mono', monospace;
502
+ font-size: 13px; font-weight: 500;
503
+ color: var(--text-sub);
504
+ min-width: 100px; text-align: center;
505
+ }
506
+
507
+ /* ── Move timeline ───────────────────────────────────────────────────── */
508
+ #move-timeline {
509
+ display: flex; gap: 3px;
510
+ margin-top: 10px;
511
+ max-width: calc(var(--sq) * 8 + 46px);
512
+ width: 100%;
513
+ overflow-x: auto;
514
+ padding-bottom: 4px;
515
+ scrollbar-width: thin;
516
+ scrollbar-color: var(--border-bright) transparent;
517
+ }
518
+ .tl-dot {
519
+ flex-shrink: 0;
520
+ width: 10px; height: 10px;
521
+ border-radius: 50%;
522
+ cursor: pointer;
523
+ transition: transform 0.15s, opacity 0.15s;
524
+ opacity: 0.65;
525
+ }
526
+ .tl-dot:hover { opacity: 1; transform: scale(1.3); }
527
+ .tl-dot.tl-active {
528
+ opacity: 1; transform: scale(1.25);
529
+ box-shadow: 0 0 6px currentColor;
530
+ }
531
+
532
+ /* ── Info box ────────────────────────────────────────────────────────── */
533
+ #info-box {
534
+ margin-top: 12px;
535
+ width: 100%;
536
+ max-width: calc(var(--sq) * 8 + 46px);
537
+ background: var(--bg-card);
538
+ border: 1px solid var(--border);
539
+ border-radius: 4px;
540
+ overflow: visible;
541
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
542
+ margin-bottom: 24px;
543
+ }
544
+
545
+ .info-start {
546
+ padding: 28px 32px;
547
+ color: var(--text-muted);
548
+ font-size: 15px; font-weight: 300;
549
+ font-style: italic;
550
+ }
551
+
552
+ .info-header {
553
+ padding: 16px 18px 14px;
554
+ border-bottom: 1px solid var(--border);
555
+ display: grid;
556
+ grid-template-columns: auto 1fr auto;
557
+ align-items: center;
558
+ gap: 12px;
559
+ }
560
+
561
+ @media (max-width: 420px) {
562
+ .info-header { grid-template-columns: auto 1fr; gap: 8px; padding: 12px 14px; }
563
+ .ep-badge { display: none; }
564
+ .info-comment { padding: 10px 14px; font-size: 13px; }
565
+ .info-row { padding: 8px 14px; gap: 8px; grid-template-columns: 90px 1fr; }
566
+ .info-label { font-size: 10px; }
567
+ .info-val { font-size: 13px; }
568
+ .info-val.mono { font-size: 11px; }
569
+ .move-name { font-size: 16px; }
570
+ .cls-label { font-size: 10px; }
571
+ }
572
+
573
+ .cls-pill {
574
+ display: inline-flex; align-items: center; gap: 7px;
575
+ padding: 5px 12px 5px 9px;
576
+ border-radius: 99px;
577
+ border: 1px solid;
578
+ white-space: nowrap;
579
+ }
580
+ .cls-icon {
581
+ font-size: 12px;
582
+ font-family: 'JetBrains Mono', monospace;
583
+ }
584
+ .cls-label {
585
+ font-size: 11px; font-weight: 600;
586
+ letter-spacing: 0.15em;
587
+ font-family: 'Crimson Pro', serif;
588
+ }
589
+
590
+ .move-name {
591
+ font-family: 'Playfair Display', serif;
592
+ font-size: 20px; font-weight: 600;
593
+ color: var(--text);
594
+ }
595
+
596
+ .ep-badge {
597
+ font-family: 'JetBrains Mono', monospace;
598
+ font-size: 11px;
599
+ color: var(--text-muted);
600
+ white-space: nowrap;
601
+ }
602
+
603
+ .info-comment {
604
+ padding: 13px 18px;
605
+ font-size: 15px; font-weight: 300;
606
+ font-style: italic;
607
+ color: var(--text-sub);
608
+ border-bottom: 1px solid var(--border);
609
+ line-height: 1.5;
610
+ }
611
+
612
+ .info-details { padding: 4px 0 8px; }
613
+
614
+ .info-row {
615
+ display: grid;
616
+ grid-template-columns: 120px 1fr;
617
+ align-items: baseline;
618
+ gap: 12px;
619
+ padding: 9px 18px;
620
+ }
621
+ .info-row + .info-row { border-top: 1px solid var(--border); }
622
+
623
+ .info-label {
624
+ font-size: 11px; font-weight: 500;
625
+ letter-spacing: 0.18em; text-transform: uppercase;
626
+ color: var(--text-muted);
627
+ }
628
+ .info-val {
629
+ font-size: 15px;
630
+ color: var(--text);
631
+ }
632
+ .info-val.mono {
633
+ font-family: 'JetBrains Mono', monospace;
634
+ font-size: 13px;
635
+ }
636
+ .cont-text {
637
+ color: var(--text-sub);
638
+ line-height: 1.7;
639
+ word-break: break-word;
640
+ }
641
+
642
+ .best-row.found .info-val {
643
+ color: #6aa84f;
644
+ font-weight: 500;
645
+ }
646
+ /* ── Screen 4: Summary ───────────────────────────────────────────────── */
647
+ #s-summary {
648
+ justify-content: flex-start;
649
+ align-items: center;
650
+ padding: 0 16px 40px;
651
+ }
652
+
653
+ .summary-header {
654
+ display: flex; align-items: center; justify-content: space-between;
655
+ width: 100%; max-width: 680px;
656
+ padding: 12px 0;
657
+ border-bottom: 1px solid var(--border);
658
+ margin-bottom: 24px;
659
+ gap: 8px;
660
+ }
661
+ .summary-title {
662
+ font-family: 'Playfair Display', serif;
663
+ font-size: 20px; font-weight: 600;
664
+ color: var(--text);
665
+ text-align: center; flex: 1;
666
+ }
667
+
668
+ .summary-grid {
669
+ display: grid;
670
+ grid-template-columns: 1fr 1fr;
671
+ gap: 16px;
672
+ width: 100%; max-width: 680px;
673
+ }
674
+ @media (max-width: 520px) {
675
+ .summary-grid { grid-template-columns: 1fr; }
676
+ }
677
+
678
+ .summary-card {
679
+ background: var(--bg-card);
680
+ border: 1px solid var(--border);
681
+ border-radius: 4px;
682
+ overflow: hidden;
683
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
684
+ }
685
+
686
+ .summary-card-header {
687
+ padding: 14px 20px 12px;
688
+ border-bottom: 1px solid var(--border);
689
+ display: flex; align-items: baseline; justify-content: space-between;
690
+ gap: 12px;
691
+ }
692
+ .summary-player-name {
693
+ font-family: 'Playfair Display', serif;
694
+ font-size: 17px; font-weight: 600;
695
+ }
696
+ .summary-accuracy {
697
+ display: flex; align-items: baseline; gap: 5px;
698
+ }
699
+ .summary-accuracy-num {
700
+ font-family: 'JetBrains Mono', monospace;
701
+ font-size: 22px; font-weight: 500;
702
+ color: var(--gold);
703
+ }
704
+ .summary-accuracy-label {
705
+ font-size: 11px; font-weight: 400;
706
+ letter-spacing: 0.12em; text-transform: uppercase;
707
+ color: var(--text-muted);
708
+ }
709
+
710
+ .summary-rows { padding: 6px 0 10px; }
711
+
712
+ .summary-row {
713
+ display: flex; align-items: center;
714
+ padding: 6px 20px;
715
+ gap: 10px;
716
+ }
717
+ .summary-row:hover { background: rgba(255,255,255,0.02); }
718
+
719
+ .summary-icon {
720
+ width: 22px; height: 22px;
721
+ border-radius: 50%;
722
+ display: flex; align-items: center; justify-content: center;
723
+ font-size: 9px; font-weight: 700;
724
+ font-family: 'JetBrains Mono', monospace;
725
+ color: #fff;
726
+ flex-shrink: 0;
727
+ }
728
+ .summary-cls-name {
729
+ font-size: 13px; font-weight: 400;
730
+ color: var(--text-sub);
731
+ flex: 1;
732
+ }
733
+ .summary-count {
734
+ font-family: 'JetBrains Mono', monospace;
735
+ font-size: 14px; font-weight: 500;
736
+ color: var(--text);
737
+ min-width: 20px; text-align: right;
738
+ }
739
+ .summary-bar-wrap {
740
+ width: 60px; height: 4px;
741
+ background: var(--border);
742
+ border-radius: 2px;
743
+ overflow: hidden;
744
+ }
745
+ .summary-bar-fill {
746
+ height: 100%; border-radius: 2px;
747
+ }
748
+
749
+ .summary-cta {
750
+ margin-top: 20px;
751
+ display: flex; gap: 10px; justify-content: center;
752
+ }
753
+ </style>
754
+ </head>
755
+ <body>
756
+
757
+ <!-- ═══════════════════════════════════════════════════════════════════════
758
+ SCREEN 1 — PGN Input
759
+ ══════════════════════════════════════════════════════════════════════════ -->
760
+ <div id="s-input" class="screen active">
761
+ <div style="margin-top: auto; margin-bottom: auto; width: 100%; max-width: 640px; padding: 40px 20px;">
762
+ <div class="brand">
763
+ <span class="brand-line"></span>
764
+ <span class="brand-name">Chess Analysis</span>
765
+ <span class="brand-line"></span>
766
+ </div>
767
+
768
+ <div class="input-card">
769
+ <h1>Game Analysis</h1>
770
+ <p class="subtitle">Paste your PGN below and set analysis depth</p>
771
+
772
+ <label class="field-label" for="pgn">PGN Notation</label>
773
+ <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>
774
+
775
+ <div class="speed-group">
776
+ <label class="field-label">Analysis Speed</label>
777
+ <div class="speed-btns">
778
+ <button class="speed-btn" data-depth="8">Fast<span class="speed-sub">Depth 8</span></button>
779
+ <button class="speed-btn active" data-depth="10">Default<span class="speed-sub">Depth 10</span></button>
780
+ <button class="speed-btn" data-depth="13">Slow<span class="speed-sub">Depth 13</span></button>
781
+ </div>
782
+ </div>
783
+
784
+ <div class="adv-toggle">
785
+ <button id="adv-btn">Advanced Options ▾</button>
786
+ </div>
787
+
788
+ <div class="depth-group" id="adv-group">
789
+ <label class="field-label" for="depth">Custom Depth</label>
790
+ <div class="depth-row">
791
+ <input type="range" id="depth" min="6" max="22" value="10">
792
+ <span id="depth-val">10</span>
793
+ </div>
794
+ <span class="depth-hint">Deeper = stronger analysis, longer wait</span>
795
+ </div>
796
+
797
+ <div style="text-align: center; margin-top: 28px;">
798
+ <button id="analyze-btn" class="btn btn-gold">Analyze Game</button>
799
+ </div>
800
+ </div>
801
+ </div>
802
+ </div>
803
+
804
+
805
+ <!-- ═══════════════════════════════════════════════════════════════════════
806
+ SCREEN 2 — Loading
807
+ ══════════════════════════════════════════════════════════════════════════ -->
808
+ <div id="s-loading" class="screen">
809
+ <div class="loading-inner" style="margin: auto;">
810
+ <h2>Analyzing</h2>
811
+ <div class="progress-track">
812
+ <div id="progress-bar"></div>
813
+ </div>
814
+ <p id="progress-msg">Initializing engine…</p>
815
+ <p id="progress-pct">0%</p>
816
+ </div>
817
+ </div>
818
+
819
+
820
+ <!-- ═══════════════════════════════════════════════════════════════════════
821
+ SCREEN 3 — Summary
822
+ ══════════════════════════════════════════════════════════════════════════ -->
823
+ <div id="s-summary" class="screen">
824
+
825
+ <div class="summary-header">
826
+ <button id="summary-back-btn" class="btn btn-ghost">← New Analysis</button>
827
+ <div class="summary-title">Game Summary</div>
828
+ <button id="summary-review-btn" class="btn btn-gold" style="font-size:14px; padding:9px 20px;">Review Board →</button>
829
+ </div>
830
+
831
+ <div class="summary-grid" id="summary-grid">
832
+ <!-- populated by JS -->
833
+ </div>
834
+
835
+ </div>
836
+
837
+
838
+ <!-- ═══════════════════════════════════════════════════════════════════════
839
+ SCREEN 4 — Analysis
840
+ ══════════════════════════════════════════════════════════════════════════ -->
841
+ <div id="s-analysis" class="screen">
842
+
843
+ <div class="analysis-header">
844
+ <button id="back-btn" class="btn btn-ghost">← New Analysis</button>
845
+ <div class="players">
846
+ <span id="white-name" class="player-name white-player">White</span>
847
+ <span class="vs">vs</span>
848
+ <span id="black-name" class="player-name black-player">Black</span>
849
+ </div>
850
+ <button id="flip-btn" class="btn btn-ghost">⇅ Flip</button>
851
+ </div>
852
+
853
+ <div class="board-area">
854
+ <div class="board-with-coords">
855
+ <div id="eval-bar-wrap"><div id="eval-bar-fill"></div></div>
856
+ <div class="rank-labels" id="rank-labels"></div>
857
+ <div class="board-column">
858
+ <div class="board-wrap">
859
+ <div id="board"></div>
860
+ <svg id="best-arrow-svg" viewBox="0 0 8 8" preserveAspectRatio="none">
861
+ <defs>
862
+ <marker id="arrowhead" markerWidth="3" markerHeight="3"
863
+ refX="2.5" refY="1.5" orient="auto">
864
+ <polygon points="0 0, 3 1.5, 0 3" fill="rgba(91,141,217,0.9)"/>
865
+ </marker>
866
+ </defs>
867
+ <line id="best-arrow-line" x1="0" y1="0" x2="0" y2="0"
868
+ stroke="rgba(91,141,217,0.85)" stroke-width="0.22"
869
+ stroke-linecap="round" marker-end="url(#arrowhead)"
870
+ display="none"/>
871
+ </svg>
872
+ </div>
873
+ <div class="file-labels" id="file-labels"></div>
874
+ </div>
875
+ </div>
876
+ </div>
877
+
878
+ <div class="nav-row">
879
+ <button id="prev-btn" class="nav-btn" title="Previous move (←)">←</button>
880
+ <span id="move-indicator">Start</span>
881
+ <button id="next-btn" class="nav-btn" title="Next move (→)">→</button>
882
+ </div>
883
+
884
+ <div id="move-timeline"></div>
885
+
886
+ <div id="info-box">
887
+ <div class="info-start"><p>Press → to begin stepping through the game.</p></div>
888
+ </div>
889
+
890
+ </div>
891
+
892
+ <script>
893
+ // ── Constants ──────────────────────────────────────────────────────────
894
+ const PIECE_MAP = {
895
+ 'K':'♔','Q':'♕','R':'♖','B':'♗','N':'♘','P':'♙',
896
+ 'k':'♚','q':'♛','r':'♜','b':'♝','n':'♞','p':'♟'
897
+ };
898
+
899
+ const CLS_META = {
900
+ Book: { color:'#b5854a', icon:'📖', label:'Book' },
901
+ Brilliant: { color:'#22d1cc', icon:'!!', label:'Brilliant' },
902
+ Great: { color:'#5b8dd9', icon:'!', label:'Great' },
903
+ Miss: { color:'#e05555', icon:'×', label:'Miss' },
904
+ Best: { color:'#5fb153', icon:'★', label:'Best' },
905
+ Excellent: { color:'#96d16a', icon:'👍', label:'Excellent' },
906
+ Good: { color:'#a8d672', icon:'✓', label:'Good' },
907
+ Inaccuracy: { color:'#f0c040', icon:'?!', label:'Inaccuracy' },
908
+ Mistake: { color:'#e08030', icon:'?', label:'Mistake' },
909
+ Blunder: { color:'#d43030', icon:'??', label:'Blunder' },
910
+ };
911
+
912
+ // ── State ──────────────────────────────────────────────────────────────
913
+ let gameData = null;
914
+ let curIdx = -1; // -1 = start, 0..n-1 = after that move
915
+ let flipped = false;
916
+
917
+ // ── Eval bar ───────────────────────────────────────────────────────────
918
+ function getWhiteEP(mv) {
919
+ return mv.color === 'white' ? mv.ep_after : 1.0 - mv.ep_after;
920
+ }
921
+
922
+ function updateEvalBar(epWhite) {
923
+ // epWhite: 0.0 (black winning) → 1.0 (white winning), 0.5 = equal
924
+ const fill = document.getElementById('eval-bar-fill');
925
+ if (!fill) return;
926
+ const pct = Math.max(2, Math.min(98, epWhite * 100));
927
+ fill.style.height = pct + '%';
928
+ }
929
+
930
+ // ── Summary screen ─────────────────────────────────────────────────────
931
+ const CLS_ORDER = [
932
+ "Book","Brilliant","Great","Best","Excellent","Good",
933
+ "Inaccuracy","Mistake","Blunder","Miss"
934
+ ];
935
+ const CLS_DISPLAY = {
936
+ "Miss": "Missed Tactic",
937
+ };
938
+
939
+ function renderSummary(data) {
940
+ const grid = document.getElementById('summary-grid');
941
+ grid.innerHTML = '';
942
+
943
+ const colors = [
944
+ { key: 'white', name: data.white, nameClass: 'white-player' },
945
+ { key: 'black', name: data.black, nameClass: 'black-player' },
946
+ ];
947
+
948
+ for (const { key, name, nameClass } of colors) {
949
+ const stats = data.summary[key];
950
+ const counts = stats.counts;
951
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
952
+ const maxCount = Math.max(1, ...Object.values(counts));
953
+
954
+ const rows = CLS_ORDER.map(cls => {
955
+ const meta = CLS_META[cls];
956
+ const count = counts[cls] || 0;
957
+ const barPct = Math.round((count / maxCount) * 100);
958
+ const label = CLS_DISPLAY[cls] || cls;
959
+ return `
960
+ <div class="summary-row">
961
+ <div class="summary-icon" style="background:${meta.color}">${meta.icon}</div>
962
+ <span class="summary-cls-name">${label}</span>
963
+ <div class="summary-bar-wrap">
964
+ <div class="summary-bar-fill" style="width:${barPct}%;background:${meta.color}88"></div>
965
+ </div>
966
+ <span class="summary-count">${count}</span>
967
+ </div>`;
968
+ }).join('');
969
+
970
+ grid.innerHTML += `
971
+ <div class="summary-card">
972
+ <div class="summary-card-header">
973
+ <span class="summary-player-name ${nameClass}">${name}</span>
974
+ <div class="summary-accuracy">
975
+ <span class="summary-accuracy-num">${stats.accuracy}%</span>
976
+ <span class="summary-accuracy-label">accuracy</span>
977
+ </div>
978
+ </div>
979
+ <div class="summary-rows">${rows}</div>
980
+ </div>`;
981
+ }
982
+ }
983
+
984
+ // ── Screen switching ───────────────────────────────────────────────────
985
+ function showScreen(id) {
986
+ document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
987
+ document.getElementById(id).classList.add('active');
988
+ }
989
+
990
+ // ── FEN parser ─────────────────────────────────────────────────────────
991
+ function parseFEN(fen) {
992
+ const rows = fen.split(' ')[0].split('/');
993
+ const board = {};
994
+ for (let r = 0; r < 8; r++) {
995
+ const rank = 8 - r;
996
+ let file = 0;
997
+ for (const ch of rows[r]) {
998
+ if (ch >= '1' && ch <= '8') { file += +ch; }
999
+ else { board['abcdefgh'[file] + rank] = ch; file++; }
1000
+ }
1001
+ }
1002
+ return board;
1003
+ }
1004
+
1005
+ // ── Board renderer ─────────────────────────────────────────────────────
1006
+ function sqToGrid(sqName) {
1007
+ // Returns {col, row} in 0-7 grid coords for current flip state
1008
+ const files = ['a','b','c','d','e','f','g','h'];
1009
+ const file = sqName[0];
1010
+ const rank = parseInt(sqName[1]);
1011
+ const fi = files.indexOf(file);
1012
+ const col = flipped ? 7 - fi : fi;
1013
+ const row = flipped ? rank - 1 : 8 - rank;
1014
+ return { col, row };
1015
+ }
1016
+
1017
+ function renderBestArrow(bestUci) {
1018
+ const line = document.getElementById('best-arrow-line');
1019
+ if (!line) return;
1020
+ if (!bestUci || bestUci.length < 4) { line.setAttribute('display','none'); return; }
1021
+ const from = bestUci.slice(0, 2);
1022
+ const to = bestUci.slice(2, 4);
1023
+ const f = sqToGrid(from);
1024
+ const t = sqToGrid(to);
1025
+ // Centre of each square in 0-8 SVG coordinate space
1026
+ const x1 = f.col + 0.5, y1 = f.row + 0.5;
1027
+ const x2 = t.col + 0.5, y2 = t.row + 0.5;
1028
+ // Shorten slightly so arrowhead sits inside the square
1029
+ const dx = x2 - x1, dy = y2 - y1;
1030
+ const len = Math.sqrt(dx*dx + dy*dy);
1031
+ const shorten = 0.38;
1032
+ const nx = dx/len, ny = dy/len;
1033
+ line.setAttribute('x1', x1 + nx * 0.25);
1034
+ line.setAttribute('y1', y1 + ny * 0.25);
1035
+ line.setAttribute('x2', x2 - nx * shorten);
1036
+ line.setAttribute('y2', y2 - ny * shorten);
1037
+ line.setAttribute('display', '');
1038
+ }
1039
+
1040
+ function renderBoard(fen, fromSq, toSq, cls, bestUci) {
1041
+ const pieces = parseFEN(fen);
1042
+ const boardEl = document.getElementById('board');
1043
+ boardEl.innerHTML = '';
1044
+
1045
+ const rankOrder = flipped ? [1,2,3,4,5,6,7,8] : [8,7,6,5,4,3,2,1];
1046
+ const fileOrder = flipped
1047
+ ? ['h','g','f','e','d','c','b','a']
1048
+ : ['a','b','c','d','e','f','g','h'];
1049
+
1050
+ const meta = cls ? (CLS_META[cls] || null) : null;
1051
+
1052
+ for (const rank of rankOrder) {
1053
+ for (let fi = 0; fi < 8; fi++) {
1054
+ const file = fileOrder[fi];
1055
+ const sq = file + rank;
1056
+ const isLight = (fi + rank) % 2 === 0;
1057
+
1058
+ const div = document.createElement('div');
1059
+ div.className = `sq ${isLight ? 'light' : 'dark'}`;
1060
+
1061
+ // From-square highlight
1062
+ if (sq === fromSq) {
1063
+ const ov = document.createElement('div');
1064
+ ov.className = 'overlay from-ov';
1065
+ div.appendChild(ov);
1066
+ }
1067
+
1068
+ // To-square highlight + badge
1069
+ if (sq === toSq && meta) {
1070
+ const ov = document.createElement('div');
1071
+ ov.className = 'overlay to-ov';
1072
+ ov.style.background = meta.color;
1073
+ div.appendChild(ov);
1074
+
1075
+ const badge = document.createElement('div');
1076
+ badge.className = 'cls-badge';
1077
+ badge.textContent = meta.icon;
1078
+ badge.style.background = meta.color;
1079
+ div.appendChild(badge);
1080
+ }
1081
+
1082
+ // Piece
1083
+ const piece = pieces[sq];
1084
+ if (piece) {
1085
+ const span = document.createElement('span');
1086
+ span.className = `piece ${piece === piece.toUpperCase() ? 'wp' : 'bp'}`;
1087
+ span.textContent = PIECE_MAP[piece] || piece;
1088
+ div.appendChild(span);
1089
+ }
1090
+
1091
+ boardEl.appendChild(div);
1092
+ }
1093
+ }
1094
+
1095
+ updateCoords(rankOrder, fileOrder);
1096
+ renderBestArrow(bestUci || null);
1097
+ }
1098
+
1099
+ function updateCoords(rankOrder, fileOrder) {
1100
+ const rl = document.getElementById('rank-labels');
1101
+ rl.innerHTML = rankOrder.map(r =>
1102
+ `<div class="coord" style="width:auto; padding-right:0">${r}</div>`
1103
+ ).join('');
1104
+
1105
+ const fl = document.getElementById('file-labels');
1106
+ fl.innerHTML = fileOrder.map(f =>
1107
+ `<div class="coord" style="height:auto">${f}</div>`
1108
+ ).join('');
1109
+ }
1110
+
1111
+ // ── Timeline ───────────────────────────────────────────────────────────
1112
+ function buildTimeline() {
1113
+ const tl = document.getElementById('move-timeline');
1114
+ tl.innerHTML = '';
1115
+ gameData.moves.forEach((mv, i) => {
1116
+ const dot = document.createElement('div');
1117
+ dot.className = 'tl-dot';
1118
+ dot.style.background = (CLS_META[mv.classification] || {color:'#555'}).color;
1119
+ dot.title = `${mv.move_number}${mv.color==='white'?'.':'...'} ${mv.san} — ${mv.classification}`;
1120
+ dot.addEventListener('click', () => navigateTo(i));
1121
+ tl.appendChild(dot);
1122
+ });
1123
+ }
1124
+
1125
+ function refreshTimeline() {
1126
+ const dots = document.querySelectorAll('.tl-dot');
1127
+ dots.forEach((d, i) => {
1128
+ d.classList.toggle('tl-active', i === curIdx);
1129
+ });
1130
+ // Scroll active dot into view
1131
+ if (curIdx >= 0 && dots[curIdx]) {
1132
+ dots[curIdx].scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: 'nearest' });
1133
+ }
1134
+ }
1135
+
1136
+ // ── Info box ───────────────────────────────────────────────────────────
1137
+ function renderStartInfo() {
1138
+ document.getElementById('info-box').innerHTML = `
1139
+ <div class="info-start">
1140
+ <p>Press <strong>→</strong> or the right arrow key to step through the game.</p>
1141
+ </div>`;
1142
+ }
1143
+
1144
+ function renderMoveInfo(mv) {
1145
+ const meta = CLS_META[mv.classification] || { color:'#888', icon:'?', label: mv.classification };
1146
+ const ep = mv.ep_loss > 0.001
1147
+ ? `−${(mv.ep_loss * 100).toFixed(1)}% EP`
1148
+ : 'No EP lost';
1149
+
1150
+ const moveLabel = mv.color === 'white'
1151
+ ? `${mv.move_number}. ${mv.san}`
1152
+ : `${mv.move_number}… ${mv.san}`;
1153
+
1154
+ const bestHtml = mv.best_move_san
1155
+ ? `<div class="info-row best-row">
1156
+ <span class="info-label">Best move</span>
1157
+ <span class="info-val mono">${mv.color === 'white' ? mv.move_number + '.' : mv.move_number + '…'} ${mv.best_move_san}</span>
1158
+ </div>`
1159
+ : `<div class="info-row best-row found">
1160
+ <span class="info-label">Best move</span>
1161
+ <span class="info-val found">✓ You found the best move</span>
1162
+ </div>`;
1163
+
1164
+ const openingHtml = mv.opening_name
1165
+ ? `<div class="info-row">
1166
+ <span class="info-label">Opening</span>
1167
+ <span class="info-val">${mv.opening_eco ? '<span style="font-family:monospace;color:var(--gold-dim);font-size:12px;margin-right:6px">' + mv.opening_eco + '</span>' : ''}${mv.opening_name}</span>
1168
+ </div>`
1169
+ : '';
1170
+
1171
+ const contHtml = mv.continuation_fmt
1172
+ ? `<div class="info-row cont-row">
1173
+ <span class="info-label">Continuation</span>
1174
+ <span class="info-val mono cont-text">${mv.continuation_fmt}</span>
1175
+ </div>`
1176
+ : '';
1177
+
1178
+ document.getElementById('info-box').innerHTML = `
1179
+ <div class="info-header">
1180
+ <div class="cls-pill" style="background:${meta.color}18; border-color:${meta.color}55; color:${meta.color}">
1181
+ <span class="cls-icon">${meta.icon}</span>
1182
+ <span class="cls-label">${meta.label.toUpperCase()}</span>
1183
+ </div>
1184
+ <div class="move-name">${moveLabel}</div>
1185
+ <div class="ep-badge">${ep}</div>
1186
+ </div>
1187
+ <div class="info-comment">"${mv.comment}"</div>
1188
+ <div class="info-details">
1189
+ ${openingHtml}
1190
+ ${bestHtml}
1191
+ ${contHtml}
1192
+ </div>`;
1193
+ }
1194
+
1195
+ // ── Navigation ─────────────────────────────────────────────────────────
1196
+ function navigateTo(idx) {
1197
+ if (!gameData) return;
1198
+ const max = gameData.moves.length - 1;
1199
+ curIdx = Math.max(-1, Math.min(idx, max));
1200
+
1201
+ document.getElementById('prev-btn').disabled = curIdx === -1;
1202
+ document.getElementById('next-btn').disabled = curIdx === max;
1203
+
1204
+ if (curIdx === -1) {
1205
+ renderBoard(gameData.initial_fen, null, null, null, null);
1206
+ document.getElementById('move-indicator').textContent = 'Start';
1207
+ renderStartInfo();
1208
+ updateEvalBar(0.5);
1209
+ } else {
1210
+ const mv = gameData.moves[curIdx];
1211
+ // Show best-move arrow except for Book (theory) and Best (already played it)
1212
+ const showArrow = mv.best_move_uci && mv.classification !== 'Book' && mv.classification !== 'Best';
1213
+ renderBoard(mv.fen_after, mv.from_square, mv.to_square, mv.classification,
1214
+ showArrow ? mv.best_move_uci : null);
1215
+ document.getElementById('move-indicator').textContent =
1216
+ `Move ${curIdx + 1} / ${gameData.moves.length}`;
1217
+ renderMoveInfo(mv);
1218
+ updateEvalBar(getWhiteEP(mv));
1219
+ }
1220
+
1221
+ refreshTimeline();
1222
+ }
1223
+
1224
+ // ── Load completed analysis ────────────────────────────────────────────
1225
+ function loadAnalysis(data) {
1226
+ gameData = data;
1227
+ curIdx = -1;
1228
+ flipped = false;
1229
+
1230
+ document.getElementById('white-name').textContent = data.white;
1231
+ document.getElementById('black-name').textContent = data.black;
1232
+
1233
+ buildTimeline();
1234
+ navigateTo(-1);
1235
+
1236
+ // Show summary first
1237
+ renderSummary(data);
1238
+ showScreen('s-summary');
1239
+ }
1240
+
1241
+ // ── Progress bar ───────────────────────────────────────────────────────
1242
+ function setProgress(pct, msg) {
1243
+ document.getElementById('progress-bar').style.width = `${Math.round(pct * 100)}%`;
1244
+ document.getElementById('progress-msg').textContent = msg;
1245
+ document.getElementById('progress-pct').textContent = `${Math.round(pct * 100)}%`;
1246
+ }
1247
+
1248
+ // ── Submit analysis ────────────────────────────────────────────────────
1249
+ async function submitAnalysis() {
1250
+ const pgn = document.getElementById('pgn').value.trim();
1251
+ const depth = parseInt(document.getElementById('depth').value, 10);
1252
+
1253
+ if (!pgn) { alert('Please paste a PGN to analyze.'); return; }
1254
+
1255
+ showScreen('s-loading');
1256
+ setProgress(0, 'Connecting to analysis engine…');
1257
+
1258
+ let jobId;
1259
+ try {
1260
+ const res = await fetch('/api/analyze', {
1261
+ method: 'POST',
1262
+ headers: { 'Content-Type': 'application/json' },
1263
+ body: JSON.stringify({ pgn, depth })
1264
+ });
1265
+ const d = await res.json();
1266
+ if (d.error) throw new Error(d.error);
1267
+ jobId = d.job_id;
1268
+ } catch (err) {
1269
+ alert('Error: ' + err.message);
1270
+ showScreen('s-input');
1271
+ return;
1272
+ }
1273
+
1274
+ const evtSrc = new EventSource(`/api/stream/${jobId}`);
1275
+
1276
+ evtSrc.onmessage = (e) => {
1277
+ const data = JSON.parse(e.data);
1278
+ if (data.type === 'progress') {
1279
+ setProgress(data.progress, data.message);
1280
+ } else if (data.type === 'complete') {
1281
+ evtSrc.close();
1282
+ loadAnalysis(data.data);
1283
+ } else if (data.type === 'error') {
1284
+ evtSrc.close();
1285
+ alert('Analysis error: ' + data.message);
1286
+ showScreen('s-input');
1287
+ }
1288
+ };
1289
+
1290
+ evtSrc.onerror = () => {
1291
+ evtSrc.close();
1292
+ alert('Connection lost. Please try again.');
1293
+ showScreen('s-input');
1294
+ };
1295
+ }
1296
+
1297
+ // ── Speed picker + advanced options ───────────────────────────────────
1298
+ let advancedOpen = false;
1299
+
1300
+ document.querySelectorAll('.speed-btn').forEach(btn => {
1301
+ btn.addEventListener('click', () => {
1302
+ document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
1303
+ btn.classList.add('active');
1304
+ document.getElementById('depth').value = btn.dataset.depth;
1305
+ document.getElementById('depth-val').textContent = btn.dataset.depth;
1306
+ });
1307
+ });
1308
+
1309
+ document.getElementById('adv-btn').addEventListener('click', () => {
1310
+ advancedOpen = !advancedOpen;
1311
+ document.getElementById('adv-group').style.display = advancedOpen ? 'block' : 'none';
1312
+ document.querySelector('.speed-group').style.display = advancedOpen ? 'none' : 'block';
1313
+ document.getElementById('adv-btn').textContent =
1314
+ advancedOpen ? 'Advanced Options ▴' : 'Advanced Options ▾';
1315
+ });
1316
+
1317
+ document.getElementById('depth').addEventListener('input', e => {
1318
+ document.getElementById('depth-val').textContent = e.target.value;
1319
+ });
1320
+
1321
+ document.getElementById('analyze-btn').addEventListener('click', submitAnalysis);
1322
+
1323
+ document.getElementById('summary-back-btn').addEventListener('click', () => {
1324
+ showScreen('s-input');
1325
+ });
1326
+
1327
+ document.getElementById('summary-review-btn').addEventListener('click', () => {
1328
+ showScreen('s-analysis');
1329
+ });
1330
+
1331
+ document.getElementById('back-btn').addEventListener('click', () => {
1332
+ showScreen('s-summary');
1333
+ });
1334
+
1335
+ document.getElementById('prev-btn').addEventListener('click', () => navigateTo(curIdx - 1));
1336
+ document.getElementById('next-btn').addEventListener('click', () => navigateTo(curIdx + 1));
1337
+
1338
+ document.getElementById('flip-btn').addEventListener('click', () => {
1339
+ flipped = !flipped;
1340
+ if (!gameData) return;
1341
+ if (curIdx === -1) {
1342
+ renderBoard(gameData.initial_fen, null, null, null, null);
1343
+ } else {
1344
+ const mv = gameData.moves[curIdx];
1345
+ const showArrow = mv.best_move_uci && mv.classification !== 'Book' && mv.classification !== 'Best';
1346
+ renderBoard(mv.fen_after, mv.from_square, mv.to_square, mv.classification,
1347
+ showArrow ? mv.best_move_uci : null);
1348
+ }
1349
+ });
1350
+
1351
+ document.addEventListener('keydown', (e) => {
1352
+ if (!gameData) return;
1353
+ const active = document.querySelector('.screen.active');
1354
+ if (!active || active.id !== 's-analysis') return;
1355
+ if (e.key === 'ArrowRight') { e.preventDefault(); navigateTo(curIdx + 1); }
1356
+ if (e.key === 'ArrowLeft') { e.preventDefault(); navigateTo(curIdx - 1); }
1357
+ });
1358
+ </script>
1359
+ </body>
1360
+ </html>