Files
HexTrails/labs/bench/style.css
T
cabinwandClaude Opus 5 47be0ad9c4 feat(bench): live observer page
Watch an agent play in real time, replayed with the main game's renderer and
animations. The board holds the stage with the live panel beside it: a
millisecond stopwatch, how long the current step has been thinking, steps,
decisions, pieces cleared, dead taps and rotations, then the agent's running
commentary. A banner over the board names who is playing, self-reported by the
agent via --agent.

The page polls unconditionally; the checkbox only decides whether the cursor
chases the newest step, and it follows the agent into its next run once this
one ends. Clicking any step in the feed replays the board at that moment, and
the arrow keys walk it — there is no scrub bar, since the feed already is one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:00:33 +08:00

170 lines
11 KiB
CSS

:root{
/* ink3 is the smallest type on the page; it has to clear 4.5:1 on white. */
--ink:#1a2430; --ink2:#516678; --ink3:#647385; --ink4:#5a6974;
/* One source of truth for the two-column width, so the panels below the
stage line up with it instead of running the full window. */
--rail:clamp(270px,25vw,340px);
--boardmax:calc((100svh - 205px) * .92);
--stage-w:calc(var(--boardmax) + var(--rail) + 16px);
--page:#eceff2; --card:#fff; --line:#d6dee5; --line2:#c2ccd6;
/* --live fills bars and the dark board; --live-ink is the readable text version. */
--accent:#3f6d95; --live:#2ea36a; --live-ink:#1f7d50; --warn:#c2762a; --bad:#b8412b;
--board-bg:#11181f; --board-ink:#e8eef4; --board-dim:#8798a8;
font-family:system-ui,-apple-system,"PingFang SC",sans-serif;color:var(--ink);background:var(--page);font-synthesis:none;
}
*{box-sizing:border-box}
body{margin:0;padding-bottom:28px}
h1,h2{margin:0}
code{font-family:ui-monospace,Menlo,monospace;font-size:.92em;background:#e2e8ee;padding:1px 4px;border-radius:3px}
/* ---------- top bar ---------- */
.topbar{display:flex;align-items:center;gap:14px;padding:10px 20px;background:var(--card);border-bottom:1px solid var(--line);flex-wrap:wrap}
.back{color:var(--ink2);text-decoration:none;font-size:13px;white-space:nowrap}
.badge{font-size:10px;letter-spacing:.14em;color:var(--ink3);border:1px solid var(--line2);border-radius:20px;padding:5px 11px;margin-left:auto;white-space:nowrap}
.picker{display:flex;align-items:center;gap:9px;flex:1;min-width:280px;font-size:12px;color:var(--ink2)}
.picker select{font:13px system-ui;padding:6px 9px;border:1px solid var(--line2);border-radius:8px;background:#fff;color:var(--ink);flex:1;min-width:0;max-width:560px}
.toggle{display:inline-flex;align-items:center;gap:6px;cursor:pointer;white-space:nowrap}
input[type=checkbox]{accent-color:var(--accent)}
button{font:14px system-ui;color:var(--ink2);background:#f7fafc;border:1px solid var(--line2);border-radius:10px;padding:7px 12px;cursor:pointer}
button:hover{background:#fff;color:var(--ink)}
button:disabled{opacity:.4;cursor:default}
button.small{font-size:12px;padding:5px 10px;border-radius:8px}
button:focus-visible,a:focus-visible,select:focus-visible,input:focus-visible,.steps:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
/* Reachable by screen readers, invisible on screen. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
/* ---------- stage: board left, live rail right ---------- */
/* The board is a portrait hexagon, so its card is height-bound: cap the board
column near the height it can actually use and centre the pair, instead of
stretching a card whose middle 40% would be empty. */
.stage{display:grid;grid-template-columns:minmax(0,var(--boardmax)) var(--rail);
gap:16px;padding:16px 20px 0;align-items:stretch;justify-content:center}
.board-col{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px;display:flex;flex-direction:column;min-width:0}
/* The canvas is taken out of flow on purpose. Left in flow, `height:100%`
degrades to auto during intrinsic sizing and the box falls back to the
canvas *attribute* ratio — which resize() writes from this very box. That
loop let a detour through a short window permanently inflate the card. */
.board-wrap{flex:1;width:100%;position:relative;min-height:clamp(340px,calc(100svh - 235px),760px)}
canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
/* Only surfaces when a run's JSON can't be read — otherwise it takes no space. */
.notice{margin:10px 0 0;text-align:center;font-size:13px;line-height:1.6;color:var(--bad)}
/* 谁在玩:棋盘正上方的身份横幅 */
.who{display:flex;align-items:baseline;justify-content:space-between;gap:12px;flex-wrap:wrap;
padding:0 2px 10px;margin-bottom:10px;border-bottom:1px solid var(--line)}
.who-id{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;min-width:0}
.who-id b{font-size:19px;font-weight:700;letter-spacing:-.01em}
/* Agents self-report free text, so assume the worst: a slug or user-agent
string with no spaces has to break rather than push the page sideways. */
.who-agent{font-size:13px;color:var(--accent);font-weight:600;min-width:0;overflow-wrap:anywhere}
/* Hang the separator off the model name. As ::before on the agent it wrapped
to the start of the next line and read like a bullet. */
.who-id:has(.who-agent:not(:empty)) b::after{content:'·';margin-left:10px;color:var(--ink3);font-weight:400}
.who-level{font-size:11.5px;color:var(--ink3);font-variant-numeric:tabular-nums}
.rail{display:flex;flex-direction:column;gap:16px;min-width:0;min-height:0}
/* ---------- scoreboard: a vertical panel beside the board ---------- */
.scoreboard{background:var(--board-bg);color:var(--board-ink);border-radius:14px;padding:16px;
display:flex;flex-direction:column;gap:13px;flex:0 0 auto;transition:opacity .2s}
/* Rewound: the clock and the totals belong to the whole run, so they no longer
describe the board sitting right next to them. Say so instead of lying. */
.scoreboard.rewound{opacity:.55}
@media(prefers-reduced-motion:reduce){.scoreboard{transition:none}}
.scoreboard .clock{display:flex;flex-direction:column;gap:4px;min-width:0}
.scoreboard .k{font-size:10px;letter-spacing:.12em;color:var(--board-dim);text-transform:uppercase;white-space:nowrap}
.scoreboard b{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-variant-numeric:tabular-nums;font-weight:600;line-height:1;font-size:19px}
.scoreboard small{font-size:11px;color:var(--board-dim);margin-left:4px;font-family:ui-monospace,Menlo,monospace}
.clock.primary b{font-size:clamp(32px,3.1vw,42px);letter-spacing:-.02em}
.clock.think b{font-size:23px;color:#f0c674}
.tick.hot{color:#ffcf6b}
/* Each readout is a wrapping row: the label owns a line, the number keeps its
suffix beside it. Stacking all three (a flex column blockifies `inline`)
cost a third line per cell and made the panel needlessly tall. */
.scoreboard .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px 12px;border-top:1px solid #27323d;padding-top:13px}
.scoreboard .cell{display:flex;flex-wrap:wrap;align-items:baseline;min-width:0}
.scoreboard .cell .k{flex:0 0 100%;margin-bottom:4px}
.scoreboard .bar{height:5px;background:#27323d;border-radius:3px;overflow:hidden}
.scoreboard .bar i{display:block;height:100%;background:var(--live);width:0;transition:width .3s}
.pill{align-self:flex-start;font-size:12px;font-weight:600;letter-spacing:.04em;padding:7px 14px;border-radius:20px;white-space:nowrap;background:#25303b;color:var(--board-dim)}
.pill.rewound{background:#2b3742;color:#c7d4de}
.pill.playing{background:rgba(46,163,106,.18);color:#5ed69b}
.pill.playing::before{content:'●';margin-right:6px;animation:blink 1.4s infinite}
.pill.cleared{background:rgba(46,163,106,.25);color:#7fe6b4}
.pill.gaveup{background:rgba(184,65,43,.22);color:#ff9b85}
.pill.budget{background:rgba(194,118,42,.22);color:#f3c98a}
@keyframes blink{50%{opacity:.25}}
/* ---------- live feed ---------- */
.feed-col{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px;display:flex;flex-direction:column;flex:1 1 auto;min-height:200px}
/* The button and the live state are siblings of the heading, not inside it —
nested, they became part of the heading's accessible name. */
.feed-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px}
.feed-col h2{font-size:11px;letter-spacing:.16em;color:var(--ink3);text-transform:uppercase}
.feed-state{font-size:10.5px;letter-spacing:.02em;color:var(--live-ink);font-weight:600}
.back-live{margin-left:auto;color:var(--accent);border-color:var(--accent)}
.steps{margin:0;padding:0;list-style:none;overflow-y:auto;flex:1 1 0;min-height:0;font-size:12.5px;counter-reset:step}
.steps li{padding:8px 10px 8px 32px;border-bottom:1px solid #eef2f5;position:relative;cursor:pointer;line-height:1.55;counter-increment:step}
.steps li::before{content:counter(step);position:absolute;left:8px;top:8px;font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:var(--ink3)}
.steps li:last-child{border-bottom:none}
.steps li.lead{border-top:1px solid var(--line);margin-top:2px}
.steps li:first-child.lead{border-top:none;margin-top:0}
.steps li b{font-weight:600}
.steps li i{display:block;color:var(--ink2);font-style:normal;font-size:11.5px;margin-top:2px}
.steps li em{float:right;margin-left:8px;font-style:normal;font-size:10.5px;color:var(--accent);background:#eaf1f7;border-radius:9px;padding:1px 7px;font-variant-numeric:tabular-nums;font-weight:600}
.steps li.current{background:#eaf1f7;box-shadow:inset 3px 0 0 var(--accent)}
.steps li:hover{background:#f2f6f9}
/* ---------- details below ---------- */
.details{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;padding:16px 20px 0;
width:100%;max-width:calc(var(--stage-w) + 40px);margin-inline:auto}
.panel{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px}
.panel h2{font-size:11px;letter-spacing:.16em;color:var(--ink3);text-transform:uppercase;margin-bottom:10px}
.kv{margin:0;font-size:12.5px}
.kv>div{display:flex;justify-content:space-between;gap:10px;padding:5px 0;border-bottom:1px dashed #e4eaef}
.kv>div:last-child{border-bottom:none}
.kv dt{color:var(--ink3)}
.kv dd{margin:0;font-variant-numeric:tabular-nums;text-align:right;color:var(--ink);font-weight:600}
.kv .giveup dd{color:var(--bad);text-align:left;font-weight:400}
footer{text-align:center;margin:20px auto 0;max-width:760px;padding:0 20px;font-size:11px;color:var(--ink4);line-height:1.7}
/* The rail needs ~260px to stay readable; below that it goes under the board,
where the clock turns into a wide two-up row instead of a tall column. */
@media(max-width:820px){
.stage{grid-template-columns:1fr;align-items:start}
/* Stacked, the card is full width, so the board should be tall enough to
use it — 62vw left a 603px canvas holding a 330px board. */
.board-wrap{min-height:clamp(280px,min(96vw,64svh),680px)}
.scoreboard{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:12px 18px}
.scoreboard .clock.primary{grid-column:1}
.scoreboard .clock.think{grid-column:2}
.clock.primary b{font-size:38px}
.scoreboard .stats{grid-column:1 / -1;grid-template-columns:repeat(4,minmax(0,1fr))}
.scoreboard .bar,.pill{grid-column:1 / -1}
/* Stacked, the rail's height comes from its content, so `flex:1 1 0` resolved
to zero and the list vanished — taking the only way to replay a step with
it. Give the list its own height instead of a share of a leftover. */
.feed-col{min-height:0}
.steps{flex:0 0 auto;max-height:min(340px,52svh)}
}
@media(max-width:520px){
.topbar{padding:8px 12px;gap:8px}
.picker{min-width:100%;order:3}
.picker select{max-width:none}
.stage,.details{padding-left:12px;padding-right:12px}
.board-col,.feed-col,.panel{padding:12px}
.scoreboard{padding:14px;gap:11px 14px}
.clock.primary b{font-size:34px}
.clock.think b{font-size:20px}
.scoreboard .stats{grid-template-columns:1fr 1fr}
.who-id b{font-size:17px}
}
/* Below ~400px the two clocks no longer fit on one row: 「本步思考」 gets about
94px and its mono value needs 96. Stack them rather than clip the digits. */
@media(max-width:400px){
.scoreboard .clock.primary,.scoreboard .clock.think{grid-column:1 / -1}
.clock.think b{font-size:22px}
}
@media(prefers-reduced-motion:reduce){.pill.playing::before{animation:none}.scoreboard .bar i{transition:none}}