/* ── 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 (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%; } }