File size: 18,486 Bytes
c03d895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
/* ── Results panel ──────────────────────────────────────────────────── */
.results-panel { container: results / inline-size; }
.results-panel.is-pending .moves-view { pointer-events: none; }
.results-panel.is-loading .moves-view { opacity: .55; pointer-events: none; }
/* The auto-play "stop" control must stay clickable even while a probe is in
   flight - otherwise there's only a brief window between probe cycles where
   auto-play can actually be turned off. */
.results-panel.is-loading .btn--col-autoplay { pointer-events: auto; opacity: 1; }

/* Shown only in the compact table view (see the `results` container query below). */
.metric-tabs { display: none; }

/* ── SSE progress bar ───────────────────────────────────────────────── */
/* Inline next to the panel title, to its right, instead of a full-width
   bar under the panel header - keeps the header height stable and puts
   progress right next to the label it's updating towards. */
.probe-status { align-items: center; gap: 0.625rem; min-width: 0; }
.probe-progress {
    width: 10.5rem;
    height: 0.25rem;
    border-radius: 0.125rem;
    background: var(--border);
    flex-shrink: 0;
    position: relative;
    overflow: hidden;
}
.probe-progress__fill {
    position: absolute;
    left: 0; top: 0; bottom: 0;
    background: linear-gradient(90deg, var(--progress-from), var(--progress-to));
    transition: width 0.3s ease;
    width: 0%;
}
.probe-progress.is-indeterminate .probe-progress__fill {
    width: 35%;
    animation: probe-pending 1.4s ease-in-out infinite;
}
@keyframes probe-pending {
    from { transform: translateX(-100%); }
    to { transform: translateX(290%); }
}
@media (prefers-reduced-motion: reduce) {
    .probe-progress.is-indeterminate .probe-progress__fill { animation: none; left: 32.5%; }
}
.probe-progress-label {
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-family: var(--font-mono);
    white-space: nowrap;
    /* Optical alignment with the adjacent header text. */
    position: relative;
    top: -0.5px;
}
.probe-progress-label:empty {
    display: none;
}
@media (max-width: 767px) {
    .results-panel .panel-header { flex-wrap: wrap; }
    .results-panel .panel-header__start { display: contents; }
    .probe-status { order: 3; flex: 0 0 100%; }
    .probe-status .probe-progress { flex: 1; width: auto; }
}

/* ── Moves view ─────────────────────────────────────────────────────── */
/* The header table is fixed above .moves-body-scroll, the only vertically
   scrolling element of the results panel, so the scrollbar starts at the
   first moves row. Both tables share the column widths below. No scrollbar
   gutter is reserved: while the body overflows, its table is widened by the
   scrollbar's measured width (see .has-vscroll below and syncMovesGutter()
   in engine/scroll.js), which keeps its columns at the header's pixel
   widths. */
.moves-view {
    flex: 1;
    min-height: 17.5rem;
    display: flex;
    flex-direction: column;
}
.moves-head { flex: 0 0 auto; overflow: hidden; }
.moves-body-scroll {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow-x: hidden;
    overflow-y: auto;
}

/* The extra width lies beneath the scrollbar, outside the visible content box. */
.moves-body-scroll.has-vscroll .moves-table--body {
    width: calc(100% + var(--moves-vscroll-width, 0px));
}

/* ── Moves table ────────────────────────────────────────────────────── */
.moves-table {
    width: 100%;
    flex: 0 0 auto;
    border-collapse: collapse;
    /* Fixed column widths (see the col rules below) keep column boundaries,
       and the auto-play buttons in the header, stable as score text length
       changes. */
    table-layout: fixed;
}

.moves-table thead th {
    height: var(--thead-row-h);
    background: var(--surface-2);
}
#root-row td { height: var(--thead-root-h); }

/* Keep move notation readable; score columns share the remaining width. */
.moves-table col.col-rank  { width: 2rem; }
.moves-table col.col-move  { width: 4.25rem; }
.moves-table col.col-score { width: auto; }
.moves-table col.group-dtc.col-score { width: 9rem; }

/* ── Group header row (DTZ / DTC / DTM / DTM50 labels) - centred ────── */
.moves-table thead tr.group-row th {
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-dim);
    text-align: center;           /* centre all group labels */
    padding: 0.375rem var(--space-2) var(--space-1);
    white-space: nowrap;
}
/* Flex row inside the th (not on the th itself - display:flex on a table
   cell suppresses its table-cell box, which breaks the whole table layout
   and stacks the columns). Inline-flex so it still respects text-align. */
.group-row-inner {
    display: inline-flex;
    align-items: center;
    gap: 0.3125rem;
}
/* Left edge on group boundaries - :not(.col-score) so it draws once per
   metric, at its Move cell (or at the group header's own colspan=2 cell),
   not again at the Score cell beside it. */
.moves-table th.group-dtz:not(.col-score),
.moves-table th.group-dtc:not(.col-score),
.moves-table th.group-dtm:not(.col-score),
.moves-table th.group-dtm50:not(.col-score) { box-shadow: inset 1px 0 0 var(--border-2); }

/* ── Column header row (Move / Score labels) ────────────────────────── */
.moves-table thead tr.col-row th {
    font-size: var(--text-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
    text-align: left;
    padding: 0.1875rem var(--space-2) 0.4375rem;
    box-shadow: inset 0 -1px 0 var(--border-2);
    white-space: nowrap;
}
.moves-table thead tr.col-row th.group-dtz:not(.col-score),
.moves-table thead tr.col-row th.group-dtc:not(.col-score),
.moves-table thead tr.col-row th.group-dtm:not(.col-score),
.moves-table thead tr.col-row th.group-dtm50:not(.col-score) {
    box-shadow: inset 1px 0 0 var(--border-2), inset 0 -1px 0 var(--border-2);
}
/* Override group-boundary padding for col-row: add left spacing after
   border - :not(.col-score) for the same reason as the border rules
   above; Score's own right-aligned padding (below) is unaffected either
   way. */
.moves-table thead tr.col-row th.group-dtz:not(.col-score)  { padding-left: var(--space-2); }
.moves-table thead tr.col-row th.group-dtc:not(.col-score)  { padding-left: var(--space-2); }
.moves-table thead tr.col-row th.group-dtm:not(.col-score)  { padding-left: var(--space-2); }
.moves-table thead tr.col-row th.group-dtm50:not(.col-score){ padding-left: var(--space-2); }
/* Move header: tight padding and tracking so "MOVE" fits the narrow move column. */
.moves-table thead tr.col-row th.col-move { padding-right: var(--space-1); letter-spacing: 0.03em; }
.moves-table thead tr.col-row th.col-rank { padding-left: 0; padding-right: 0; text-align: center; text-overflow: clip; letter-spacing: 0; }
/* Score column right-aligned */
.moves-table thead tr.col-row th.col-score { text-align: right; padding-right: 0.875rem; }

/* ── Auto-play buttons - sit in the gap between Move and Score within
   the existing column-header row, instead of a dedicated row ─────────── */
.btn--col-autoplay {
    position: relative;
    height: 1rem;
    min-width: 1.25rem;
    padding: 0 0.3125rem;
    border-radius: 0.1875rem;
    line-height: 1;
    justify-content: center;
    color: var(--text-dim);
}
/* Invisible hit-area expansion - the visible button stays small enough to
   sit inline with the group label, but the clickable/tappable region
   clears a 24px minimum in both dimensions (WCAG 2.5.8 Target Size). */
.btn--col-autoplay::after {
    content: '';
    position: absolute;
    inset: -0.25rem;
}
/* Two icons are always present; CSS toggles which is visible via the
   existing .is-playing class. Both are hand-built SVG rather than text
   glyphs, since a Unicode symbol ('▶'/'⏹') at 0.5625rem is at the mercy of the
   browser/OS's emoji font and renders inconsistently and fuzzily at that
   size. Both use fill: currentColor, so they automatically pick up the
   amber .is-playing colour from the .btn.is-playing rule below with no
   extra colour rule needed here. */
.autoplay-icon {
    width: 0.5625rem;
    height: 0.5625rem;
    fill: currentColor;
}
.autoplay-icon--play { display: block; }
.autoplay-icon--stop { display: none; }
.btn--col-autoplay.is-playing .autoplay-icon--play { display: none; }
.btn--col-autoplay.is-playing .autoplay-icon--stop { display: block; }

/* ── Table body cells ───────────────────────────────────────────────── */
.moves-table td {
    padding: 0.375rem var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
}
.moves-table tbody tr:hover { background: var(--surface-2); }

.moves-table td.col-rank {
    color: var(--text-muted);
    font-size: var(--text-xs);
    text-align: center;
    padding-left: 0.125rem;
    padding-right: 0.125rem;
}

.moves-table td.col-move {
    color: var(--blue);
    font-weight: 500;
    padding-right: var(--space-1);
    cursor: pointer;
}

/* Group boundary edges on td, drawn the same way as the header cells. */
.moves-table td.group-dtz:not(.col-score)  { box-shadow: inset 1px 0 0 var(--border-2); padding-left: var(--space-2); }
.moves-table td.group-dtc:not(.col-score)  { box-shadow: inset 1px 0 0 var(--border-2); padding-left: var(--space-2); }
.moves-table td.group-dtm:not(.col-score)  { box-shadow: inset 1px 0 0 var(--border-2); padding-left: var(--space-2); }
.moves-table td.group-dtm50:not(.col-score){ box-shadow: inset 1px 0 0 var(--border-2); padding-left: var(--space-2); }

.moves-table td.col-score {
    text-align: right;
    font-weight: 500;
    color: var(--text-muted);
    padding-left: var(--space-1);
    padding-right: 0.875rem;
}

/* Outcome colour mapping via data attribute */
.moves-table td.col-score[data-outcome]                { color: var(--text-muted); }
.moves-table td.col-score[data-outcome="win"]          { color: var(--win); }
.moves-table td.col-score[data-outcome="cursed_win"]   { color: var(--amber); }
.moves-table td.col-score[data-outcome="blessed_loss"] { color: var(--amber); }
.moves-table td.col-score[data-outcome="loss"]         { color: var(--loss); }
.moves-table td.col-score[data-outcome="draw"]         { color: var(--draw); }
.moves-table td.col-score.is-mate                      { font-weight: 700; }

.moves-table td.is-empty-cell { color: var(--text-muted); text-align: center; opacity: .35; }

/* ── Root row ───────────────────────────────────────────────────────── */
/* Lives in <thead> (see ResultsPanel.jsx), pinned below the group and
   column rows. The label reuses
   .col-move for identical column alignment/font as a real move - only
   the pointer cursor is dropped, since it isn't a clickable move. Gets
   its own --surface-3 background plus a thin --gold-dim accent bar on
   the leading edge, so it reads as a distinct, pinned reference row
   rather than blending into the header above or the ranked rows below.
   .is-hidden (display:none) is applied by ResultsPanel.jsx when the
   "Show Root Row" setting is off. */
.moves-table td.col-move.is-root {
    cursor: default;
    color: var(--gold);
    font-weight: 600;
}
#root-row td {
    background: var(--surface-3);
    border: 0;
    box-shadow: inset 0 -1px 0 var(--border-2);
}
#root-row td.group-dtz:not(.col-score),
#root-row td.group-dtc:not(.col-score),
#root-row td.group-dtm:not(.col-score),
#root-row td.group-dtm50:not(.col-score) {
    box-shadow: inset 1px 0 0 var(--border-2), inset 0 -1px 0 var(--border-2);
}
#root-row td:first-child {
    box-shadow: inset 3px 0 0 var(--gold-dim), inset 0 -1px 0 var(--border-2);
}
#root-row.is-hidden { display: none; }

/* Pair hover - highlight both move + score cells together */
.moves-table td.is-pair-hover { background: var(--surface-3); }
.moves-table td.is-pair-hover.col-move { color: var(--text); }
.moves-table td.col-move[data-san],
.moves-table td.col-score[data-san] {
    cursor: pointer;
    transition: color var(--tr), background var(--tr);
}
.moves-table td.col-move[data-san]:active  { color: var(--gold); background: var(--surface-3); }
.moves-table td.col-score[data-san]:active { background: var(--surface-3); }

/* ── Info dot / warning dot ────────────────────────────────────────────
   .info-dot sits after the "Draw" score text for a move whose draw is
   caused by insufficient material - the reason is a hover tooltip
   (native title attribute) instead of being spelled out inline, so the
   score column never has to fit "Draw by Insufficient Material". With
   touch or pen input, a tap on a dot shows the same text as a notice.
   Where a coarse pointer is available, each dot carries a transparent
   --touch-target wide box spanning the full height of its score cell,
   centered on the icon and clipped to the cell.
   .warning-dot sits in the same spot for a move flagged cursed win or
   blessed loss. Both share identical layout, icon, and colours (see
   DotIcon in ResultsPanel.jsx, which renders both) - only the hover
   title tells the two apart, so their rules are declared together
   below. */
.info-dot, .warning-dot {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 0.8125rem; height: 0.8125rem;
    margin-left: var(--space-1);
    cursor: help;
    vertical-align: middle;
    flex-shrink: 0;
}
@media (any-pointer: coarse) {
    .moves-table td.col-score { position: relative; }
    .info-dot::after, .warning-dot::after {
        content: '';
        position: absolute;
        top: 0;
        bottom: 0;
        width: var(--touch-target);
    }
}
.info-dot__icon, .warning-dot__icon {
    width: 100%;
    height: 100%;
    display: block;
    overflow: visible;
}
.info-dot__bg,   .warning-dot__bg   { fill: var(--text-dim); }
.info-dot__mark, .warning-dot__mark { fill: var(--surface); }

/* ── Empty state ────────────────────────────────────────────────────── */
.moves-empty-state {
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding: 3rem var(--space-5);
    color: var(--text-muted);
    text-align: center;
    font-size: var(--text-xs);
    flex: 1;
}
.moves-empty-state__icon { font-size: 1.75rem; opacity: .18; }

/* ── Loading shimmer ────────────────────────────────────────────────── */
@keyframes shimmer {
    0%, 100% { opacity: 0.3; }
    50%       { opacity: 0.65; }
}
.is-loading .moves-table td.col-score,
.is-loading .moves-table td.col-move { animation: shimmer 1.1s ease-in-out infinite; }

/* ── Compact view ───────────────────────────────────────────────────────
   Below 49rem the nine-column table is replaced by a three-column one
   so distances, units and annotations fit without wrapping. At 49rem,
   the 2rem rank + four 4.25rem moves + 9rem DTC score leave 7rem for
   each other score, enough for "Not Available" or five-digit plies
   with a warning dot and cell padding.
   (rank, move, score) for the metric chosen in .metric-tabs. */
@container results (width < 49rem) {
    .metric-tabs {
        display: grid;
        grid-template-columns: repeat(4, minmax(0, 1fr));
        min-height: 2rem;
    }
    .metric-tab {
        min-width: 0;
        padding: 0 var(--space-1);
        border: 0 solid var(--border);
        border-bottom-width: 1px;
        border-radius: 0;
        background: var(--surface-2);
        color: var(--text-dim);
        font: 600 var(--text-xs) var(--font-ui);
        letter-spacing: 0.03em;
    }
    .metric-tab + .metric-tab { border-left: 1px solid var(--border); }
    .metric-tab.is-active {
        color: var(--gold);
        background: var(--gold-glow);
        box-shadow: inset 0 0 0 1px var(--gold-dim);
    }
    .metric-tab:focus-visible { outline-offset: -2px; }

    .moves-table[data-active-metric] .group-dtz,
    .moves-table[data-active-metric] .group-dtc,
    .moves-table[data-active-metric] .group-dtm,
    .moves-table[data-active-metric] .group-dtm50 { display: none; }
    .moves-table[data-active-metric="dtz"]   .group-dtz,
    .moves-table[data-active-metric="dtc"]   .group-dtc,
    .moves-table[data-active-metric="dtm"]   .group-dtm,
    .moves-table[data-active-metric="dtm50"] .group-dtm50 { display: table-cell; }

    .moves-table[data-active-metric="dtz"] th.group-dtz:not(.col-score),
    .moves-table[data-active-metric="dtz"] td.group-dtz:not(.col-score) { padding-left: 0.625rem; }

    .moves-table[data-active-metric] col.group-dtz,
    .moves-table[data-active-metric] col.group-dtc,
    .moves-table[data-active-metric] col.group-dtm,
    .moves-table[data-active-metric] col.group-dtm50 { display: none; }
    .moves-table[data-active-metric] col.col-rank { width: 14%; }
    .moves-table[data-active-metric="dtz"]   col.group-dtz,
    .moves-table[data-active-metric="dtc"]   col.group-dtc,
    .moves-table[data-active-metric="dtm"]   col.group-dtm,
    .moves-table[data-active-metric="dtm50"] col.group-dtm50 { display: table-column; }
    .moves-table[data-active-metric] col.col-move  { width: 26%; }
    .moves-table[data-active-metric] col.col-score { width: 60%; }
}