@ucsandman/legcli 0.15.1 → 0.16.1

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.
@@ -135,7 +135,7 @@
135
135
 
136
136
  /* type scale. Fixed rem, range 13 to 52. The old board ran 13 to 21, which
137
137
  is why it read as a spreadsheet */
138
- --t--1: 0.8125rem; /* 13px the row register and the scaffolding strip */
138
+ --t--1: 0.875rem; /* 14px the row register and the scaffolding strip */
139
139
  --t-0: 0.9375rem; /* 15px labels, meta, controls, the narrow floor */
140
140
  --t-1: 1.0625rem; /* 17px prompts, panel heads, body default */
141
141
  --t-2: 1.3125rem; /* 21px section heads, secondary readings */
@@ -191,13 +191,21 @@ code { font-family: var(--mono); font-size: 0.94em; }
191
191
  .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)); }
192
192
 
193
193
  .masthead { position: relative; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding-block: 28px 24px; }
194
- .brand { font-size: var(--t-1); font-weight: var(--w-head); letter-spacing: -.01em; }
195
- .brand-glyph { display: inline-block; margin-right: 4px; }
196
- .masthead-right { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
197
- .floor-link { color: var(--text-2); text-decoration: none; border-bottom: 1px solid var(--line-strong); padding-bottom: 1px; }
198
- .floor-link:hover { color: var(--text); border-bottom-color: var(--text-2); }
199
-
200
- .sse-status { display: inline-flex; align-items: center; gap: 8px; }
194
+ .brand { display: inline-flex; align-items: center; gap: 10px; font-size: var(--t-1); font-weight: var(--w-head); letter-spacing: -.01em; }
195
+ /* the mark: a tile with the L cut as hip, knee and foot. Inline SVG in the
196
+ markup, currentColor, so it is one shape on the board, the floor, the
197
+ favicon and the site */
198
+ .brand-glyph { display: inline-flex; width: 22px; height: 22px; color: var(--text); }
199
+ .brand-glyph svg { width: 22px; height: 22px; display: block; }
200
+ .masthead-right { display: flex; align-items: center; gap: 8px 24px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
201
+ /* connection and scheduler are one status cluster, read as one phrase */
202
+ .masthead-status { display: inline-flex; align-items: center; gap: 10px; color: var(--text-3); }
203
+ .masthead-status .sched-status:not(:empty)::before { content: ""; display: inline-block; width: 3px; height: 3px; border-radius: 999px; background: var(--line-strong); margin: 0 10px 3px 0; vertical-align: middle; }
204
+ /* the other page is a control, in the same clothes as every other quiet control */
205
+ .floor-link { display: inline-flex; align-items: center; min-height: 36px; padding: 0 14px; border: 1px solid var(--control-edge); border-radius: var(--r-control); background: var(--e3); color: var(--text); font-weight: var(--w-head); text-decoration: none; transition: background-color 150ms var(--ease), border-color 150ms var(--ease); }
206
+ .floor-link:hover { background: var(--e4); border-color: #7D828A; color: var(--text); }
207
+
208
+ .sse-status { display: inline-flex; align-items: center; gap: 8px; flex-direction: row-reverse; }
201
209
  .sse-word { color: var(--text-3); }
202
210
  .sse-rule { width: 8px; height: 8px; border-radius: 999px; background: var(--ok); flex: none; }
203
211
  .sse-rule.is-connecting { background: var(--warn); }
@@ -224,24 +232,24 @@ code { font-family: var(--mono); font-size: 0.94em; }
224
232
  /* the strip itself never wraps: the disclosure keeps the strip's own line at
225
233
  the right (A.2) and the tokens wrap inside their own box, so four logins
226
234
  that need two rows of tokens cost no third line for one button. */
227
- .capacity-strip { display: flex; align-items: center; gap: 12px 32px; flex-wrap: nowrap; min-height: 44px; font-size: var(--t-0); }
228
- .cap-tokens { display: flex; align-items: center; gap: 12px 32px; flex-wrap: wrap; flex: 1 1 auto; min-width: 0; }
229
- .cap-token { display: inline-flex; align-items: center; gap: 10px; min-height: 24px; }
235
+ /* the tokens are a small aligned table, dot, name, track, figure, so four
236
+ logins read as four rows of one instrument and not a ragged sentence. The
237
+ name is text: identity is the dot and the fill, never the colour of a word */
238
+ .capacity-strip { display: flex; align-items: flex-start; gap: 12px 32px; flex-wrap: nowrap; min-height: 44px; font-size: var(--t-0); }
239
+ .cap-tokens { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px 40px; flex: 1 1 auto; min-width: 0; }
240
+ /* the name column is as wide as the longest login on the strip (strip.js sets
241
+ --cap-name on the container): a second claude login reads "claude/work",
242
+ and a fixed 7ch put the slash over the track */
243
+ .cap-token { display: grid; grid-template-columns: 8px var(--cap-name, 7ch) 120px minmax(0, 1fr); align-items: center; gap: 10px; min-height: 28px; }
230
244
  .cap-name { font-weight: var(--w-head); color: var(--text-2); }
231
- .cap-name.id-claude { color: var(--id-claude); }
232
- .cap-name.id-codex { color: var(--id-codex); }
233
- .cap-name.id-agy { color: var(--id-agy); }
234
- .cap-name.id-grok { color: var(--id-grok); }
235
- .cap-name.id-copilot { color: var(--id-copilot); }
236
- .cap-name.id-fake { color: var(--id-fake); }
237
245
  .cap-track { position: relative; flex: none; width: 120px; height: 6px; background: var(--track); border-radius: 999px; box-shadow: var(--sink); overflow: hidden; }
238
246
  .cap-fill { position: absolute; inset: 0 auto 0 0; border-radius: 999px; background: var(--calm); }
239
- .cap-figure { color: var(--text-2); white-space: nowrap; }
247
+ .cap-figure { color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
240
248
  /* a login at its wall is the one place a word IS the failure, so it may carry
241
249
  the colour; a percentage never does, the track carries that */
242
250
  .cap-figure.is-out { color: var(--danger-text); }
243
- .cap-figure--none { color: var(--text-3); }
244
- .cap-open { margin-left: auto; flex: none; }
251
+ .cap-figure--none { color: var(--text-3); grid-column: 3 / -1; }
252
+ .cap-open { margin-left: auto; flex: none; margin-top: -4px; }
245
253
  .capacity-drawer { margin-top: 16px; }
246
254
 
247
255
  /* the model rail on a panel head inside the drawer: a measured percentage, or
@@ -297,12 +305,12 @@ code { font-family: var(--mono); font-size: 0.94em; }
297
305
  .reading-sub--lead { margin-top: 22px; }
298
306
 
299
307
  /* ---- section heads ------------------------------------------------------ */
300
- .section-head { padding-block: 32px 16px; }
308
+ .section-head { padding-block: 40px 14px; }
301
309
  .section-head h2 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
302
- .section-head .region-meta { margin-top: 8px; font-size: var(--t-0); color: var(--text-3); }
310
+ .section-head .region-meta { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); }
303
311
  .section-head--quiet { padding-block: 56px 16px; display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
304
312
  .section-head--quiet .region-meta { margin-top: 0; }
305
- .hoisted { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; font-size: var(--t-0); }
313
+ .hoisted { margin-top: 4px; display: flex; flex-direction: column; gap: 4px; font-size: var(--t-0); }
306
314
  .hoisted-note { color: var(--text-2); }
307
315
  .hoisted-note.tone-warn { color: var(--warn-text); }
308
316
  .hoisted-note.tone-danger { color: var(--danger-text); }
@@ -310,44 +318,59 @@ code { font-family: var(--mono); font-size: 0.94em; }
310
318
 
311
319
  /* ---- terminals: rows inside one panel, separated by the only rule left --- */
312
320
  .terms { padding: 4px 0; }
313
- .term { padding: 26px 32px; }
321
+ .term { padding: 22px 28px; transition: background-color 150ms var(--ease); }
314
322
  .term + .term { border-top: 1px solid var(--line); }
315
- .term.is-urgent { background: linear-gradient(to right, rgba(230,67,67,.07), transparent 40%); }
316
- .term-row { display: flex; align-items: center; gap: 28px; }
323
+ .term:hover { background: rgba(255,255,255,.015); }
324
+ /* a row whose state just changed is lit for a moment, then settles: the one
325
+ motion on the board, and it answers what moved while the reader was away */
326
+ .term.is-changed { animation: settle 1400ms var(--ease) 1; }
327
+ @keyframes settle { 0% { background-color: rgba(92,145,255,.14); } 100% { background-color: transparent; } }
328
+ .term-row { display: flex; align-items: flex-start; gap: 32px; }
317
329
  .term-body { flex: 1 1 auto; min-width: 0; }
318
330
  .term-row > .term-body { flex-basis: 24ch; }
319
331
  .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; }
320
- .term-register { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
332
+ .term-register { display: flex; align-items: center; gap: 6px 14px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
321
333
  .term-where { color: var(--text-2); }
322
334
  /* the register's data tokens (A.4 rows 7 to 9). They are one line with the
323
335
  status word, in the register's own muted tone: what changed and how long it
324
336
  has been quiet are observations, not demands. The model token is the one
325
337
  fact here a reader looks for by name, so it takes the identity colour and
326
338
  the mono face the other machine strings on this page use. */
327
- .term-dirty, .term-ahead, .term-quiet { color: var(--text-3); }
328
- .term-model { font-family: var(--mono); }
339
+ .term-dirty, .term-ahead, .term-quiet { color: var(--text-3); font-size: var(--t--1); }
340
+ /* the rung is a dot in the agent's colour beside its name in text, the same
341
+ grammar as the capacity strip; the chip-id class stays for the tests and the
342
+ dot, the colour no longer lands on the word */
343
+ .term-model { display: inline-flex; align-items: center; gap: 7px; color: var(--text-2); }
344
+ .term-model .dot { width: 7px; height: 7px; }
329
345
  /* the files line and the row's capacity phrase share one line, the phrase at
330
346
  the right (A.2). The phrase is per MODEL, which is a fact about this row;
331
347
  the region head carries the share clause for the login. */
332
- .term-meta { display: flex; align-items: baseline; justify-content: space-between; gap: 8px 24px; flex-wrap: wrap; }
333
- .term-meta .files { flex: 1 1 24ch; min-width: 0; }
348
+ .term-meta { display: flex; align-items: baseline; justify-content: flex-start; gap: 6px 24px; flex-wrap: wrap; margin-top: 6px; }
349
+ .term-meta .files { flex: 0 1 auto; min-width: 0; }
334
350
  /* the phrase wraps rather than pushing the row sideways: with a forecast it is
335
351
  `about 2h 40m of fable left, from 9 samples over 4h`, two and a half times
336
352
  the percentage form, and a nowrap run that long overflows the 400px board. */
337
- .term-capacity { margin-top: 10px; flex: none; display: inline-flex; align-items: baseline; gap: 10px; font-size: var(--t--1); color: var(--text-3); }
353
+ .term-capacity { margin-top: 4px; flex: 0 1 auto; min-width: 0; display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: 10px; font-size: var(--t--1); color: var(--text-3); }
338
354
  /* the climb is a link beside the figure that explains it, never a fifth button
339
355
  in the 2x2 grid, which is the shipped shape */
340
356
  .term-climb { font-size: var(--t--1); }
341
357
  /* the keyboard ring: a row the keys act on, marked on the edge the eye scans
342
358
  down rather than by a fill that would compete with is-urgent */
343
359
  .term.is-focused { box-shadow: inset 3px 0 0 var(--focus); }
344
- .term-when { color: var(--text-3); min-width: 6ch; text-align: right; }
360
+ .term-when { color: var(--text-3); text-align: right; }
345
361
  .term-id { color: var(--text-3); min-width: 4ch; text-align: right; font-family: var(--mono); }
346
- .term-actions { flex: none; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; width: 272px; }
347
- .term-actions .btn { white-space: nowrap; width: 100%; }
348
- /* a row with one or three controls keeps the grid honest rather than stretching
349
- the odd one across both columns */
350
- .term-actions .btn:only-child { grid-column: 1 / -1; }
362
+ /* the clock over the actions, both right-aligned, in one side column */
363
+ .term-side { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; }
364
+ .term-clock { font-family: var(--mono); gap: 10px; }
365
+ .term-actions { flex: none; display: flex; justify-content: flex-end; gap: 8px; }
366
+ .term-actions .btn { white-space: nowrap; }
367
+ /* a control that cannot run is not drawn: its reason is hoisted to the region
368
+ head (the blocker paragraph stays in the DOM for aria-describedby) */
369
+ .term-actions .btn:disabled { display: none; }
370
+ /* End and Remove are quiet at rest; the colour comes on hover, and the
371
+ confirm row is where the decision is made */
372
+ .term-actions .btn-danger { color: var(--text-2); }
373
+ .term-actions .btn-danger:hover { color: var(--danger-text); }
351
374
 
352
375
  .state { display: inline-flex; align-items: center; gap: 8px; color: var(--text-2); font-weight: var(--w-head); }
353
376
  .mark { width: 8px; height: 8px; border-radius: 999px; flex: none; background: var(--text-3); }
@@ -360,16 +383,22 @@ code { font-family: var(--mono); font-size: 0.94em; }
360
383
  }
361
384
  @keyframes breathe { 0%,100% { opacity: 1 } 50% { opacity: .45 } }
362
385
 
363
- .term-prompt { margin-top: 14px; font-size: var(--t-1); line-height: 1.45; max-width: 62ch; color: var(--text); }
386
+ .term-prompt { margin-top: 10px; font-size: var(--t-1); line-height: 1.45; max-width: 62ch; color: var(--text); }
364
387
  .term-prompt--empty { color: var(--text-3); }
365
- .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; }
388
+ /* two lines of the prompt, then the expansion: a pasted brief is read in
389
+ Details, not in the list */
390
+ .panel-prompt { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; width: 100%; text-align: left; background: none; border: 0; padding: 0; margin-top: 10px; font-size: var(--t-1); line-height: 1.45; max-width: 64ch; color: var(--text); cursor: pointer; }
391
+ .panel-prompt[aria-expanded="true"] { -webkit-line-clamp: unset; }
366
392
  .panel-prompt:hover { color: var(--accent-hi); }
367
393
 
368
- .sentence { margin-top: 10px; font-size: var(--t-0); color: var(--text-2); max-width: 64ch; }
394
+ .sentence { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); max-width: 64ch; }
369
395
  .sentence.tone-warn { color: var(--warn-text); }
370
396
  .sentence.tone-danger { color: var(--danger-text); }
371
397
  .sentence.tone-ok { color: var(--ok-text); }
372
398
  .sentence.tone-muted { color: var(--text-3); }
399
+ /* a warning sentence carries its colour only on the row that needs a person;
400
+ on a running row it is an observation, and an observation is not amber */
401
+ .term:not(.is-urgent) .sentence.tone-warn { color: var(--text-2); }
373
402
  .files { margin-top: 10px; font-size: var(--t--1); color: var(--text-3); }
374
403
  .file { color: var(--text-3); }
375
404
  .file.is-overlap { color: var(--warn-text); }
@@ -440,11 +469,15 @@ code { font-family: var(--mono); font-size: 0.94em; }
440
469
  .drawer-msg-when { font-size: var(--t--1); color: var(--text-3); font-family: var(--mono); }
441
470
 
442
471
  /* ---- ledger: on the ground, unpanelled --------------------------------- */
443
- .ledger { margin-top: 56px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 40px; padding-top: 32px; border-top: 1px solid var(--edge); }
444
- .ledger-cell h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
445
- .ledger-cell .region-meta { margin-top: 6px; font-size: var(--t-0); color: var(--text-3); }
446
- .ledger-actions { margin-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; }
447
- .ledger-cell .empty-line { margin-top: 12px; font-size: var(--t-0); color: var(--text-3); }
472
+ /* four cells of one anatomy: a count, a line of meta, and the verb on one
473
+ baseline whatever the meta's length */
474
+ .ledger { margin-top: 48px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 32px 40px; padding-top: 32px; border-top: 1px solid var(--edge); }
475
+ .ledger-cell { display: flex; flex-direction: column; }
476
+ .ledger-cell h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; line-height: 1.2; }
477
+ .ledger-cell .region-meta { margin-top: 6px; font-size: var(--t-0); color: var(--text-3); display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
478
+ .ledger-cell .region-meta:empty { display: none; }
479
+ .ledger-actions { margin-top: auto; padding-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; }
480
+ .ledger-cell .empty-line { margin-top: 6px; font-size: var(--t-0); color: var(--text-3); }
448
481
 
449
482
  .list { margin-top: 20px; }
450
483
  .list li { padding: 14px 0; font-size: var(--t-0); }
@@ -528,7 +561,9 @@ code { font-family: var(--mono); font-size: 0.94em; }
528
561
  .row-meta { font-size: var(--t-0); color: var(--text-3); display: flex; gap: 14px; flex-wrap: wrap; }
529
562
  .row-actions { margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap; }
530
563
  /* the demoted-notes disclosure in a terminal row */
531
- .also { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
564
+ .also { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
565
+ .term .also { margin-top: 2px; min-height: 28px; font-weight: var(--w-text); text-decoration-color: var(--line-strong); }
566
+ .term .also:hover { color: var(--text-2); }
532
567
  .status-word { font-weight: var(--w-head); color: var(--text-2); }
533
568
  .elapsed { font-family: var(--mono); color: var(--text-3); }
534
569
  .where { color: var(--text-2); }
@@ -671,10 +706,10 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
671
706
  120px of instrument costs the percentage its own line, and the percentage
672
707
  with its state word carries the whole fact on its own */
673
708
  .capacity-strip { display: block; }
674
- .cap-tokens { display: block; }
675
- .cap-token { display: flex; min-height: 24px; gap: 8px; }
709
+ .cap-tokens { grid-template-columns: 1fr; }
710
+ .cap-token { grid-template-columns: 8px var(--cap-name, 7ch) minmax(0, 1fr); }
676
711
  .cap-track { display: none; }
677
- .cap-figure { margin-left: auto; }
712
+ .cap-figure { grid-column: 3; text-align: right; }
678
713
  .cap-open { margin-left: 0; margin-top: 12px; }
679
714
  .panel { padding: 22px; border-radius: 14px; }
680
715
  .logins-pair { grid-template-columns: 1fr; }
@@ -689,16 +724,26 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
689
724
  code { font-size: 1em; }
690
725
  .section-head { padding-block: 24px 16px; }
691
726
  .section-head--quiet { padding-block: 24px 16px; }
692
- .term { padding: 22px; }
693
- .term-row { flex-direction: column; align-items: stretch; gap: 14px; }
694
- .term-clock { order: -1; width: auto; justify-content: flex-end; gap: 14px; }
727
+ .term { padding: 20px; }
728
+ .term-row { flex-direction: column; align-items: stretch; gap: 12px; }
729
+ /* the 24ch basis is a width in the row direction; stacked, it became 264px
730
+ of empty height under every row */
731
+ .term-row > .term-body { flex-basis: auto; }
732
+ /* the side column dissolves: the clock goes above the register and the
733
+ actions below the body, both in the row's own flow */
734
+ .term-side { display: contents; }
735
+ .term-clock { order: -1; width: auto; justify-content: flex-end; gap: 10px; }
736
+ .term-actions { order: 1; justify-content: flex-start; flex-wrap: wrap; }
695
737
  .term-id { min-width: 0; }
696
- .term-actions { width: auto; justify-content: flex-start; }
697
- .term-actions .btn { flex: 1 1 auto; }
698
738
  .term-register { gap: 10px 14px; }
699
739
  .kv { grid-template-columns: 1fr; gap: 4px 0; }
700
740
  .kv-val + .kv-key { margin-top: 12px; }
701
741
  .ledger { grid-template-columns: 1fr; gap: 32px; margin-top: 40px; }
742
+ .region-settings .section-head--quiet .disclosure { margin-left: 0; }
743
+ #card-entry { padding: 18px 20px; }
744
+ #card-entry .entry-opts .btn-text { margin-left: 0; }
745
+ .region-floor.is-empty { grid-template-columns: 1fr; gap: 6px; }
746
+ .first-run { padding: 22px; }
702
747
  .history-row { grid-template-columns: 1fr; }
703
748
  .history-filters { grid-template-columns: 1fr; }
704
749
  .line { grid-template-columns: 1fr auto; gap: 6px 14px; }
@@ -711,6 +756,7 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
711
756
 
712
757
  @media (prefers-reduced-motion: reduce) {
713
758
  .mark.state-active { animation: none; }
759
+ .term.is-changed { animation: none; }
714
760
  * { transition: none !important; }
715
761
  .btn:active { transform: none; }
716
762
  }
@@ -772,6 +818,7 @@ dialog { max-height: calc(100vh - 40px); overflow-y: auto; }
772
818
  rather than in the page's own ground. The board's copy is inside the
773
819
  Background panel and inherits its surface; this one has none to inherit. */
774
820
  .floor-entry { margin-top: 20px; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-well); padding: 20px 24px; }
821
+ .floor-entry .confirm-row { background: none; padding: 0; }
775
822
  .floor-entry .confirm-row { margin-top: 0; }
776
823
  .floor-entry .confirm-row input[type="text"] { flex: 1 1 320px; min-width: 0; }
777
824
  .floor-entry .field-help { margin-top: 10px; }
@@ -798,3 +845,81 @@ dialog { max-height: calc(100vh - 40px); overflow-y: auto; }
798
845
  a label with no control focuses nothing when it is clicked, which is the one
799
846
  thing a label promises. Same face as the row labels beside it. */
800
847
  .form-row .form-label { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
848
+
849
+ /* ---- 2026-09-19: the polish pass (.design/board-v5) --------------------- */
850
+ /* A disclosure says where it opens with a chevron drawn here, not a typed > */
851
+ .disclosure::after { content: ""; display: inline-block; width: 7px; height: 7px; margin-left: 10px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(45deg); transition: transform 150ms var(--ease); }
852
+ .disclosure[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }
853
+
854
+ /* The verdict names a row; the sentence is the way to that row. A button in
855
+ the h1's own clothes: no underline, the accent only on hover and focus. */
856
+ .verdict-link { font: inherit; letter-spacing: inherit; color: inherit; background: none; border: 0; padding: 0; text-align: left; cursor: pointer; text-wrap: inherit; }
857
+ .verdict-link:hover { color: var(--accent-hi); }
858
+
859
+ /* The background entry: on the board it is the terminals panel's own footer
860
+ (a sibling, joined to the panel above it); under the Background panel, and
861
+ on the floor, it is a well of its own. Its choices are quiet chips inside
862
+ the sentence, and More settings stays the one text link. */
863
+ #card-entry { margin-top: 12px; background: var(--e1); border: 1px solid var(--edge); border-top-color: var(--line); border-radius: var(--r-well); padding: 20px 28px 18px; box-shadow: var(--sink); }
864
+ /* entry.js parks the entry right after the Terminals section; when the panel
865
+ above it has rows the two join into one object, and an empty panel is not
866
+ drawn at all */
867
+ #session-grid:empty { display: none; }
868
+ .region-terminals:has(#session-grid:not(:empty)):has(+ #card-entry:not([hidden])) #session-grid { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
869
+ .region-terminals:has(#session-grid:not(:empty)) + #card-entry { border-radius: 0 0 var(--r-panel) var(--r-panel); margin-top: -1px; }
870
+ #background #card-entry { margin-top: 14px; }
871
+ #card-entry .confirm-row { margin: 0; background: none; padding: 0; gap: 12px; }
872
+ #card-entry .confirm-row > span:first-child { font-weight: var(--w-head); color: var(--text-2); }
873
+ #card-entry .confirm-row input[type="text"] { flex: 1 1 280px; min-width: 0; background: var(--e0); }
874
+ #card-entry .confirm-row .field-help { flex: 1 0 100%; margin: 0; }
875
+ #card-entry .entry-opts { margin-top: 12px; display: flex; align-items: center; gap: 8px 6px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
876
+ #card-entry .entry-opts .btn-sm { min-height: 30px; font-weight: var(--w-text); color: var(--text-2); background: var(--e2); border-color: var(--edge-hi); }
877
+ #card-entry .entry-opts .btn-sm:hover { color: var(--text); background: var(--e3); }
878
+ #card-entry .entry-opts .btn-text { min-height: 30px; }
879
+ #card-entry .entry-opts select { font-size: var(--t--1); min-height: 30px; padding: 4px 8px; }
880
+
881
+ /* First run. A board with no terminal is the first thing a new reader sees,
882
+ so it is one lit panel with the three commands and a Copy each, not a grey
883
+ sentence. sessions.js hides it the moment a terminal is live. */
884
+ .first-run { margin-top: 0; padding: 28px 32px; }
885
+ .first-run h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
886
+ .first-run p { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); max-width: 56ch; }
887
+ .first-run-cmds { margin-top: 20px; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
888
+ .first-run-cmd { display: flex; align-items: center; justify-content: space-between; gap: 12px; background: var(--e0); border: 1px solid var(--edge); border-radius: var(--r-control); padding: 10px 10px 10px 16px; }
889
+ .first-run-cmd code { font-size: var(--t-0); color: var(--text); }
890
+ .first-run-cmd .dot { margin-right: 10px; }
891
+ .first-run-note { margin-top: 16px; font-size: var(--t--1); color: var(--text-3); }
892
+
893
+ /* Settings is the last ledger row, not a stray sentence with a button */
894
+ .region-settings .section-head--quiet { margin-top: 40px; padding-block: 24px 0; border-top: 1px solid var(--edge); align-items: center; }
895
+ .region-settings .section-head--quiet h2 { font-size: var(--t-2); }
896
+ .region-settings .section-head--quiet .region-meta { flex: 1 1 auto; }
897
+ .region-settings .section-head--quiet .disclosure { margin-left: auto; }
898
+
899
+ /* The floor: an empty station is one line, heading and sentence side by side,
900
+ so seven empty stations are seven lines and not seven screens. A station
901
+ with rows keeps its full section. floor.js sets is-empty. */
902
+ .region-floor.is-empty { display: grid; grid-template-columns: 16ch minmax(0, 1fr); gap: 8px 24px; align-items: baseline; padding: 18px 0; margin-top: 0; border-top: 1px solid var(--edge); }
903
+ .region-floor.is-empty .section-head { padding: 0; display: flex; align-items: baseline; gap: 6px 12px; flex-wrap: wrap; }
904
+ .region-floor.is-empty .section-head h2 { font-size: var(--t-1); white-space: nowrap; }
905
+ .region-floor.is-empty .section-head--quiet .btn { margin-left: 4px; min-height: 30px; }
906
+ .region-floor.is-empty .region-meta:empty, .region-floor.is-empty .card-list:empty, .region-floor.is-empty .ftable { display: none; }
907
+ .region-floor.is-empty .empty-line { margin: 0; color: var(--text-3); }
908
+ .region-floor:not(.is-empty) + .region-floor.is-empty { margin-top: 24px; }
909
+ .masthead-left { font-size: var(--t-0); }
910
+ /* ---- end of the 2026-09-19 block ---------------------------------------- */
911
+
912
+ /* ---- 2026-09-19, second pass: what the rendered seed showed ------------- */
913
+ /* the rung word and the ladder's rung names are text: the chip-id class was
914
+ winning on order, and the identity dot beside each one already says who */
915
+ .term-model[class*="chip-id-"], .ladder-name[class*="chip-id-"] { color: var(--text-2); }
916
+ /* the cards cell's verb sits on the baseline the other cells share, whatever
917
+ order its markup arrives in */
918
+ .ledger-cell .ledger-actions { order: 9; }
919
+ /* a number field wears the same clothes as a text field */
920
+ input[type="number"] { 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; width: 7ch; }
921
+ /* a finished row has no Land, so its land blocker is read by nothing on screen */
922
+ #finished-list .blocker { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
923
+ /* the entry's help line sits after Start on the same row */
924
+ #card-entry .confirm-row .field-help { flex: 0 1 auto; }
925
+ /* ---- end of the second pass ----------------------------------------------- */
@@ -68,7 +68,7 @@
68
68
  // release lands on disk under a board that was started before it: the page
69
69
  // then draws controls the process has no routes for (an empty agent select,
70
70
  // no buckets). test/files-version.test.mjs pins this to package.json.
71
- const FILES_VERSION = '0.15.1'
71
+ const FILES_VERSION = '0.16.1'
72
72
  const TIMELINE_CAP = 12
73
73
  // mirrors LOOPBACK in src/auth.mjs; state.bind is "<host>:<port>" and an IPv6
74
74
  // host arrives bracketed
@@ -1744,7 +1744,10 @@
1744
1744
  initSettings()
1745
1745
  initNewCardDialog()
1746
1746
  document.getElementById('new-card-btn').addEventListener('click', () => openNewCardDialog())
1747
- document.getElementById('empty-new-card-btn').addEventListener('click', () => openNewCardDialog())
1747
+ // first run: each command has a Copy, the same clipboard path the drawer uses
1748
+ for (const b of document.querySelectorAll('#first-run [data-copy]')) {
1749
+ b.addEventListener('click', () => { copyToClipboard(b.getAttribute('data-copy')); b.textContent = 'Copied'; setTimeout(() => { b.textContent = 'Copy' }, 1400) })
1750
+ }
1748
1751
  document.getElementById('drawer-close').addEventListener('click', () => collapseRow())
1749
1752
  document.addEventListener('keydown', (e) => {
1750
1753
  if (e.key !== 'Escape') return
@@ -234,7 +234,7 @@
234
234
  }
235
235
 
236
236
  function nounButton(text, key) {
237
- return el('button', { type: 'button', class: 'btn btn-text', onclick: () => { entryState.editing = key; renderEntryLine() } }, [text])
237
+ return el('button', { type: 'button', class: 'btn btn-secondary btn-sm entry-noun', onclick: () => { entryState.editing = key; renderEntryLine() } }, [text])
238
238
  }
239
239
 
240
240
  function renderEntryLine() {
@@ -262,7 +262,7 @@
262
262
  // flex row with a gap on the raised surface, which is exactly this line.
263
263
  box.appendChild(el('div', { class: 'confirm-row' }, [el('span', {}, ['Run in the background:']), task, start, reason]))
264
264
 
265
- const line = el('p', { class: 'field-help' })
265
+ const line = el('p', { class: 'field-help entry-opts' })
266
266
  line.appendChild(document.createTextNode('in '))
267
267
  if (entryState.editing === 'repo') {
268
268
  line.appendChild(nounSelect(knownRepos().map((r) => ({ value: r.path, label: r.name })), repo ? repo.path : '', (v) => { entryState.repo = knownRepos().find((r) => r.path === v) || null }))
@@ -297,13 +297,13 @@
297
297
  // the sentence names the legs Start posts, models and all
298
298
  line.appendChild(nounButton(ladderSentence(entryChain()), 'ladder'))
299
299
  }
300
- line.appendChild(document.createTextNode(', '))
300
+ line.appendChild(document.createTextNode(' '))
301
301
  if (entryState.editing === 'pipeline') {
302
302
  line.appendChild(nounSelect(Object.keys(PIPELINE_WORDS).map((v) => ({ value: v, label: PIPELINE_WORDS[v] })), entryState.pipeline, (v) => { entryState.pipeline = v }))
303
303
  } else {
304
304
  line.appendChild(nounButton(PIPELINE_WORDS[entryState.pipeline] || 'build only', 'pipeline'))
305
305
  }
306
- line.appendChild(document.createTextNode('. '))
306
+ line.appendChild(document.createTextNode(' '))
307
307
  line.appendChild(el('button', { type: 'button', class: 'btn btn-text', onclick: () => onMoreSettings(entryState) }, ['More settings']))
308
308
  box.appendChild(line)
309
309
  }
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🦿</text></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#F3F4F7"/><path d="M8 5.5v8.5l4 4h5" stroke="#0E1012" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
@@ -11,7 +11,7 @@
11
11
 
12
12
  <header class="masthead wrap">
13
13
  <div class="masthead-left">
14
- <h1 class="brand"><span class="brand-glyph" aria-hidden="true">🦿</span> Leg floor</h1>
14
+ <h1 class="brand"><span class="brand-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><rect width="24" height="24" rx="6" fill="currentColor"/><path d="M8 5.5v8.5l4 4h5" stroke="#0E1012" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span> Leg floor</h1>
15
15
  <!-- the connection state sits second, immediately after the brand, the same
16
16
  place it sits on the board: the two bars are read as one bar. -->
17
17
  <div class="sse-status" role="status">
@@ -43,7 +43,7 @@
43
43
  <section class="capacity" aria-label="Capacity by login">
44
44
  <div class="capacity-strip">
45
45
  <div class="cap-tokens" id="capacity-tokens"></div>
46
- <button type="button" class="btn btn-secondary disclosure cap-open" id="capacity-toggle" aria-expanded="false" aria-controls="capacity-drawer">Capacity and models &gt;</button>
46
+ <button type="button" class="btn btn-secondary disclosure cap-open" id="capacity-toggle" aria-expanded="false" aria-controls="capacity-drawer">Capacity and models</button>
47
47
  </div>
48
48
  <div class="drawer capacity-drawer" id="capacity-drawer" hidden>
49
49
  <section class="logins" id="floor-accounts" aria-label="Logins"></section>
@@ -50,7 +50,7 @@
50
50
  // name. test/board-updates.test.mjs pins `state.stopped` in this file by
51
51
  // source text, which is why the module object keeps the name.
52
52
  // pinned to package.json by test/files-version.test.mjs; see board.js FILES_VERSION
53
- const FILES_VERSION = '0.15.1'
53
+ const FILES_VERSION = '0.16.1'
54
54
  const state = { es: null, retryMs: 1000, timers: [], stopped: false, sseRequest: 0, floorRequest: 0, trunkRequest: 0, headRequest: 0, cardsRequest: 0, lastReadingAt: null, bind: (typeof location !== 'undefined' && location.host) || '127.0.0.1:4747', pendingFloor: null, pendingCards: null, cards: new Map(), blockers: new Map(), doneOpen: false, ringId: null, scheduler: {}, trunkOff: false }
55
55
 
56
56
  // What the entry row reads on every render: the terminals (for the repo it
@@ -443,6 +443,8 @@
443
443
  const table = body.parentElement
444
444
  const section = table && table.parentElement
445
445
  if (section) { const old = section.querySelector('.empty-line'); if (old) old.remove() }
446
+ // an empty station is one line (board.css .region-floor.is-empty)
447
+ if (section && section.classList) section.classList.toggle('is-empty', !list.length)
446
448
  const thead = table && table.querySelector('thead')
447
449
  if (!list.length) {
448
450
  if (thead) thead.hidden = true
@@ -747,6 +749,7 @@
747
749
  const count = document.getElementById(station.count)
748
750
  if (count) count.textContent = String(cards.length)
749
751
  if (section && section.querySelector) { const old = section.querySelector('.empty-line'); if (old && old.remove) old.remove() }
752
+ if (section && section.classList) section.classList.toggle('is-empty', !cards.length)
750
753
  if (!cards.length) {
751
754
  box.textContent = ''
752
755
  box.signature = ''
@@ -304,7 +304,7 @@
304
304
  ? `${byAgent}${filtered ? `; ${state.total} match the filters` : ''}${state.refreshedAt ? `; looked ${whenAgo(state.refreshedAt)}` : ''}`
305
305
  : 'Leg looks in the Claude Code, Codex, Grok, Antigravity and Copilot homes on this machine, and its own sessions.'
306
306
  const btn = el('button', { type: 'button', class: 'btn btn-secondary', 'aria-expanded': state.open ? 'true' : 'false', 'aria-controls': 'history-drawer', 'data-focus-key': 'history-toggle' },
307
- [state.open ? 'Hide conversations' : (all ? `Browse ${all}` : 'Browse')])
307
+ [state.open ? 'Hide' : 'Browse'])
308
308
  btn.addEventListener('click', () => { state.open = !state.open; if (state.open && state.worktrees === null) loadWorktrees(); render() })
309
309
  slot.appendChild(btn)
310
310
  panel.hidden = !state.open
@@ -18,13 +18,15 @@
18
18
  <div class="wrap">
19
19
 
20
20
  <header class="masthead">
21
- <p class="brand"><span class="brand-glyph" aria-hidden="true">🦿</span> Leg</p>
21
+ <p class="brand"><span class="brand-glyph" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true"><rect width="24" height="24" rx="6" fill="currentColor"/><path d="M8 5.5v8.5l4 4h5" stroke="#0E1012" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></span> Leg</p>
22
22
  <div class="masthead-right">
23
- <div class="sse-status" role="status">
24
- <span class="sse-word" id="sse-text">connecting</span>
25
- <span class="sse-rule is-connecting" id="sse-dot" aria-hidden="true"></span>
23
+ <div class="masthead-status">
24
+ <div class="sse-status" role="status">
25
+ <span class="sse-word" id="sse-text">connecting</span>
26
+ <span class="sse-rule is-connecting" id="sse-dot" aria-hidden="true"></span>
27
+ </div>
28
+ <div class="sched-status" id="sched-status"></div>
26
29
  </div>
27
- <div class="sched-status" id="sched-status"></div>
28
30
  <a class="floor-link" href="/floor">Floor</a>
29
31
  </div>
30
32
  </header>
@@ -46,7 +48,7 @@
46
48
  <section class="capacity" aria-label="Capacity by login">
47
49
  <div class="capacity-strip">
48
50
  <div class="cap-tokens" id="capacity-tokens"></div>
49
- <button type="button" class="btn btn-secondary disclosure cap-open" id="capacity-toggle" aria-expanded="false" aria-controls="capacity-drawer">Capacity and models &gt;</button>
51
+ <button type="button" class="btn btn-secondary disclosure cap-open" id="capacity-toggle" aria-expanded="false" aria-controls="capacity-drawer">Capacity and models</button>
50
52
  </div>
51
53
  <div class="drawer capacity-drawer" id="capacity-drawer" hidden>
52
54
  <section class="logins" id="accounts" aria-label="Logins"></section>
@@ -67,7 +69,19 @@
67
69
  <span class="chip" id="whoami" hidden></span>
68
70
  </div>
69
71
  <div class="panel terms" id="session-grid"></div>
70
- <p class="empty-line" hidden>No terminal is running. Start one in any repo: <code>leg claude</code>, <code>leg codex</code> or <code>leg agy</code>. It appears here within a second of the agent's first turn.</p>
72
+ <!-- first run: the board with no terminal is the first screen a new reader
73
+ sees, so it is a lit panel with the commands and a Copy each. sessions.js
74
+ hides it the moment a terminal is live; board.js wires Copy. -->
75
+ <div class="panel panel--lit first-run" id="first-run" hidden>
76
+ <h3>No terminal is running</h3>
77
+ <p>Start one in any repo with the same command you use today, and Leg runs it with the board alongside. It appears here within a second of the agent's first turn.</p>
78
+ <div class="first-run-cmds">
79
+ <div class="first-run-cmd"><span><span class="dot id-claude" aria-hidden="true"></span><code>leg claude</code></span><button type="button" class="btn btn-secondary btn-sm" data-copy="leg claude" aria-label="Copy leg claude">Copy</button></div>
80
+ <div class="first-run-cmd"><span><span class="dot id-codex" aria-hidden="true"></span><code>leg codex</code></span><button type="button" class="btn btn-secondary btn-sm" data-copy="leg codex" aria-label="Copy leg codex">Copy</button></div>
81
+ <div class="first-run-cmd"><span><span class="dot id-agy" aria-hidden="true"></span><code>leg agy</code></span><button type="button" class="btn btn-secondary btn-sm" data-copy="leg agy" aria-label="Copy leg agy">Copy</button></div>
82
+ </div>
83
+ <p class="first-run-note">Usage per login, the handoff ladder and the context bundle all start with the first terminal.</p>
84
+ </div>
71
85
  </section>
72
86
 
73
87
  <!-- C.1: a card is a terminal you are not sitting at. Live cards are rows
@@ -118,7 +132,7 @@
118
132
  <div class="ledger-actions">
119
133
  <button type="button" class="btn btn-secondary" id="new-card-btn">New card</button>
120
134
  </div>
121
- <p class="empty-line" id="empty-state" hidden>A card runs an agent headless in its own worktree. <button type="button" class="btn btn-text" id="empty-new-card-btn">New card</button> queues one.</p>
135
+ <p class="empty-line" id="empty-state" hidden>A card runs an agent headless in its own worktree.</p>
122
136
  </div>
123
137
 
124
138
  </section>