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>
170 lines
11 KiB
CSS
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}}
|