@ucsandman/legcli 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +560 -0
- package/LICENSE +58 -0
- package/NOTICE +4 -0
- package/README.md +558 -0
- package/bin/fake-agent.mjs +143 -0
- package/bin/leg.mjs +449 -0
- package/docs/DECISIONS.md +119 -0
- package/docs/DEMO.md +214 -0
- package/docs/DEVIATIONS.md +182 -0
- package/docs/ERRORS.md +326 -0
- package/docs/README.md +40 -0
- package/docs/REUSE.md +289 -0
- package/docs/ROADMAP-v2.md +55 -0
- package/docs/VOCABULARY.md +243 -0
- package/docs/adapters.md +300 -0
- package/docs/board-guide.md +579 -0
- package/docs/cli-contracts.md +401 -0
- package/docs/concepts.md +426 -0
- package/docs/configuration.md +220 -0
- package/docs/faq.md +206 -0
- package/docs/getting-started.md +251 -0
- package/docs/real-run.md +80 -0
- package/docs/review-2026-09-14.md +72 -0
- package/docs/screenshots/board-400px.png +0 -0
- package/docs/screenshots/board-details-open.png +0 -0
- package/docs/screenshots/board-done.png +0 -0
- package/docs/screenshots/board-drawer.png +0 -0
- package/docs/screenshots/board-empty.png +0 -0
- package/docs/screenshots/board-handoff.png +0 -0
- package/docs/screenshots/board-running.png +0 -0
- package/docs/screenshots/demo-1-claude-running.png +0 -0
- package/docs/screenshots/demo-2-limit-hit.png +0 -0
- package/docs/screenshots/demo-3-handoff-bundle.png +0 -0
- package/docs/screenshots/demo-4-codex-running.png +0 -0
- package/docs/screenshots/demo-5-done.png +0 -0
- package/docs/screenshots/floor-final-1280.png +0 -0
- package/docs/screenshots/floor-final-400.png +0 -0
- package/docs/screenshots/floor-landing.png +0 -0
- package/docs/screenshots/floor.png +0 -0
- package/docs/screenshots/share-guest-1280.png +0 -0
- package/docs/screenshots/share-owner-1280.png +0 -0
- package/docs/screenshots/terminals-1280.png +0 -0
- package/docs/screenshots/thumb-done.jpg +0 -0
- package/docs/screenshots/thumb-handoff-details.jpg +0 -0
- package/docs/screenshots/thumb-limit-hit.jpg +0 -0
- package/docs/screenshots/youtube-thumbnail.jpg +0 -0
- package/fixtures/limits/agy/agy-resource-exhausted.json +11 -0
- package/fixtures/limits/claude/claude-budget-limit.json +11 -0
- package/fixtures/limits/claude/claude-max-turns.json +11 -0
- package/fixtures/limits/claude/claude-model-limit.json +11 -0
- package/fixtures/limits/claude/claude-session-limit.json +11 -0
- package/fixtures/limits/claude/claude-weekly-limit.json +11 -0
- package/fixtures/limits/codex/codex-quota-exceeded.json +11 -0
- package/fixtures/limits/codex/codex-rate-limit-exceeded.json +11 -0
- package/fixtures/limits/codex/codex-skills-notice.json +11 -0
- package/fixtures/limits/codex/codex-usage-limit-reached.json +11 -0
- package/fixtures/limits/codex/codex-usage-limit.json +11 -0
- package/fixtures/limits/generic/generic-429.json +11 -0
- package/fixtures/limits/generic/generic-overloaded.json +11 -0
- package/fixtures/limits/generic/generic-quota.json +11 -0
- package/fixtures/limits/generic/generic-rate-limit.json +11 -0
- package/fixtures/limits/generic/generic-resource-exhausted.json +11 -0
- package/fixtures/limits/generic/generic-usage-limit.json +11 -0
- package/fixtures/limits/grok/grok-not-logged-in.json +11 -0
- package/fixtures/limits/negative/auth-source-set.json +11 -0
- package/fixtures/limits/negative/compile-error.json +11 -0
- package/fixtures/limits/negative/empty-stdout-exit-0.json +11 -0
- package/fixtures/live/agy/attempt-1-scratch-workspace.out.log +1 -0
- package/fixtures/live/agy/cmd.txt +1 -0
- package/fixtures/live/agy/err.log +0 -0
- package/fixtures/live/agy/out.log +1 -0
- package/fixtures/live/agy/parsed.json +5 -0
- package/fixtures/live/agy/run.json +18 -0
- package/fixtures/live/agy/supervisor.log +2 -0
- package/fixtures/live/claude/cmd.txt +1 -0
- package/fixtures/live/claude/err.log +0 -0
- package/fixtures/live/claude/limit-rate_limit.json +21 -0
- package/fixtures/live/claude/out.log +1 -0
- package/fixtures/live/claude/parsed.json +11 -0
- package/fixtures/live/claude/run.json +18 -0
- package/fixtures/live/claude/supervisor.log +2 -0
- package/fixtures/live/codex/cmd.txt +1 -0
- package/fixtures/live/codex/err.log +1 -0
- package/fixtures/live/codex/last.md +2 -0
- package/fixtures/live/codex/limit-usage_limit_exceeded.json +25 -0
- package/fixtures/live/codex/out.log +8 -0
- package/fixtures/live/codex/parsed.json +15 -0
- package/fixtures/live/codex/run.json +18 -0
- package/fixtures/live/codex/supervisor.log +2 -0
- package/fixtures/live/grok/cmd.txt +1 -0
- package/fixtures/live/grok/err.log +32 -0
- package/fixtures/live/grok/out.log +7 -0
- package/fixtures/live/grok/parsed.json +5 -0
- package/fixtures/live/grok/run.json +18 -0
- package/fixtures/live/grok/supervisor.log +2 -0
- package/fixtures/verified.json +34 -0
- package/package.json +68 -0
- package/scripts/board-shots.mjs +69 -0
- package/scripts/build-docs-site.mjs +308 -0
- package/scripts/check-claims.mjs +150 -0
- package/scripts/clean-clone-check.sh +42 -0
- package/scripts/license-sign.mjs +23 -0
- package/scripts/limits-table.mjs +38 -0
- package/scripts/live-limits.mjs +36 -0
- package/scripts/privacy-check.mjs +63 -0
- package/scripts/probe.mjs +92 -0
- package/scripts/seed-fake-cards.mjs +34 -0
- package/scripts/seed-floor-board.mjs +69 -0
- package/scripts/seed-wes-board.mjs +91 -0
- package/scripts/stripe-setup.mjs +99 -0
- package/scripts/vercel-env.mjs +40 -0
- package/src/accounts.mjs +119 -0
- package/src/adapters/agy.mjs +56 -0
- package/src/adapters/claude.mjs +54 -0
- package/src/adapters/codex.mjs +73 -0
- package/src/adapters/common.mjs +32 -0
- package/src/adapters/fake.mjs +48 -0
- package/src/adapters/grok.mjs +51 -0
- package/src/adapters/index.mjs +27 -0
- package/src/adapters/resolve.mjs +40 -0
- package/src/attach.mjs +592 -0
- package/src/auth.mjs +85 -0
- package/src/board/board.css +543 -0
- package/src/board/board.js +1307 -0
- package/src/board/favicon.svg +1 -0
- package/src/board/floor.html +127 -0
- package/src/board/floor.js +670 -0
- package/src/board/fonts/OFL-atkinson-hyperlegible-next.txt +93 -0
- package/src/board/fonts/OFL-azeret-mono.txt +94 -0
- package/src/board/fonts/atkinson-board.woff2 +0 -0
- package/src/board/fonts/azeret-board.woff2 +0 -0
- package/src/board/index.html +251 -0
- package/src/board/sessions.js +1451 -0
- package/src/bundle.mjs +103 -0
- package/src/cards.mjs +114 -0
- package/src/chain.mjs +264 -0
- package/src/commands.mjs +79 -0
- package/src/contract.mjs +73 -0
- package/src/env.mjs +19 -0
- package/src/fsx.mjs +72 -0
- package/src/git-snapshot.mjs +152 -0
- package/src/handoff.mjs +171 -0
- package/src/hook.mjs +49 -0
- package/src/land.mjs +106 -0
- package/src/launcher.mjs +272 -0
- package/src/leases.mjs +86 -0
- package/src/ledger.mjs +369 -0
- package/src/license.mjs +168 -0
- package/src/limits.mjs +97 -0
- package/src/live-capture.mjs +69 -0
- package/src/mergequeue.mjs +225 -0
- package/src/orchestrator.mjs +365 -0
- package/src/pipeline.mjs +100 -0
- package/src/preferences.mjs +47 -0
- package/src/presets.mjs +23 -0
- package/src/ratelimit.mjs +49 -0
- package/src/redact.mjs +47 -0
- package/src/resume.mjs +354 -0
- package/src/runner.mjs +438 -0
- package/src/scheduler.mjs +118 -0
- package/src/server.mjs +845 -0
- package/src/session-detail.mjs +129 -0
- package/src/sessions.mjs +202 -0
- package/src/share.mjs +163 -0
- package/src/stations/agent.mjs +42 -0
- package/src/stations/build.mjs +9 -0
- package/src/stations/human.mjs +5 -0
- package/src/stations/land.mjs +6 -0
- package/src/stations/plan.mjs +8 -0
- package/src/stations/pr.mjs +31 -0
- package/src/stations/review.mjs +8 -0
- package/src/stations/test.mjs +27 -0
- package/src/store.mjs +90 -0
- package/src/sync/dashclaw.mjs +70 -0
- package/src/sync/index.mjs +54 -0
- package/src/sync/workboard.mjs +54 -0
- package/src/taps/agy.mjs +64 -0
- package/src/taps/claude-usage.mjs +67 -0
- package/src/taps/claude.mjs +170 -0
- package/src/taps/codex.mjs +286 -0
- package/src/trust.mjs +323 -0
- package/src/usage.mjs +179 -0
- package/src/wait.mjs +30 -0
- package/src/worktree.mjs +207 -0
package/src/auth.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// auth — who is allowed in. Loopback with no token is open; any other bind
|
|
2
|
+
// address refuses to start without a token, and with a token every /api
|
|
3
|
+
// request needs `Authorization: Bearer <token>` (timing-safe). With `baton
|
|
4
|
+
// share` on, each human has their own token and the board knows their name
|
|
5
|
+
// and role (src/share.mjs); a loopback request is still the owner, so the
|
|
6
|
+
// machine's own browser needs nothing.
|
|
7
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
8
|
+
import { identify, isOn as shareIsOn, personNamed } from './share.mjs'
|
|
9
|
+
|
|
10
|
+
export const LOOPBACK = ['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1']
|
|
11
|
+
|
|
12
|
+
export function isLoopback(bind) {
|
|
13
|
+
return LOOPBACK.includes(String(bind ?? '').trim().toLowerCase().replace(/^\[|\]$/g, ''))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Tokenless owner access is safe only when both ends name loopback. Checking
|
|
17
|
+
// the socket alone lets an attacker-controlled DNS name rebind to 127.0.0.1;
|
|
18
|
+
// its page then has a same-origin Host/Origin pair and can drive the board.
|
|
19
|
+
export function isLoopbackRequest(req) {
|
|
20
|
+
if (!isLoopback(remoteAddress(req))) return false
|
|
21
|
+
const host = req?.headers?.host
|
|
22
|
+
if (typeof host !== 'string' || !host) return false
|
|
23
|
+
try {
|
|
24
|
+
const target = new URL(`http://${host}`)
|
|
25
|
+
return isLoopback(target.hostname)
|
|
26
|
+
} catch { return false }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class BindRefused extends Error {
|
|
30
|
+
constructor(bind) {
|
|
31
|
+
super(`refusing to bind ${bind} without LEG_TOKEN; see README "Network exposure"`)
|
|
32
|
+
this.name = 'BindRefused'
|
|
33
|
+
this.exitCode = 3
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function checkBind({ bind, token, share = null }) {
|
|
38
|
+
// share on means every request carries a personal token, so the bind is guarded
|
|
39
|
+
if (!isLoopback(bind) && !token && !shareIsOn(share ?? undefined)) throw new BindRefused(bind)
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function remoteAddress(req) { return req?.socket?.remoteAddress ?? req?.connection?.remoteAddress ?? '' }
|
|
44
|
+
|
|
45
|
+
export function tokenMatches(token, presented) {
|
|
46
|
+
if (!token || typeof presented !== 'string') return false
|
|
47
|
+
const a = Buffer.from(token)
|
|
48
|
+
const b = Buffer.from(presented)
|
|
49
|
+
if (a.length !== b.length) return false
|
|
50
|
+
return timingSafeEqual(a, b)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Bearer header, or ?token= for EventSource (which cannot set headers).
|
|
54
|
+
export function presentedToken(req, url) {
|
|
55
|
+
const h = req.headers.authorization
|
|
56
|
+
if (h && /^Bearer\s+/i.test(h)) return h.replace(/^Bearer\s+/i, '').trim()
|
|
57
|
+
return url.searchParams.get('token')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// → { ok, subject, person } where person is { name, role } when share is on.
|
|
61
|
+
export function authorize({ token, req, url, share = null, bind = null, loopbackOwner = null }) {
|
|
62
|
+
const presented = presentedToken(req, url)
|
|
63
|
+
if (shareIsOn(share ?? undefined)) {
|
|
64
|
+
const person = identify(share, presented)
|
|
65
|
+
if (person) return { ok: true, subject: person.name, person }
|
|
66
|
+
// the machine's own browser is the owner over LOOPBACK only: with a
|
|
67
|
+
// non-loopback (Tailscale/LAN) bind the server also listens on 127.0.0.1
|
|
68
|
+
// (createBoardServer), so the owner opens the loopback URL tokenless while a
|
|
69
|
+
// real remote peer's address is never loopback and always needs a token.
|
|
70
|
+
const localOwner = loopbackOwner === null ? isLoopback(remoteAddress(req)) : loopbackOwner
|
|
71
|
+
if (!presented && share.loopback_owner !== false && localOwner) {
|
|
72
|
+
const owner = personNamed(share, share.owner) ?? share.people.find((p) => p.role === 'owner') ?? null
|
|
73
|
+
if (owner) return { ok: true, subject: owner.name, person: owner }
|
|
74
|
+
}
|
|
75
|
+
return { ok: false, subject: null, person: null }
|
|
76
|
+
}
|
|
77
|
+
// checkBind's invariant, re-applied per request: share.json stops reading as
|
|
78
|
+
// on (`baton share off`, a truncated file) while the shared address is still
|
|
79
|
+
// bound, and nobody but this machine may be the local owner in that window
|
|
80
|
+
if (!token && loopbackOwner === false) return { ok: false, subject: null, person: null }
|
|
81
|
+
if (!token && bind && !isLoopback(bind) && !isLoopback(remoteAddress(req))) return { ok: false, subject: null, person: null }
|
|
82
|
+
if (!token) return { ok: true, subject: 'local', person: null }
|
|
83
|
+
if (tokenMatches(token, presented)) return { ok: true, subject: 'token', person: null }
|
|
84
|
+
return { ok: false, subject: null, person: null }
|
|
85
|
+
}
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/* Leg board: dark-only operator UI. Shared by index.html and floor.html.
|
|
2
|
+
*
|
|
3
|
+
* The design this implements is .design/BOARD-V4-DIRECTION.md: mock B,
|
|
4
|
+
* "dark product surface", with C's headline framing and A's staleness
|
|
5
|
+
* sentence. It replaces the printed-operator's-manual brief that produced six
|
|
6
|
+
* rejected rounds. The shape of that brief was hairline rules as the only
|
|
7
|
+
* structure, a flat single plane, 2px radii, a saturated navy ground and a
|
|
8
|
+
* monospace face for prose. None of it survives here on purpose.
|
|
9
|
+
*
|
|
10
|
+
* What the design is, in one line each:
|
|
11
|
+
* - the ground is neutral near-black, and the panels are raised objects on
|
|
12
|
+
* it, lit from above with a 1px top highlight and a shadow beneath
|
|
13
|
+
* - size encodes importance: the login that is the problem gets the wide
|
|
14
|
+
* lit panel, the ones that are not get half panels under it
|
|
15
|
+
* - the largest thing on the board is a sentence, not a number
|
|
16
|
+
* - one accent, one blue, on the primary action and nowhere else
|
|
17
|
+
* - severity paints inside a gauge track and marks a state dot; it is never
|
|
18
|
+
* the colour of a word
|
|
19
|
+
* - history sits on the ground, unpanelled, so it cannot compete with what
|
|
20
|
+
* is live
|
|
21
|
+
*
|
|
22
|
+
* ---------------------------------------------------------------------------
|
|
23
|
+
* SHAPES THE TEST SUITE PINS. Each item names the test file that goes red if
|
|
24
|
+
* the shape moves. Read that test before you change the shape it describes.
|
|
25
|
+
* ---------------------------------------------------------------------------
|
|
26
|
+
*
|
|
27
|
+
* 1. The token block below ships HEX values, with the OKLCH the value was
|
|
28
|
+
* designed in written after the semicolon on the same line.
|
|
29
|
+
* test/board-a11y.test.mjs hand-parses that block with a flat regex,
|
|
30
|
+
* resolves var() exactly one level, and converts each value with
|
|
31
|
+
* parseInt on the first six hex digits. A bare oklch() value converts to
|
|
32
|
+
* NaN and the contrast math then fails without saying why. Keep the
|
|
33
|
+
* values hex. Text after the semicolon is outside the captured value,
|
|
34
|
+
* which is what makes the OKLCH comment safe. Two further rules for that
|
|
35
|
+
* block: no comment inside it may contain a semicolon, and none may
|
|
36
|
+
* contain the word color followed by a colon, because either one is read
|
|
37
|
+
* as a declaration.
|
|
38
|
+
* 2. The literal two-selector rule for the html and body elements, carrying
|
|
39
|
+
* both a text colour and a background. board-a11y finds it by matching
|
|
40
|
+
* the trimmed selector exactly, so no comment may sit between the
|
|
41
|
+
* previous rule's closing brace and that selector.
|
|
42
|
+
* 3. At least six rules whose selector contains "chip" or "pill" and that
|
|
43
|
+
* set a text colour, none of them inside a media or keyframes block,
|
|
44
|
+
* which the flat parser does not descend into. board-a11y measures each
|
|
45
|
+
* against var(--panel-2) and needs 3.0:1. In this design .chip is a flat
|
|
46
|
+
* inline data token, not a capsule: the test checks its contrast, not its
|
|
47
|
+
* shape, so the name is load-bearing and is kept honestly.
|
|
48
|
+
* 4. The focus ring. board-a11y requires the focus-visible pseudo-class
|
|
49
|
+
* followed, within eighty characters, by a declaration of a two-pixel
|
|
50
|
+
* solid outline. A box-shadow ring, or the colour split onto its own
|
|
51
|
+
* line, breaks the match. There are two such rules in this file.
|
|
52
|
+
* 5. The [hidden] attribute rule, set to display none with !important,
|
|
53
|
+
* verbatim. test/server.test.mjs matches it in the served /board.css body.
|
|
54
|
+
* 6. The reduced-motion block, keyed on prefers-reduced-motion reduce.
|
|
55
|
+
* board-a11y finds that at-rule by its exact text and brace-matches it,
|
|
56
|
+
* then requires a selector containing state-active followed by an
|
|
57
|
+
* animation of none, and a universal rule setting transition to none.
|
|
58
|
+
* 7. Four scroll boxes, each with its class as the FIRST TOKEN ON ITS LINE:
|
|
59
|
+
* .drawer-task, then .drawer-msg p, then .drawer-timeline, then
|
|
60
|
+
* .drawer-diff. Each rule carries overflow auto or overflow-y auto.
|
|
61
|
+
* test/board-drawer.test.mjs finds them by scanning lines for that
|
|
62
|
+
* prefix, not by parsing CSS. Do not merge them onto one line, do not
|
|
63
|
+
* reformat them, and do not let an earlier line in this file begin with
|
|
64
|
+
* any of those four strings.
|
|
65
|
+
* 8. No positive tabindex, and no outline of none or 0 on a selector that is
|
|
66
|
+
* not itself a focus selector. board-a11y scans board.css, index.html,
|
|
67
|
+
* floor.html, board.js and floor.js for both.
|
|
68
|
+
*
|
|
69
|
+
* Type rule that keeps the 14px floor from regressing: no rule in this file
|
|
70
|
+
* sets a font-size in px. Sizes come from the --t-* scale, and --t--1 is
|
|
71
|
+
* redefined to the body size inside the narrow breakpoint, so the floor is a
|
|
72
|
+
* property of the token rather than something every rule has to remember.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
@font-face{font-family:"Atkinson Hyperlegible Next";src:url("fonts/atkinson-board.woff2") format("woff2");font-weight:400 700;font-style:normal;font-display:swap}
|
|
76
|
+
@font-face{font-family:"Azeret Mono";src:url("fonts/azeret-board.woff2") format("woff2");font-weight:400 600;font-style:normal;font-display:swap}
|
|
77
|
+
|
|
78
|
+
:root {
|
|
79
|
+
/* families. Atkinson is self-hosted and stays: it is a real face with a
|
|
80
|
+
purpose rather than the system stack, and a board read at a glance all day
|
|
81
|
+
is exactly what hyperlegible letterforms are for. Azeret is demoted to
|
|
82
|
+
true data tokens only, never prose */
|
|
83
|
+
--sans: "Atkinson Hyperlegible Next", "Atkinson Hyperlegible", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
84
|
+
--mono: "Azeret Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
85
|
+
|
|
86
|
+
/* ground and elevation, hue 258 at a chroma low enough never to read as
|
|
87
|
+
navy. The old ramp was the same hue at four times this chroma */
|
|
88
|
+
--e0: #0E1012; /* oklch(0.170 0.006 258) page ground, unlit */
|
|
89
|
+
--e1: #171A1D; /* oklch(0.215 0.007 258) recessed well, tracks, wells, inputs */
|
|
90
|
+
--e2: #212427; /* oklch(0.258 0.008 258) the raised panel */
|
|
91
|
+
--panel-2: #212427; /* oklch(0.258 0.008 258) the panel again, under the name the contrast test reads */
|
|
92
|
+
--e3: #2C2F34; /* oklch(0.305 0.009 258) quiet control */
|
|
93
|
+
--e4: #363A40; /* oklch(0.345 0.010 258) control hover */
|
|
94
|
+
--selected: #2A3340; /* oklch(0.300 0.020 258) selected row */
|
|
95
|
+
--track: #2B2E32; /* oklch(0.300 0.008 258) gauge track */
|
|
96
|
+
|
|
97
|
+
/* edges. --line separates siblings INSIDE one panel and is the only rule
|
|
98
|
+
left in the design. --edge is the panel boundary, barely there */
|
|
99
|
+
--line: #35383D; /* oklch(0.335 0.008 258) sibling separator inside one panel */
|
|
100
|
+
--line-strong: #4A4E55; /* oklch(0.420 0.009 258) section rule on the ground */
|
|
101
|
+
--edge: #2A2D31; /* oklch(0.285 0.008 258) panel boundary */
|
|
102
|
+
--edge-hi: #4B5058; /* oklch(0.430 0.010 258) the top edge that catches light */
|
|
103
|
+
--control-edge: #686D75; /* oklch(0.540 0.010 258) quiet control boundary, 3.00:1 on the panel */
|
|
104
|
+
|
|
105
|
+
/* text */
|
|
106
|
+
--text: #F3F4F7; /* oklch(0.960 0.004 258) primary, 14.18:1 on the panel */
|
|
107
|
+
--text-2: #B9BEC6; /* oklch(0.800 0.010 258) secondary, 8.35:1 on the panel */
|
|
108
|
+
--text-3: #9399A1; /* oklch(0.680 0.012 258) meta, 5.43:1 on the panel */
|
|
109
|
+
--text-dim: #6B7079; /* oklch(0.540 0.012 258) disabled only, never encodes state */
|
|
110
|
+
|
|
111
|
+
/* the one accent. It means: this is the action to take. Nowhere else */
|
|
112
|
+
--accent: #5C91FF; /* oklch(0.680 0.185 264) */
|
|
113
|
+
--accent-hi: #7CADFF; /* oklch(0.760 0.150 264) */
|
|
114
|
+
--on-accent: #070D1A; /* oklch(0.150 0.030 264) 6.43:1 on the accent */
|
|
115
|
+
|
|
116
|
+
/* state. A fill paints inside a track and a mark is a dot. The -text
|
|
117
|
+
variants exist for the few places a word must carry the state itself */
|
|
118
|
+
--calm: #5F6670; /* oklch(0.520 0.012 258) a healthy gauge is quiet, not green */
|
|
119
|
+
--ok: #3FA463; /* oklch(0.620 0.140 152) success fill */
|
|
120
|
+
--ok-text: #6FD68F; /* oklch(0.800 0.130 150) success mark and sentence */
|
|
121
|
+
--warn: #E0A33C; /* oklch(0.760 0.140 75) gauge segment 60 to 85 */
|
|
122
|
+
--warn-text: #F0BE6E; /* oklch(0.830 0.110 78) warning mark and sentence */
|
|
123
|
+
--danger: #E64343; /* oklch(0.620 0.200 25) gauge segment past 85, and the walled fill */
|
|
124
|
+
--danger-text: #FF8A8A; /* oklch(0.760 0.150 22) error mark and sentence */
|
|
125
|
+
|
|
126
|
+
/* interaction and identity. Identity is a dot beside a printed name and the
|
|
127
|
+
gauge fill. It is never text colour on a row and never a border */
|
|
128
|
+
--focus: #EDF2FA; /* oklch(0.960 0.010 258) focus ring, appears nowhere else */
|
|
129
|
+
--id-claude: #FCA169; /* oklch(0.790 0.130 52) warm */
|
|
130
|
+
--id-codex: #69DBBA; /* oklch(0.815 0.115 172) cool green */
|
|
131
|
+
--id-agy: #CC97F3; /* oklch(0.760 0.140 310) violet, 46 degrees clear of the accent */
|
|
132
|
+
--id-fake: #A0A6AE; /* oklch(0.720 0.010 258) the scripted adapter, neutral in hue */
|
|
133
|
+
|
|
134
|
+
/* type scale. Fixed rem, range 13 to 52. The old board ran 13 to 21, which
|
|
135
|
+
is why it read as a spreadsheet */
|
|
136
|
+
--t--1: 0.8125rem; /* 13px the row register and the scaffolding strip */
|
|
137
|
+
--t-0: 0.9375rem; /* 15px labels, meta, controls, the narrow floor */
|
|
138
|
+
--t-1: 1.0625rem; /* 17px prompts, panel heads, body default */
|
|
139
|
+
--t-2: 1.3125rem; /* 21px section heads, secondary readings */
|
|
140
|
+
--t-3: 2.125rem; /* 34px the gauge numeral */
|
|
141
|
+
--t-4: 3.25rem; /* 52px the verdict */
|
|
142
|
+
|
|
143
|
+
--w-text: 400;
|
|
144
|
+
--w-head: 700;
|
|
145
|
+
|
|
146
|
+
--r-panel: 16px;
|
|
147
|
+
--r-well: 12px;
|
|
148
|
+
--r-control: 10px;
|
|
149
|
+
|
|
150
|
+
--ease: cubic-bezier(.2,0,0,1);
|
|
151
|
+
|
|
152
|
+
/* one raised object, lit from above. The inset highlight is the top edge and
|
|
153
|
+
the two shadows are contact and ambient */
|
|
154
|
+
--lift: inset 0 1px 0 rgba(255,255,255,.05), 0 1px 2px rgba(0,0,0,.45), 0 18px 44px -14px rgba(0,0,0,.70);
|
|
155
|
+
--lift-hi: inset 0 1px 0 rgba(255,255,255,.10), 0 1px 2px rgba(0,0,0,.45), 0 22px 52px -14px rgba(0,0,0,.75);
|
|
156
|
+
--sink: inset 0 1px 3px rgba(0,0,0,.50);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
* { box-sizing: border-box; }
|
|
160
|
+
|
|
161
|
+
html, body { color: var(--text); background: var(--e0); }
|
|
162
|
+
|
|
163
|
+
body {
|
|
164
|
+
margin: 0;
|
|
165
|
+
font-family: var(--sans);
|
|
166
|
+
font-size: var(--t-1);
|
|
167
|
+
line-height: 1.5;
|
|
168
|
+
-webkit-font-smoothing: antialiased;
|
|
169
|
+
text-rendering: optimizeLegibility;
|
|
170
|
+
font-variant-numeric: tabular-nums;
|
|
171
|
+
font-feature-settings: "tnum" 1;
|
|
172
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
h1, h2, h3, p, ul, ol, dl, dd, figure { margin: 0; }
|
|
176
|
+
ul, ol { padding: 0; list-style: none; }
|
|
177
|
+
button { font: inherit; color: inherit; margin: 0; }
|
|
178
|
+
code { font-family: var(--mono); font-size: 0.94em; }
|
|
179
|
+
|
|
180
|
+
[hidden] { display: none !important; }
|
|
181
|
+
|
|
182
|
+
/* one focus treatment for every control on the board */
|
|
183
|
+
:where(button, a, input, select, textarea, summary, [tabindex]):focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--r-control); }
|
|
184
|
+
.panel-prompt:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--r-control); }
|
|
185
|
+
|
|
186
|
+
/* ---- shell ------------------------------------------------------------- */
|
|
187
|
+
.wrap { max-width: 1120px; margin: 0 auto; padding-inline: clamp(20px, 4vw, 40px); padding-bottom: 72px; }
|
|
188
|
+
|
|
189
|
+
.banner { background: var(--e1); border-bottom: 1px solid var(--edge); color: var(--text-2); font-size: var(--t-0); padding: 10px clamp(20px, 4vw, 40px); padding-top: calc(10px + env(safe-area-inset-top, 0px)); }
|
|
190
|
+
|
|
191
|
+
.masthead { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding-block: 28px 0; }
|
|
192
|
+
.brand { font-size: var(--t-1); font-weight: var(--w-head); letter-spacing: -.01em; }
|
|
193
|
+
.brand-glyph { display: inline-block; margin-right: 4px; }
|
|
194
|
+
.masthead-right { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
|
|
195
|
+
.floor-link { color: var(--text-2); text-decoration: none; border-bottom: 1px solid var(--line-strong); padding-bottom: 1px; }
|
|
196
|
+
.floor-link:hover { color: var(--text); border-bottom-color: var(--text-2); }
|
|
197
|
+
|
|
198
|
+
.sse-status { display: inline-flex; align-items: center; gap: 8px; }
|
|
199
|
+
.sse-word { color: var(--text-3); }
|
|
200
|
+
.sse-rule { width: 8px; height: 8px; border-radius: 999px; background: var(--ok); flex: none; }
|
|
201
|
+
.sse-rule.is-connecting { background: var(--warn); }
|
|
202
|
+
.sse-rule.is-down { background: var(--danger); }
|
|
203
|
+
.sched-status { color: var(--text-3); }
|
|
204
|
+
|
|
205
|
+
/* ---- the verdict: the largest thing on the board is a sentence ---------- */
|
|
206
|
+
.verdict { padding-block: 56px 56px; }
|
|
207
|
+
.verdict h1 { font-size: var(--t-4); font-weight: var(--w-head); line-height: 1.08; letter-spacing: -.028em; max-width: 26ch; text-wrap: balance; }
|
|
208
|
+
.verdict p { margin-top: 16px; font-size: var(--t-2); line-height: 1.5; color: var(--text-2); max-width: 54ch; text-wrap: pretty; }
|
|
209
|
+
.verdict p:empty { display: none; }
|
|
210
|
+
|
|
211
|
+
/* ---- panel: a raised object, not a drawn rectangle ---------------------- */
|
|
212
|
+
.panel { position: relative; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-panel); padding: 32px; box-shadow: var(--lift); }
|
|
213
|
+
.panel--lit { box-shadow: var(--lift-hi); }
|
|
214
|
+
|
|
215
|
+
.panel-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 24px; }
|
|
216
|
+
.who { display: inline-flex; align-items: center; gap: 10px; font-size: var(--t-1); font-weight: var(--w-head); }
|
|
217
|
+
.who-note { font-size: var(--t-0); color: var(--text-3); }
|
|
218
|
+
|
|
219
|
+
.dot { width: 8px; height: 8px; border-radius: 999px; flex: none; background: var(--id-fake); }
|
|
220
|
+
.dot.id-claude { background: var(--id-claude); }
|
|
221
|
+
.dot.id-codex { background: var(--id-codex); }
|
|
222
|
+
.dot.id-agy { background: var(--id-agy); }
|
|
223
|
+
|
|
224
|
+
/* ---- logins ------------------------------------------------------------- */
|
|
225
|
+
.logins { display: grid; gap: 20px; }
|
|
226
|
+
.logins-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
|
227
|
+
.head-caption { margin-top: 16px; font-size: var(--t-0); color: var(--text-3); }
|
|
228
|
+
|
|
229
|
+
/* ---- the gauge: the instrument ------------------------------------------ */
|
|
230
|
+
.gauge-block + .gauge-block { margin-top: 26px; }
|
|
231
|
+
.gauge { display: flex; align-items: center; gap: 20px; }
|
|
232
|
+
.gauge-label { flex: none; width: 62px; font-size: var(--t-0); color: var(--text-3); }
|
|
233
|
+
.gauge--none .gauge-label { width: auto; }
|
|
234
|
+
.gauge-track { flex: 1 1 auto; min-width: 0; position: relative; height: 16px; background: var(--track); border-radius: 999px; box-shadow: inset 0 1px 2px rgba(0,0,0,.45); overflow: hidden; }
|
|
235
|
+
.gauge--minor .gauge-track { height: 8px; }
|
|
236
|
+
.gauge-fill { position: absolute; inset: 0 auto 0 0; border-radius: 999px; background: var(--calm); }
|
|
237
|
+
/* the reserve notch: a 2px gap cut through the bar where 85 percent sits */
|
|
238
|
+
.gauge-post { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--e1); }
|
|
239
|
+
.gauge-readout { flex: none; width: 168px; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
|
|
240
|
+
.gauge--noread::after { content: ""; flex: none; width: 96px; }
|
|
241
|
+
.gauge-read { text-align: right; font-size: var(--t-3); font-weight: var(--w-head); letter-spacing: -.02em; line-height: 1; }
|
|
242
|
+
.gauge--minor .gauge-read { font-size: var(--t-2); }
|
|
243
|
+
.gauge-read--none { font-size: var(--t-2); font-weight: var(--w-text); color: var(--text-3); }
|
|
244
|
+
.gauge-note { font-size: var(--t-0); color: var(--text-3); white-space: nowrap; }
|
|
245
|
+
|
|
246
|
+
.reading { margin-top: 22px; font-size: var(--t-2); font-weight: var(--w-head); line-height: 1.3; letter-spacing: -.012em; }
|
|
247
|
+
.reading-sub { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); max-width: 46ch; }
|
|
248
|
+
.reading-sub--lead { margin-top: 22px; }
|
|
249
|
+
|
|
250
|
+
/* ---- section heads ------------------------------------------------------ */
|
|
251
|
+
.section-head { padding-block: 56px 20px; }
|
|
252
|
+
.section-head h2 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
|
|
253
|
+
.section-head .region-meta { margin-top: 8px; font-size: var(--t-0); color: var(--text-3); }
|
|
254
|
+
.section-head--quiet { padding-block: 56px 16px; display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
|
|
255
|
+
.section-head--quiet .region-meta { margin-top: 0; }
|
|
256
|
+
.hoisted { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; font-size: var(--t-0); }
|
|
257
|
+
.hoisted-note { color: var(--text-2); }
|
|
258
|
+
.hoisted-note.tone-warn { color: var(--warn-text); }
|
|
259
|
+
.hoisted-note.tone-danger { color: var(--danger-text); }
|
|
260
|
+
.hoisted-note.tone-muted { color: var(--text-3); }
|
|
261
|
+
|
|
262
|
+
/* ---- terminals: rows inside one panel, separated by the only rule left --- */
|
|
263
|
+
.terms { padding: 4px 0; }
|
|
264
|
+
.term { padding: 26px 32px; }
|
|
265
|
+
.term + .term { border-top: 1px solid var(--line); }
|
|
266
|
+
.term.is-urgent { background: linear-gradient(to right, rgba(230,67,67,.07), transparent 40%); }
|
|
267
|
+
.term-row { display: flex; align-items: center; gap: 28px; }
|
|
268
|
+
.term-body { flex: 1 1 auto; min-width: 0; }
|
|
269
|
+
.term-row > .term-body { flex-basis: 24ch; }
|
|
270
|
+
.term-clock { flex: none; display: flex; align-items: baseline; justify-content: flex-end; gap: 14px; font-size: var(--t--1); color: var(--text-3); white-space: nowrap; }
|
|
271
|
+
.term-register { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
|
|
272
|
+
.term-where { color: var(--text-2); }
|
|
273
|
+
.term-when { color: var(--text-3); min-width: 6ch; text-align: right; }
|
|
274
|
+
.term-id { color: var(--text-3); min-width: 4ch; text-align: right; font-family: var(--mono); }
|
|
275
|
+
.term-actions { flex: none; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; width: 272px; }
|
|
276
|
+
.term-actions .btn { white-space: nowrap; width: 100%; }
|
|
277
|
+
/* a row with one or three controls keeps the grid honest rather than stretching
|
|
278
|
+
the odd one across both columns */
|
|
279
|
+
.term-actions .btn:only-child { grid-column: 1 / -1; }
|
|
280
|
+
|
|
281
|
+
.state { display: inline-flex; align-items: center; gap: 8px; color: var(--text-2); font-weight: var(--w-head); }
|
|
282
|
+
.mark { width: 8px; height: 8px; border-radius: 999px; flex: none; background: var(--text-3); }
|
|
283
|
+
.mark.tone-ok { background: var(--ok); }
|
|
284
|
+
.mark.tone-warn { background: var(--warn); }
|
|
285
|
+
.mark.tone-danger { background: var(--danger); }
|
|
286
|
+
.mark.state-active { background: var(--text); }
|
|
287
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
288
|
+
.mark.state-active { animation: breathe 2400ms var(--ease) infinite; }
|
|
289
|
+
}
|
|
290
|
+
@keyframes breathe { 0%,100% { opacity: 1 } 50% { opacity: .45 } }
|
|
291
|
+
|
|
292
|
+
.term-prompt { margin-top: 14px; font-size: var(--t-1); line-height: 1.45; max-width: 62ch; color: var(--text); }
|
|
293
|
+
.term-prompt--empty { color: var(--text-3); }
|
|
294
|
+
.panel-prompt { display: block; width: 100%; text-align: left; background: none; border: 0; padding: 0; margin-top: 14px; font-size: var(--t-1); line-height: 1.45; max-width: 62ch; color: var(--text); cursor: pointer; }
|
|
295
|
+
.panel-prompt:hover { color: var(--accent-hi); }
|
|
296
|
+
|
|
297
|
+
.sentence { margin-top: 10px; font-size: var(--t-0); color: var(--text-2); max-width: 64ch; }
|
|
298
|
+
.sentence.tone-warn { color: var(--warn-text); }
|
|
299
|
+
.sentence.tone-danger { color: var(--danger-text); }
|
|
300
|
+
.sentence.tone-ok { color: var(--ok-text); }
|
|
301
|
+
.sentence.tone-muted { color: var(--text-3); }
|
|
302
|
+
.files { margin-top: 10px; font-size: var(--t--1); color: var(--text-3); }
|
|
303
|
+
.file { color: var(--text-3); }
|
|
304
|
+
.file.is-overlap { color: var(--warn-text); }
|
|
305
|
+
.blocker { margin-top: 10px; font-size: var(--t-0); color: var(--text-3); max-width: 64ch; }
|
|
306
|
+
.blocker.is-hoisted { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
307
|
+
|
|
308
|
+
/* the flat inline data token. Not a capsule: a chip here is a word set in the
|
|
309
|
+
meta colour beside the thing it qualifies */
|
|
310
|
+
.chip { font-size: var(--t--1); color: var(--text-2); font-family: var(--mono); }
|
|
311
|
+
.chip.is-stale { color: var(--text-3); }
|
|
312
|
+
.chip-id-claude { color: var(--id-claude); }
|
|
313
|
+
.chip-id-codex { color: var(--id-codex); }
|
|
314
|
+
.chip-id-agy { color: var(--id-agy); }
|
|
315
|
+
.chip-id-fake { color: var(--id-fake); }
|
|
316
|
+
.chip-state-ok { color: var(--ok-text); }
|
|
317
|
+
.chip-state-warn { color: var(--warn-text); }
|
|
318
|
+
|
|
319
|
+
.acct-name { font-weight: var(--w-head); }
|
|
320
|
+
.acct-name.id-claude { color: var(--id-claude); }
|
|
321
|
+
.acct-name.id-codex { color: var(--id-codex); }
|
|
322
|
+
.acct-name.id-agy { color: var(--id-agy); }
|
|
323
|
+
.acct-name.id-fake { color: var(--id-fake); }
|
|
324
|
+
|
|
325
|
+
/* ---- controls ----------------------------------------------------------- */
|
|
326
|
+
.btn { display: inline-flex; align-items: center; justify-content: center; min-height: 36px; padding: 0 16px; border-radius: var(--r-control); font-size: var(--t-0); font-weight: var(--w-head); letter-spacing: -.005em; cursor: pointer; border: 1px solid transparent; background: none; transition: background-color 150ms var(--ease), border-color 150ms var(--ease), transform 150ms var(--ease), color 150ms var(--ease); }
|
|
327
|
+
.btn-primary { background: var(--accent); color: var(--on-accent); }
|
|
328
|
+
.btn-primary:hover { background: var(--accent-hi); }
|
|
329
|
+
.btn-secondary { background: var(--e3); color: var(--text); border-color: var(--control-edge); }
|
|
330
|
+
.btn-secondary:hover { background: var(--e4); border-color: #7D828A; }
|
|
331
|
+
.btn-danger { background: var(--e3); color: var(--danger-text); border-color: var(--control-edge); }
|
|
332
|
+
.btn-danger:hover { background: var(--e4); }
|
|
333
|
+
.btn-text { min-height: 32px; padding: 0 2px; background: none; color: var(--text-2); border: 0; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line-strong); }
|
|
334
|
+
.btn-text:hover { color: var(--text); text-decoration-color: var(--text-2); }
|
|
335
|
+
.btn:active { transform: translateY(1px); }
|
|
336
|
+
.btn:disabled { color: var(--text-dim); background: var(--e1); border-color: var(--edge); cursor: not-allowed; }
|
|
337
|
+
.btn:disabled:hover { background: var(--e1); }
|
|
338
|
+
|
|
339
|
+
/* ---- disclosure wells --------------------------------------------------- */
|
|
340
|
+
.drawer { margin-top: 22px; background: var(--e1); border-radius: var(--r-well); padding: 24px; box-shadow: var(--sink); }
|
|
341
|
+
.drawer-head { font-size: var(--t-1); font-weight: var(--w-head); margin-bottom: 18px; }
|
|
342
|
+
.drawer-note { margin-top: 18px; font-size: var(--t-0); color: var(--text-2); max-width: 64ch; }
|
|
343
|
+
.drawer-actions { margin-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
344
|
+
|
|
345
|
+
/* the four scroll boxes board-drawer.test.mjs scans for by line prefix. Each
|
|
346
|
+
class is the first token on its line, in this order, and no earlier line in
|
|
347
|
+
this file begins with any of the four */
|
|
348
|
+
.drawer-task { max-height: 22rem; overflow: auto; font-size: var(--t-0); color: var(--text-2); }
|
|
349
|
+
.drawer-msg p { max-height: 18rem; overflow-y: auto; font-size: var(--t-0); color: var(--text-2); }
|
|
350
|
+
.drawer-timeline { max-height: 22rem; overflow: auto; font-size: var(--t-0); }
|
|
351
|
+
.drawer-diff { max-height: 26rem; overflow: auto; font-family: var(--mono); font-size: var(--t--1); }
|
|
352
|
+
|
|
353
|
+
.drawer-msg { margin-top: 14px; }
|
|
354
|
+
.drawer-msg-role { font-size: var(--t--1); color: var(--text-3); font-weight: var(--w-head); }
|
|
355
|
+
.drawer-msg-when { font-size: var(--t--1); color: var(--text-3); }
|
|
356
|
+
|
|
357
|
+
/* ---- ledger: on the ground, unpanelled --------------------------------- */
|
|
358
|
+
.ledger { margin-top: 56px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 40px; padding-top: 32px; border-top: 1px solid var(--edge); }
|
|
359
|
+
.ledger-cell h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
|
|
360
|
+
.ledger-cell .region-meta { margin-top: 6px; font-size: var(--t-0); color: var(--text-3); }
|
|
361
|
+
.ledger-actions { margin-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
362
|
+
.ledger-cell .empty-line { margin-top: 12px; font-size: var(--t-0); color: var(--text-3); }
|
|
363
|
+
|
|
364
|
+
.list { margin-top: 20px; }
|
|
365
|
+
.list li { padding: 14px 0; font-size: var(--t-0); }
|
|
366
|
+
.list li + li { border-top: 1px solid var(--line); }
|
|
367
|
+
.line { display: grid; grid-template-columns: 72px 1fr auto; gap: 8px 20px; align-items: baseline; }
|
|
368
|
+
.line--commit { grid-template-columns: 1fr auto; }
|
|
369
|
+
.line-main { color: var(--text); }
|
|
370
|
+
.line-meta { color: var(--text-2); }
|
|
371
|
+
.line-when { color: var(--text-3); text-align: right; }
|
|
372
|
+
.group-head { display: flex; align-items: baseline; gap: 10px; margin-top: 24px; margin-bottom: 4px; font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
|
|
373
|
+
.group-head:first-child { margin-top: 0; }
|
|
374
|
+
.group-head span { font-weight: var(--w-text); color: var(--text-3); }
|
|
375
|
+
|
|
376
|
+
.trunk-row { display: grid; grid-template-columns: 1fr auto; gap: 6px 20px; align-items: baseline; padding: 14px 0; font-size: var(--t-0); }
|
|
377
|
+
.trunk-row + .trunk-row { border-top: 1px solid var(--line); }
|
|
378
|
+
.landed-by { color: var(--text-3); }
|
|
379
|
+
.finished-line { padding: 14px 0; font-size: var(--t-0); }
|
|
380
|
+
.finished-names { margin-left: 10px; color: var(--text-3); }
|
|
381
|
+
|
|
382
|
+
/* ---- cards -------------------------------------------------------------- */
|
|
383
|
+
.card-list { display: grid; gap: 14px; }
|
|
384
|
+
/* A card row reads down the same four columns as an account and a terminal
|
|
385
|
+
(board.js buildRow): R1 who, R2 what, R3 where, R4 when-act. The first three
|
|
386
|
+
stack in the body column; R4 is the clock and the actions on the right, the
|
|
387
|
+
same shape as .term-row. Without this the four cells fell back to block
|
|
388
|
+
defaults and the row read as unstyled markup. */
|
|
389
|
+
.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 28px; align-items: start; padding: 26px 32px; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-well); }
|
|
390
|
+
.r1, .r2, .r3 { grid-column: 1; min-width: 0; }
|
|
391
|
+
.r1 { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
|
|
392
|
+
.r2 { margin-top: 8px; }
|
|
393
|
+
.r3 { margin-top: 12px; display: grid; gap: 8px; }
|
|
394
|
+
.r4 { grid-column: 2; grid-row: 1 / span 3; display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
|
|
395
|
+
.r4 .elapsed { font-size: var(--t--1); white-space: nowrap; }
|
|
396
|
+
/* the same 2x2 block the terminal rows use, so the two row kinds line up */
|
|
397
|
+
.r4 .row-actions { margin-top: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; width: 272px; }
|
|
398
|
+
.r4 .row-actions .btn { white-space: nowrap; width: 100%; }
|
|
399
|
+
.r4 .row-actions .btn:only-child { grid-column: 1 / -1; }
|
|
400
|
+
.leases { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
401
|
+
.reassign-picker { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
402
|
+
/* a bare mono span: the chips carry their own font, this one does not */
|
|
403
|
+
.mono { font-family: var(--mono); }
|
|
404
|
+
.row:hover { background: var(--e3); }
|
|
405
|
+
.row.is-selected { background: var(--selected); }
|
|
406
|
+
/* a bare <button> keeps the UA's light-grey fill and padding, which put
|
|
407
|
+
near-white text on near-white and made the card title unreadable. Reset it
|
|
408
|
+
the way .panel-prompt does for terminal rows. */
|
|
409
|
+
.row-title { display: block; width: 100%; text-align: left; background: none; border: 0; padding: 0; font-size: var(--t-1); line-height: 1.45; color: var(--text); cursor: pointer; }
|
|
410
|
+
.row-title:hover { color: var(--accent-hi); }
|
|
411
|
+
.row-meta { font-size: var(--t-0); color: var(--text-3); display: flex; gap: 14px; flex-wrap: wrap; }
|
|
412
|
+
.row-actions { margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
413
|
+
/* the demoted-notes disclosure in a terminal row */
|
|
414
|
+
.also { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
|
|
415
|
+
.status-word { font-weight: var(--w-head); color: var(--text-2); }
|
|
416
|
+
.elapsed { font-family: var(--mono); color: var(--text-3); }
|
|
417
|
+
.where { color: var(--text-2); }
|
|
418
|
+
.worktree { color: var(--text-3); }
|
|
419
|
+
|
|
420
|
+
/* ---- detail panes ------------------------------------------------------- */
|
|
421
|
+
.detail { margin-top: 22px; background: var(--e1); border-radius: var(--r-well); box-shadow: var(--sink); }
|
|
422
|
+
.detail-inner { padding: 24px; }
|
|
423
|
+
.detail-heading { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; font-size: var(--t-1); font-weight: var(--w-head); margin-bottom: 12px; }
|
|
424
|
+
.detail-section { margin-top: 20px; }
|
|
425
|
+
.detail-sub { font-size: var(--t-0); color: var(--text-3); margin-bottom: 8px; }
|
|
426
|
+
.kv { display: grid; grid-template-columns: 148px 1fr; gap: 10px 20px; font-size: var(--t-0); }
|
|
427
|
+
.kv-key { color: var(--text-3); }
|
|
428
|
+
.kv-val { color: var(--text); }
|
|
429
|
+
.well { background: var(--e0); border-radius: var(--r-control); padding: 14px; }
|
|
430
|
+
.log-pre { font-family: var(--mono); font-size: var(--t--1); white-space: pre-wrap; color: var(--text-2); max-height: 26rem; overflow: auto; }
|
|
431
|
+
.hunk { font-family: var(--mono); font-size: var(--t--1); white-space: pre-wrap; }
|
|
432
|
+
.file-row { display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: 8px; text-align: left; }
|
|
433
|
+
.turn { padding: 12px 0; }
|
|
434
|
+
.turn + .turn { border-top: 1px solid var(--line); }
|
|
435
|
+
.turn-role { font-size: var(--t--1); color: var(--text-3); font-weight: var(--w-head); }
|
|
436
|
+
.turn-when { font-size: var(--t--1); color: var(--text-3); }
|
|
437
|
+
/* the kind word sits beside the clock, not welded to it: `10:40 AMstarted` */
|
|
438
|
+
.turn-when + .turn-role { margin-left: 10px; }
|
|
439
|
+
|
|
440
|
+
/* ---- chain rail, order rows, confirm ------------------------------------ */
|
|
441
|
+
.chain-rail { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
|
|
442
|
+
.cap-line { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; font-size: var(--t-0); color: var(--text-3); margin-top: 8px; }
|
|
443
|
+
.order-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; font-size: var(--t-0); }
|
|
444
|
+
.order-row-name { flex: 1 1 auto; }
|
|
445
|
+
/* the label on each agent row in the New card dialog */
|
|
446
|
+
.fallback-row-title { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
|
|
447
|
+
.confirm-row { margin-top: 12px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; background: var(--e4); border-radius: var(--r-control); padding: 12px 14px; font-size: var(--t-0); }
|
|
448
|
+
.actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
449
|
+
|
|
450
|
+
/* ---- the floor: the scheduler's view, tables on the same ground --------- */
|
|
451
|
+
.masthead-left { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; }
|
|
452
|
+
.repos-status, .counts-status { color: var(--text-3); }
|
|
453
|
+
.counts-status span { color: var(--text-2); font-weight: var(--w-head); margin-left: 2px; }
|
|
454
|
+
.region-floor { margin-top: 12px; }
|
|
455
|
+
.ftable { width: 100%; border-collapse: collapse; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-panel); box-shadow: var(--lift); overflow: hidden; font-size: var(--t-0); }
|
|
456
|
+
.ftable th, .ftable td { text-align: left; vertical-align: top; padding: 14px 18px; }
|
|
457
|
+
.ftable thead th { color: var(--text-3); font-weight: var(--w-head); font-size: var(--t--1); border-bottom: 1px solid var(--line); }
|
|
458
|
+
.ftable tbody tr + tr td { border-top: 1px solid var(--line); }
|
|
459
|
+
.ftable tbody tr:hover td { background: var(--e3); }
|
|
460
|
+
.ftable td.c-elapsed, .ftable td.c-since, .ftable td.c-time { font-family: var(--mono); color: var(--text-3); white-space: nowrap; }
|
|
461
|
+
.ftable .c-actions { text-align: right; }
|
|
462
|
+
.ftable .c-actions .btn { white-space: nowrap; }
|
|
463
|
+
.ftable-wrap { overflow-x: auto; }
|
|
464
|
+
.ftable td.is-empty { color: var(--text-3); }
|
|
465
|
+
|
|
466
|
+
/* ---- settings and forms ------------------------------------------------- */
|
|
467
|
+
.settings-body { display: grid; gap: 24px; }
|
|
468
|
+
.field { display: grid; gap: 8px; }
|
|
469
|
+
.field label, .form-row label { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
|
|
470
|
+
.field-help { font-size: var(--t-0); color: var(--text-3); max-width: 64ch; }
|
|
471
|
+
.field-status { font-size: var(--t-0); color: var(--text-3); }
|
|
472
|
+
.board-facts { font-size: var(--t-0); color: var(--text-3); }
|
|
473
|
+
input[type="text"], input[type="password"], textarea, select { font: inherit; font-size: var(--t-0); color: var(--text); background: var(--e0); border: 1px solid var(--control-edge); border-radius: var(--r-control); padding: 9px 12px; min-height: 36px; }
|
|
474
|
+
input[type="checkbox"] { accent-color: var(--accent); width: 16px; height: 16px; }
|
|
475
|
+
fieldset { border: 1px solid var(--edge); border-radius: var(--r-well); padding: 18px; display: grid; gap: 12px; }
|
|
476
|
+
legend { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); padding-inline: 6px; }
|
|
477
|
+
.form-row { display: grid; gap: 8px; margin-top: 18px; }
|
|
478
|
+
.form-error { background: var(--e1); border-left: 3px solid var(--danger); border-radius: var(--r-control); padding: 12px 14px; font-size: var(--t-0); color: var(--danger-text); }
|
|
479
|
+
.advanced-options { margin-top: 22px; }
|
|
480
|
+
.advanced-options summary { cursor: pointer; font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
|
|
481
|
+
.dialog-title { font-size: var(--t-2); font-weight: var(--w-head); }
|
|
482
|
+
.dialog-intro { margin-top: 8px; font-size: var(--t-0); color: var(--text-3); max-width: 64ch; }
|
|
483
|
+
.dialog-actions { margin-top: 26px; display: flex; gap: 10px; justify-content: flex-end; }
|
|
484
|
+
dialog { background: var(--e2); color: var(--text); border: 1px solid var(--edge); border-radius: var(--r-panel); box-shadow: var(--lift-hi); padding: 32px; max-width: 640px; width: calc(100vw - 40px); }
|
|
485
|
+
dialog::backdrop { background: rgba(0,0,0,.6); }
|
|
486
|
+
|
|
487
|
+
.empty-line { font-size: var(--t-0); color: var(--text-3); margin-top: 18px; max-width: 64ch; }
|
|
488
|
+
.sysmsg { position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%); display: grid; gap: 8px; z-index: 50; }
|
|
489
|
+
.sysmsg:empty { display: none; }
|
|
490
|
+
.sysmsg-item { background: var(--e4); color: var(--text); border-radius: var(--r-control); padding: 12px 16px; font-size: var(--t-0); box-shadow: var(--lift); }
|
|
491
|
+
.prov { font-size: var(--t--1); color: var(--text-3); }
|
|
492
|
+
.prov.is-stale { color: var(--warn-text); }
|
|
493
|
+
.tone-muted { color: var(--text-3); }
|
|
494
|
+
.tone-warn { color: var(--warn-text); }
|
|
495
|
+
.tone-danger { color: var(--danger-text); }
|
|
496
|
+
.tone-ok { color: var(--ok-text); }
|
|
497
|
+
|
|
498
|
+
/* ===========================================================================
|
|
499
|
+
narrow. One breakpoint, because the layout has one narrow form: everything
|
|
500
|
+
in a single column. --t--1 is redefined to the body size here, which is what
|
|
501
|
+
holds the 14px floor without every rule having to remember it.
|
|
502
|
+
=========================================================================== */
|
|
503
|
+
@media (max-width: 760px) {
|
|
504
|
+
.wrap { --t-4: 2rem; --t-3: 1.75rem; --t-2: 1.1875rem; --t--1: 0.9375rem; }
|
|
505
|
+
.verdict { padding-block: 36px 32px; }
|
|
506
|
+
.verdict h1 { max-width: none; letter-spacing: -.02em; }
|
|
507
|
+
.verdict p { margin-top: 16px; }
|
|
508
|
+
.panel { padding: 22px; border-radius: 14px; }
|
|
509
|
+
.logins-pair { grid-template-columns: 1fr; }
|
|
510
|
+
.gauge { gap: 14px; flex-wrap: wrap; }
|
|
511
|
+
.gauge-label { width: 100%; }
|
|
512
|
+
.gauge-track { flex: 1 1 110px; }
|
|
513
|
+
.gauge-readout { display: contents; }
|
|
514
|
+
.gauge-read { width: 68px; }
|
|
515
|
+
.gauge-note { width: 100%; text-align: left; margin-top: 8px; }
|
|
516
|
+
.gauge--noread::after { width: 68px; }
|
|
517
|
+
.gauge-block + .gauge-block { margin-top: 22px; }
|
|
518
|
+
code { font-size: 1em; }
|
|
519
|
+
.section-head { padding-block: 40px 16px; }
|
|
520
|
+
.section-head--quiet { padding-block: 40px 16px; }
|
|
521
|
+
.term { padding: 22px; }
|
|
522
|
+
.term-row { flex-direction: column; align-items: stretch; gap: 14px; }
|
|
523
|
+
.term-clock { order: -1; width: auto; justify-content: flex-end; gap: 14px; }
|
|
524
|
+
.term-id { min-width: 0; }
|
|
525
|
+
.term-actions { width: auto; justify-content: flex-start; }
|
|
526
|
+
.term-actions .btn { flex: 1 1 auto; }
|
|
527
|
+
.term-register { gap: 10px 14px; }
|
|
528
|
+
.kv { grid-template-columns: 1fr; gap: 4px 0; }
|
|
529
|
+
.kv-val + .kv-key { margin-top: 12px; }
|
|
530
|
+
.ledger { grid-template-columns: 1fr; gap: 32px; margin-top: 40px; }
|
|
531
|
+
.line { grid-template-columns: 1fr auto; gap: 6px 14px; }
|
|
532
|
+
.line-main { grid-column: 1 / -1; }
|
|
533
|
+
.line-meta { grid-column: 1; }
|
|
534
|
+
.line--commit .line-main { grid-column: 1; }
|
|
535
|
+
.drawer { padding: 18px; }
|
|
536
|
+
dialog { padding: 22px; }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
@media (prefers-reduced-motion: reduce) {
|
|
540
|
+
.mark.state-active { animation: none; }
|
|
541
|
+
* { transition: none !important; }
|
|
542
|
+
.btn:active { transform: none; }
|
|
543
|
+
}
|