explorer / frontend /src /styles /chess /tablebase.css
noobpwnftw's picture
Deploy ChessTB Explorer on Hugging Face Spaces
e16348e
Raw History Blame
18.5 kB
/* ── 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%; }
}