@ucsandman/legcli 0.15.0 → 0.16.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.
@@ -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,21 @@ 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
+ .cap-token { display: grid; grid-template-columns: 8px 7ch 120px minmax(0, 1fr); align-items: center; gap: 10px; min-height: 28px; }
230
241
  .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
242
  .cap-track { position: relative; flex: none; width: 120px; height: 6px; background: var(--track); border-radius: 999px; box-shadow: var(--sink); overflow: hidden; }
238
243
  .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; }
244
+ .cap-figure { color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
240
245
  /* a login at its wall is the one place a word IS the failure, so it may carry
241
246
  the colour; a percentage never does, the track carries that */
242
247
  .cap-figure.is-out { color: var(--danger-text); }
243
- .cap-figure--none { color: var(--text-3); }
244
- .cap-open { margin-left: auto; flex: none; }
248
+ .cap-figure--none { color: var(--text-3); grid-column: 3 / -1; }
249
+ .cap-open { margin-left: auto; flex: none; margin-top: -4px; }
245
250
  .capacity-drawer { margin-top: 16px; }
246
251
 
247
252
  /* the model rail on a panel head inside the drawer: a measured percentage, or
@@ -297,12 +302,12 @@ code { font-family: var(--mono); font-size: 0.94em; }
297
302
  .reading-sub--lead { margin-top: 22px; }
298
303
 
299
304
  /* ---- section heads ------------------------------------------------------ */
300
- .section-head { padding-block: 32px 16px; }
305
+ .section-head { padding-block: 40px 14px; }
301
306
  .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); }
307
+ .section-head .region-meta { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); }
303
308
  .section-head--quiet { padding-block: 56px 16px; display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
304
309
  .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); }
310
+ .hoisted { margin-top: 4px; display: flex; flex-direction: column; gap: 4px; font-size: var(--t-0); }
306
311
  .hoisted-note { color: var(--text-2); }
307
312
  .hoisted-note.tone-warn { color: var(--warn-text); }
308
313
  .hoisted-note.tone-danger { color: var(--danger-text); }
@@ -310,44 +315,59 @@ code { font-family: var(--mono); font-size: 0.94em; }
310
315
 
311
316
  /* ---- terminals: rows inside one panel, separated by the only rule left --- */
312
317
  .terms { padding: 4px 0; }
313
- .term { padding: 26px 32px; }
318
+ .term { padding: 22px 28px; transition: background-color 150ms var(--ease); }
314
319
  .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; }
320
+ .term:hover { background: rgba(255,255,255,.015); }
321
+ /* a row whose state just changed is lit for a moment, then settles: the one
322
+ motion on the board, and it answers what moved while the reader was away */
323
+ .term.is-changed { animation: settle 1400ms var(--ease) 1; }
324
+ @keyframes settle { 0% { background-color: rgba(92,145,255,.14); } 100% { background-color: transparent; } }
325
+ .term-row { display: flex; align-items: flex-start; gap: 32px; }
317
326
  .term-body { flex: 1 1 auto; min-width: 0; }
318
327
  .term-row > .term-body { flex-basis: 24ch; }
319
328
  .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); }
329
+ .term-register { display: flex; align-items: center; gap: 6px 14px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
321
330
  .term-where { color: var(--text-2); }
322
331
  /* the register's data tokens (A.4 rows 7 to 9). They are one line with the
323
332
  status word, in the register's own muted tone: what changed and how long it
324
333
  has been quiet are observations, not demands. The model token is the one
325
334
  fact here a reader looks for by name, so it takes the identity colour and
326
335
  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); }
336
+ .term-dirty, .term-ahead, .term-quiet { color: var(--text-3); font-size: var(--t--1); }
337
+ /* the rung is a dot in the agent's colour beside its name in text, the same
338
+ grammar as the capacity strip; the chip-id class stays for the tests and the
339
+ dot, the colour no longer lands on the word */
340
+ .term-model { display: inline-flex; align-items: center; gap: 7px; color: var(--text-2); }
341
+ .term-model .dot { width: 7px; height: 7px; }
329
342
  /* the files line and the row's capacity phrase share one line, the phrase at
330
343
  the right (A.2). The phrase is per MODEL, which is a fact about this row;
331
344
  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; }
345
+ .term-meta { display: flex; align-items: baseline; justify-content: flex-start; gap: 6px 24px; flex-wrap: wrap; margin-top: 6px; }
346
+ .term-meta .files { flex: 0 1 auto; min-width: 0; }
334
347
  /* the phrase wraps rather than pushing the row sideways: with a forecast it is
335
348
  `about 2h 40m of fable left, from 9 samples over 4h`, two and a half times
336
349
  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); }
350
+ .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
351
  /* the climb is a link beside the figure that explains it, never a fifth button
339
352
  in the 2x2 grid, which is the shipped shape */
340
353
  .term-climb { font-size: var(--t--1); }
341
354
  /* the keyboard ring: a row the keys act on, marked on the edge the eye scans
342
355
  down rather than by a fill that would compete with is-urgent */
343
356
  .term.is-focused { box-shadow: inset 3px 0 0 var(--focus); }
344
- .term-when { color: var(--text-3); min-width: 6ch; text-align: right; }
357
+ .term-when { color: var(--text-3); text-align: right; }
345
358
  .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; }
359
+ /* the clock over the actions, both right-aligned, in one side column */
360
+ .term-side { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; }
361
+ .term-clock { font-family: var(--mono); gap: 10px; }
362
+ .term-actions { flex: none; display: flex; justify-content: flex-end; gap: 8px; }
363
+ .term-actions .btn { white-space: nowrap; }
364
+ /* a control that cannot run is not drawn: its reason is hoisted to the region
365
+ head (the blocker paragraph stays in the DOM for aria-describedby) */
366
+ .term-actions .btn:disabled { display: none; }
367
+ /* End and Remove are quiet at rest; the colour comes on hover, and the
368
+ confirm row is where the decision is made */
369
+ .term-actions .btn-danger { color: var(--text-2); }
370
+ .term-actions .btn-danger:hover { color: var(--danger-text); }
351
371
 
352
372
  .state { display: inline-flex; align-items: center; gap: 8px; color: var(--text-2); font-weight: var(--w-head); }
353
373
  .mark { width: 8px; height: 8px; border-radius: 999px; flex: none; background: var(--text-3); }
@@ -360,16 +380,22 @@ code { font-family: var(--mono); font-size: 0.94em; }
360
380
  }
361
381
  @keyframes breathe { 0%,100% { opacity: 1 } 50% { opacity: .45 } }
362
382
 
363
- .term-prompt { margin-top: 14px; font-size: var(--t-1); line-height: 1.45; max-width: 62ch; color: var(--text); }
383
+ .term-prompt { margin-top: 10px; font-size: var(--t-1); line-height: 1.45; max-width: 62ch; color: var(--text); }
364
384
  .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; }
385
+ /* two lines of the prompt, then the expansion: a pasted brief is read in
386
+ Details, not in the list */
387
+ .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; }
388
+ .panel-prompt[aria-expanded="true"] { -webkit-line-clamp: unset; }
366
389
  .panel-prompt:hover { color: var(--accent-hi); }
367
390
 
368
- .sentence { margin-top: 10px; font-size: var(--t-0); color: var(--text-2); max-width: 64ch; }
391
+ .sentence { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); max-width: 64ch; }
369
392
  .sentence.tone-warn { color: var(--warn-text); }
370
393
  .sentence.tone-danger { color: var(--danger-text); }
371
394
  .sentence.tone-ok { color: var(--ok-text); }
372
395
  .sentence.tone-muted { color: var(--text-3); }
396
+ /* a warning sentence carries its colour only on the row that needs a person;
397
+ on a running row it is an observation, and an observation is not amber */
398
+ .term:not(.is-urgent) .sentence.tone-warn { color: var(--text-2); }
373
399
  .files { margin-top: 10px; font-size: var(--t--1); color: var(--text-3); }
374
400
  .file { color: var(--text-3); }
375
401
  .file.is-overlap { color: var(--warn-text); }
@@ -440,11 +466,15 @@ code { font-family: var(--mono); font-size: 0.94em; }
440
466
  .drawer-msg-when { font-size: var(--t--1); color: var(--text-3); font-family: var(--mono); }
441
467
 
442
468
  /* ---- 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); }
469
+ /* four cells of one anatomy: a count, a line of meta, and the verb on one
470
+ baseline whatever the meta's length */
471
+ .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); }
472
+ .ledger-cell { display: flex; flex-direction: column; }
473
+ .ledger-cell h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; line-height: 1.2; }
474
+ .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; }
475
+ .ledger-cell .region-meta:empty { display: none; }
476
+ .ledger-actions { margin-top: auto; padding-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; }
477
+ .ledger-cell .empty-line { margin-top: 6px; font-size: var(--t-0); color: var(--text-3); }
448
478
 
449
479
  .list { margin-top: 20px; }
450
480
  .list li { padding: 14px 0; font-size: var(--t-0); }
@@ -528,7 +558,9 @@ code { font-family: var(--mono); font-size: 0.94em; }
528
558
  .row-meta { font-size: var(--t-0); color: var(--text-3); display: flex; gap: 14px; flex-wrap: wrap; }
529
559
  .row-actions { margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap; }
530
560
  /* 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); }
561
+ .also { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
562
+ .term .also { margin-top: 2px; min-height: 28px; font-weight: var(--w-text); text-decoration-color: var(--line-strong); }
563
+ .term .also:hover { color: var(--text-2); }
532
564
  .status-word { font-weight: var(--w-head); color: var(--text-2); }
533
565
  .elapsed { font-family: var(--mono); color: var(--text-3); }
534
566
  .where { color: var(--text-2); }
@@ -671,10 +703,10 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
671
703
  120px of instrument costs the percentage its own line, and the percentage
672
704
  with its state word carries the whole fact on its own */
673
705
  .capacity-strip { display: block; }
674
- .cap-tokens { display: block; }
675
- .cap-token { display: flex; min-height: 24px; gap: 8px; }
706
+ .cap-tokens { grid-template-columns: 1fr; }
707
+ .cap-token { grid-template-columns: 8px 7ch minmax(0, 1fr); }
676
708
  .cap-track { display: none; }
677
- .cap-figure { margin-left: auto; }
709
+ .cap-figure { grid-column: 3; text-align: right; }
678
710
  .cap-open { margin-left: 0; margin-top: 12px; }
679
711
  .panel { padding: 22px; border-radius: 14px; }
680
712
  .logins-pair { grid-template-columns: 1fr; }
@@ -689,16 +721,26 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
689
721
  code { font-size: 1em; }
690
722
  .section-head { padding-block: 24px 16px; }
691
723
  .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; }
724
+ .term { padding: 20px; }
725
+ .term-row { flex-direction: column; align-items: stretch; gap: 12px; }
726
+ /* the 24ch basis is a width in the row direction; stacked, it became 264px
727
+ of empty height under every row */
728
+ .term-row > .term-body { flex-basis: auto; }
729
+ /* the side column dissolves: the clock goes above the register and the
730
+ actions below the body, both in the row's own flow */
731
+ .term-side { display: contents; }
732
+ .term-clock { order: -1; width: auto; justify-content: flex-end; gap: 10px; }
733
+ .term-actions { order: 1; justify-content: flex-start; flex-wrap: wrap; }
695
734
  .term-id { min-width: 0; }
696
- .term-actions { width: auto; justify-content: flex-start; }
697
- .term-actions .btn { flex: 1 1 auto; }
698
735
  .term-register { gap: 10px 14px; }
699
736
  .kv { grid-template-columns: 1fr; gap: 4px 0; }
700
737
  .kv-val + .kv-key { margin-top: 12px; }
701
738
  .ledger { grid-template-columns: 1fr; gap: 32px; margin-top: 40px; }
739
+ .region-settings .section-head--quiet .disclosure { margin-left: 0; }
740
+ #card-entry { padding: 18px 20px; }
741
+ #card-entry .entry-opts .btn-text { margin-left: 0; }
742
+ .region-floor.is-empty { grid-template-columns: 1fr; gap: 6px; }
743
+ .first-run { padding: 22px; }
702
744
  .history-row { grid-template-columns: 1fr; }
703
745
  .history-filters { grid-template-columns: 1fr; }
704
746
  .line { grid-template-columns: 1fr auto; gap: 6px 14px; }
@@ -711,6 +753,7 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
711
753
 
712
754
  @media (prefers-reduced-motion: reduce) {
713
755
  .mark.state-active { animation: none; }
756
+ .term.is-changed { animation: none; }
714
757
  * { transition: none !important; }
715
758
  .btn:active { transform: none; }
716
759
  }
@@ -772,6 +815,7 @@ dialog { max-height: calc(100vh - 40px); overflow-y: auto; }
772
815
  rather than in the page's own ground. The board's copy is inside the
773
816
  Background panel and inherits its surface; this one has none to inherit. */
774
817
  .floor-entry { margin-top: 20px; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-well); padding: 20px 24px; }
818
+ .floor-entry .confirm-row { background: none; padding: 0; }
775
819
  .floor-entry .confirm-row { margin-top: 0; }
776
820
  .floor-entry .confirm-row input[type="text"] { flex: 1 1 320px; min-width: 0; }
777
821
  .floor-entry .field-help { margin-top: 10px; }
@@ -798,3 +842,81 @@ dialog { max-height: calc(100vh - 40px); overflow-y: auto; }
798
842
  a label with no control focuses nothing when it is clicked, which is the one
799
843
  thing a label promises. Same face as the row labels beside it. */
800
844
  .form-row .form-label { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
845
+
846
+ /* ---- 2026-09-19: the polish pass (.design/board-v5) --------------------- */
847
+ /* A disclosure says where it opens with a chevron drawn here, not a typed > */
848
+ .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); }
849
+ .disclosure[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }
850
+
851
+ /* The verdict names a row; the sentence is the way to that row. A button in
852
+ the h1's own clothes: no underline, the accent only on hover and focus. */
853
+ .verdict-link { font: inherit; letter-spacing: inherit; color: inherit; background: none; border: 0; padding: 0; text-align: left; cursor: pointer; text-wrap: inherit; }
854
+ .verdict-link:hover { color: var(--accent-hi); }
855
+
856
+ /* The background entry: on the board it is the terminals panel's own footer
857
+ (a sibling, joined to the panel above it); under the Background panel, and
858
+ on the floor, it is a well of its own. Its choices are quiet chips inside
859
+ the sentence, and More settings stays the one text link. */
860
+ #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); }
861
+ /* entry.js parks the entry right after the Terminals section; when the panel
862
+ above it has rows the two join into one object, and an empty panel is not
863
+ drawn at all */
864
+ #session-grid:empty { display: none; }
865
+ .region-terminals:has(#session-grid:not(:empty)):has(+ #card-entry:not([hidden])) #session-grid { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
866
+ .region-terminals:has(#session-grid:not(:empty)) + #card-entry { border-radius: 0 0 var(--r-panel) var(--r-panel); margin-top: -1px; }
867
+ #background #card-entry { margin-top: 14px; }
868
+ #card-entry .confirm-row { margin: 0; background: none; padding: 0; gap: 12px; }
869
+ #card-entry .confirm-row > span:first-child { font-weight: var(--w-head); color: var(--text-2); }
870
+ #card-entry .confirm-row input[type="text"] { flex: 1 1 280px; min-width: 0; background: var(--e0); }
871
+ #card-entry .confirm-row .field-help { flex: 1 0 100%; margin: 0; }
872
+ #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); }
873
+ #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); }
874
+ #card-entry .entry-opts .btn-sm:hover { color: var(--text); background: var(--e3); }
875
+ #card-entry .entry-opts .btn-text { min-height: 30px; }
876
+ #card-entry .entry-opts select { font-size: var(--t--1); min-height: 30px; padding: 4px 8px; }
877
+
878
+ /* First run. A board with no terminal is the first thing a new reader sees,
879
+ so it is one lit panel with the three commands and a Copy each, not a grey
880
+ sentence. sessions.js hides it the moment a terminal is live. */
881
+ .first-run { margin-top: 0; padding: 28px 32px; }
882
+ .first-run h3 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
883
+ .first-run p { margin-top: 8px; font-size: var(--t-0); color: var(--text-2); max-width: 56ch; }
884
+ .first-run-cmds { margin-top: 20px; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
885
+ .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; }
886
+ .first-run-cmd code { font-size: var(--t-0); color: var(--text); }
887
+ .first-run-cmd .dot { margin-right: 10px; }
888
+ .first-run-note { margin-top: 16px; font-size: var(--t--1); color: var(--text-3); }
889
+
890
+ /* Settings is the last ledger row, not a stray sentence with a button */
891
+ .region-settings .section-head--quiet { margin-top: 40px; padding-block: 24px 0; border-top: 1px solid var(--edge); align-items: center; }
892
+ .region-settings .section-head--quiet h2 { font-size: var(--t-2); }
893
+ .region-settings .section-head--quiet .region-meta { flex: 1 1 auto; }
894
+ .region-settings .section-head--quiet .disclosure { margin-left: auto; }
895
+
896
+ /* The floor: an empty station is one line, heading and sentence side by side,
897
+ so seven empty stations are seven lines and not seven screens. A station
898
+ with rows keeps its full section. floor.js sets is-empty. */
899
+ .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); }
900
+ .region-floor.is-empty .section-head { padding: 0; display: flex; align-items: baseline; gap: 6px 12px; flex-wrap: wrap; }
901
+ .region-floor.is-empty .section-head h2 { font-size: var(--t-1); white-space: nowrap; }
902
+ .region-floor.is-empty .section-head--quiet .btn { margin-left: 4px; min-height: 30px; }
903
+ .region-floor.is-empty .region-meta:empty, .region-floor.is-empty .card-list:empty, .region-floor.is-empty .ftable { display: none; }
904
+ .region-floor.is-empty .empty-line { margin: 0; color: var(--text-3); }
905
+ .region-floor:not(.is-empty) + .region-floor.is-empty { margin-top: 24px; }
906
+ .masthead-left { font-size: var(--t-0); }
907
+ /* ---- end of the 2026-09-19 block ---------------------------------------- */
908
+
909
+ /* ---- 2026-09-19, second pass: what the rendered seed showed ------------- */
910
+ /* the rung word and the ladder's rung names are text: the chip-id class was
911
+ winning on order, and the identity dot beside each one already says who */
912
+ .term-model[class*="chip-id-"], .ladder-name[class*="chip-id-"] { color: var(--text-2); }
913
+ /* the cards cell's verb sits on the baseline the other cells share, whatever
914
+ order its markup arrives in */
915
+ .ledger-cell .ledger-actions { order: 9; }
916
+ /* a number field wears the same clothes as a text field */
917
+ 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; }
918
+ /* a finished row has no Land, so its land blocker is read by nothing on screen */
919
+ #finished-list .blocker { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
920
+ /* the entry's help line sits after Start on the same row */
921
+ #card-entry .confirm-row .field-help { flex: 0 1 auto; }
922
+ /* ---- 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.0'
71
+ const FILES_VERSION = '0.16.0'
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.0'
53
+ const FILES_VERSION = '0.16.0'
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>