@ucsandman/legcli 0.11.0 → 0.13.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/README.md +95 -65
  3. package/bin/leg.mjs +123 -14
  4. package/docs/DECISIONS.md +18 -0
  5. package/docs/DEMO.md +20 -14
  6. package/docs/DEVIATIONS.md +1 -0
  7. package/docs/ERRORS.md +68 -0
  8. package/docs/ROADMAP-v2.md +50 -5
  9. package/docs/VOCABULARY.md +27 -0
  10. package/docs/board-guide.md +529 -96
  11. package/docs/cli-contracts.md +241 -5
  12. package/docs/concepts.md +167 -19
  13. package/docs/configuration.md +65 -1
  14. package/docs/faq.md +21 -5
  15. package/docs/getting-started.md +15 -11
  16. package/docs/redesign-2026-09-17.md +477 -0
  17. package/docs/screenshots/background-1280.png +0 -0
  18. package/docs/screenshots/board-400px.png +0 -0
  19. package/docs/screenshots/board-details-open.png +0 -0
  20. package/docs/screenshots/board-drawer.png +0 -0
  21. package/docs/screenshots/board-handoff.png +0 -0
  22. package/docs/screenshots/board-running.png +0 -0
  23. package/docs/screenshots/capacity-drawer-1280.png +0 -0
  24. package/docs/screenshots/floor.png +0 -0
  25. package/docs/screenshots/new-card-dialog.png +0 -0
  26. package/docs/screenshots/settings-ladder-1280.png +0 -0
  27. package/docs/screenshots/terminals-1280.png +0 -0
  28. package/fixtures/limits/claude/claude-fable-limit.json +11 -0
  29. package/fixtures/limits/claude/claude-model-limit.json +1 -1
  30. package/fixtures/limits/claude/claude-session-limit.json +1 -1
  31. package/fixtures/limits/claude/claude-weekly-limit.json +1 -1
  32. package/fixtures/live/claude/resume-model-probe.json +20 -0
  33. package/fixtures/live/claude/usage-oauth.json +87 -0
  34. package/fixtures/verified.json +1 -1
  35. package/package.json +3 -2
  36. package/scripts/board-jump-probe.mjs +335 -0
  37. package/scripts/seed-fake-cards.mjs +59 -6
  38. package/scripts/seed-wes-board.mjs +81 -12
  39. package/src/accounts.mjs +6 -1
  40. package/src/attach.mjs +378 -93
  41. package/src/audit.mjs +1 -1
  42. package/src/board/board.css +203 -11
  43. package/src/board/board.js +664 -200
  44. package/src/board/entry.js +343 -0
  45. package/src/board/floor.html +51 -39
  46. package/src/board/floor.js +585 -73
  47. package/src/board/index.html +122 -45
  48. package/src/board/sessions.js +1569 -141
  49. package/src/board/strip.js +163 -0
  50. package/src/buckets.mjs +101 -0
  51. package/src/cards.mjs +9 -1
  52. package/src/chain.mjs +13 -0
  53. package/src/hook.mjs +7 -1
  54. package/src/ledger.mjs +10 -2
  55. package/src/models.mjs +265 -0
  56. package/src/orchestrator.mjs +13 -4
  57. package/src/preferences.mjs +278 -5
  58. package/src/scheduler.mjs +24 -1
  59. package/src/server.mjs +625 -78
  60. package/src/sessions.mjs +17 -1
  61. package/src/taps/claude-usage.mjs +107 -3
  62. package/src/taps/claude.mjs +144 -5
  63. package/src/taps/codex.mjs +23 -3
  64. package/src/usage-poll.mjs +260 -0
  65. package/src/usage.mjs +439 -12
package/src/audit.mjs CHANGED
@@ -17,7 +17,7 @@ import { readEvents as readCardEvents } from './ledger.mjs'
17
17
  // the running commentary. A `status` line is commentary; a hand-off is not.
18
18
  export const AUDITED = [
19
19
  'handoff', 'handoff_requested', 'handed_off', 'landed', 'land', 'bounced', 'killed',
20
- 'approved', 'approval_needed', 'reassigned', 'paused', 'resumed', 'ended', 'done',
20
+ 'approved', 'approval_needed', 'reassigned', 'paused', 'resumed', 'taken_over', 'ended', 'done',
21
21
  'failed', 'trust', 'harness', 'worktree', 'station_done', 'leg_started', 'rerun',
22
22
  ]
23
23
 
@@ -205,11 +205,56 @@ code { font-family: var(--mono); font-size: 0.94em; }
205
205
  .sched-status { color: var(--text-3); }
206
206
 
207
207
  /* ---- the verdict: the largest thing on the board is a sentence ---------- */
208
- .verdict { padding-block: 56px 56px; }
208
+ /* 40/24 rather than 56/56, and a 17px sub rather than 21px: the verdict stays
209
+ the largest thing on the page and costs about 180px instead of 335, which is
210
+ what put the first terminal row a thousand pixels down. The 26ch measure is
211
+ the character budget's other half: 300 sampled sentences per length still
212
+ fit two 56.16px lines at 60 characters there, and sessions.js holds every
213
+ branch at 56. */
214
+ .verdict { padding-block: 40px 24px; }
209
215
  .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; }
210
- .verdict p { margin-top: 16px; font-size: var(--t-2); line-height: 1.5; color: var(--text-2); max-width: 54ch; text-wrap: pretty; }
216
+ .verdict p { margin-top: 8px; font-size: var(--t-1); line-height: 1.5; color: var(--text-2); max-width: 54ch; text-wrap: pretty; }
211
217
  .verdict p:empty { display: none; }
212
218
 
219
+ /* ---- the capacity strip: one band, one token per login ------------------ */
220
+ /* The binding bucket and nothing else. The instrument is 120x6: enough to see
221
+ a login crossing its reserve at a glance, too small to compete with the
222
+ sentence above it. */
223
+ .capacity { margin-bottom: 4px; }
224
+ /* the strip itself never wraps: the disclosure keeps the strip's own line at
225
+ the right (A.2) and the tokens wrap inside their own box, so four logins
226
+ 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; }
230
+ .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
+ .cap-track { position: relative; flex: none; width: 120px; height: 6px; background: var(--track); border-radius: 999px; box-shadow: var(--sink); overflow: hidden; }
238
+ .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; }
240
+ /* a login at its wall is the one place a word IS the failure, so it may carry
241
+ the colour; a percentage never does, the track carries that */
242
+ .cap-figure.is-out { color: var(--danger-text); }
243
+ .cap-figure--none { color: var(--text-3); }
244
+ .cap-open { margin-left: auto; flex: none; }
245
+ .capacity-drawer { margin-top: 16px; }
246
+
247
+ /* the model rail on a panel head inside the drawer: a measured percentage, or
248
+ a wall in words, never one printed as the other */
249
+ .model-rail { display: inline-flex; align-items: baseline; gap: 14px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
250
+ .model-chip { white-space: nowrap; }
251
+ .model-chip.is-out { color: var(--danger-text); }
252
+ /* an open model this login has a live terminal on is a control: pressing it
253
+ puts that rung at the top of the terminal's ladder. The one the terminal is
254
+ already on, and a walled one, stay text: there is nothing to press. */
255
+ .model-chip--pick { font-size: var(--t--1); min-height: 28px; }
256
+ .model-chip.is-current { color: var(--text-2); }
257
+
213
258
  /* ---- panel: a raised object, not a drawn rectangle ---------------------- */
214
259
  .panel { position: relative; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-panel); padding: 32px; box-shadow: var(--lift); }
215
260
  .panel--lit { box-shadow: var(--lift-hi); }
@@ -252,7 +297,7 @@ code { font-family: var(--mono); font-size: 0.94em; }
252
297
  .reading-sub--lead { margin-top: 22px; }
253
298
 
254
299
  /* ---- section heads ------------------------------------------------------ */
255
- .section-head { padding-block: 56px 20px; }
300
+ .section-head { padding-block: 32px 16px; }
256
301
  .section-head h2 { font-size: var(--t-2); font-weight: var(--w-head); letter-spacing: -.012em; }
257
302
  .section-head .region-meta { margin-top: 8px; font-size: var(--t-0); color: var(--text-3); }
258
303
  .section-head--quiet { padding-block: 56px 16px; display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; }
@@ -274,6 +319,28 @@ code { font-family: var(--mono); font-size: 0.94em; }
274
319
  .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; }
275
320
  .term-register { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; font-size: var(--t--1); color: var(--text-3); }
276
321
  .term-where { color: var(--text-2); }
322
+ /* the register's data tokens (A.4 rows 7 to 9). They are one line with the
323
+ status word, in the register's own muted tone: what changed and how long it
324
+ has been quiet are observations, not demands. The model token is the one
325
+ fact here a reader looks for by name, so it takes the identity colour and
326
+ 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); }
329
+ /* the files line and the row's capacity phrase share one line, the phrase at
330
+ the right (A.2). The phrase is per MODEL, which is a fact about this row;
331
+ 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; }
334
+ /* the phrase wraps rather than pushing the row sideways: with a forecast it is
335
+ `about 2h 40m of fable left, from 9 samples over 4h`, two and a half times
336
+ 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); }
338
+ /* the climb is a link beside the figure that explains it, never a fifth button
339
+ in the 2x2 grid, which is the shipped shape */
340
+ .term-climb { font-size: var(--t--1); }
341
+ /* the keyboard ring: a row the keys act on, marked on the edge the eye scans
342
+ down rather than by a fill that would compete with is-urgent */
343
+ .term.is-focused { box-shadow: inset 3px 0 0 var(--focus); }
277
344
  .term-when { color: var(--text-3); min-width: 6ch; text-align: right; }
278
345
  .term-id { color: var(--text-3); min-width: 4ch; text-align: right; font-family: var(--mono); }
279
346
  .term-actions { flex: none; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; width: 272px; }
@@ -502,10 +569,26 @@ code { font-family: var(--mono); font-size: 0.94em; }
502
569
  /* ---- chain rail, order rows, confirm ------------------------------------ */
503
570
  .chain-rail { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: var(--t-0); color: var(--text-3); }
504
571
  .cap-line { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; font-size: var(--t-0); color: var(--text-3); margin-top: 8px; }
505
- .order-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; font-size: var(--t-0); }
506
- .order-row-name { flex: 1 1 auto; }
507
- /* the label on each agent row in the New card dialog */
508
- .fallback-row-title { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
572
+ /* the ladder editor: one rung per row, numbered, with its when, its cost word
573
+ and three buttons. The name takes the free space so every row's controls line
574
+ up in one column whatever the rung is called. */
575
+ .ladder-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; font-size: var(--t-0); flex-wrap: wrap; }
576
+ .ladder-num { flex: none; color: var(--text-3); font-variant-numeric: tabular-nums; }
577
+ .ladder-name { flex: 1 1 12ch; min-width: 10ch; }
578
+ .ladder-rule { flex: 0 0 auto; width: 22ch; display: flex; align-items: center; gap: 8px; }
579
+ .ladder-cost { flex: none; font-size: var(--t--1); color: var(--text-3); }
580
+ /* the cost word is a column of its own so the three buttons line up down the
581
+ list whatever a rung costs */
582
+ .ladder-costcol { width: 12ch; }
583
+ .ladder-when { flex: none; min-height: 36px; font-size: var(--t--1); }
584
+ .ladder-pct { flex: none; width: 7ch; min-height: 36px; font-size: var(--t--1); }
585
+ /* a stored 0 or 100 is shown as it is and says what it does: the editor never
586
+ rewrites a number nobody chose. It wraps the rule cell rather than widening
587
+ it, so the columns beside it do not move. */
588
+ .ladder-flag { flex: 1 0 100%; font-size: var(--t--1); }
589
+ .ladder-add { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 0 2px; font-size: var(--t-0); border-top: 1px solid var(--line); }
590
+ .ladder-policy { display: grid; gap: 8px; margin-top: 18px; border: 0; padding: 0; }
591
+ .ladder-policy legend { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); padding: 0; }
509
592
  .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); }
510
593
  .actions { display: flex; gap: 10px; flex-wrap: wrap; }
511
594
 
@@ -537,6 +620,22 @@ input[type="text"], input[type="password"], textarea, select { font: inherit; fo
537
620
  input[type="checkbox"] { accent-color: var(--accent); width: 16px; height: 16px; }
538
621
  fieldset { border: 1px solid var(--edge); border-radius: var(--r-well); padding: 18px; display: grid; gap: 12px; }
539
622
  legend { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); padding-inline: 6px; }
623
+
624
+ /* the two notification toggles (spec E). A checkbox and its sentence on one
625
+ line, with the help text under it in the field colour the rest of Settings
626
+ uses; a disabled toggle carries its reason in that help text, never in a
627
+ tooltip, because the reason is the whole point of the disabled state. */
628
+ .notify .field-help { margin-top: -4px; }
629
+ .notify-toggle { display: flex; align-items: baseline; gap: 10px; font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }
630
+ .notify-toggle input[disabled] { cursor: not-allowed; }
631
+ .notify-toggle:has(input[disabled]) { color: var(--text-3); }
632
+
633
+ /* the keyboard map: a small panel over the page, opened by ? and closed by ?
634
+ or Escape. Every row names a key and the button that key presses. */
635
+ .keymap { position: fixed; right: 24px; bottom: 24px; z-index: 20; max-width: 380px; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-panel); box-shadow: var(--lift-hi); padding: 22px; }
636
+ .keymap-list { display: grid; grid-template-columns: auto 1fr; gap: 6px 16px; margin-bottom: 18px; font-size: var(--t-0); }
637
+ .keymap-key { font-family: var(--mono); color: var(--text); white-space: nowrap; }
638
+ .keymap-what { color: var(--text-2); }
540
639
  .form-row { display: grid; gap: 8px; margin-top: 18px; }
541
640
  .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); }
542
641
  .advanced-options { margin-top: 22px; }
@@ -565,9 +664,18 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
565
664
  =========================================================================== */
566
665
  @media (max-width: 760px) {
567
666
  .wrap { --t-4: 2rem; --t-3: 1.75rem; --t-2: 1.1875rem; --t--1: 0.9375rem; }
568
- .verdict { padding-block: 36px 32px; }
667
+ .verdict { padding-block: 32px 20px; }
569
668
  .verdict h1 { max-width: none; letter-spacing: -.02em; }
570
- .verdict p { margin-top: 16px; }
669
+ .verdict p { margin-top: 12px; }
670
+ /* the tracks go and the tokens stack, one login per 24px line: at this width
671
+ 120px of instrument costs the percentage its own line, and the percentage
672
+ with its state word carries the whole fact on its own */
673
+ .capacity-strip { display: block; }
674
+ .cap-tokens { display: block; }
675
+ .cap-token { display: flex; min-height: 24px; gap: 8px; }
676
+ .cap-track { display: none; }
677
+ .cap-figure { margin-left: auto; }
678
+ .cap-open { margin-left: 0; margin-top: 12px; }
571
679
  .panel { padding: 22px; border-radius: 14px; }
572
680
  .logins-pair { grid-template-columns: 1fr; }
573
681
  .gauge { gap: 14px; flex-wrap: wrap; }
@@ -579,8 +687,8 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
579
687
  .gauge--noread::after { width: 68px; }
580
688
  .gauge-block + .gauge-block { margin-top: 22px; }
581
689
  code { font-size: 1em; }
582
- .section-head { padding-block: 40px 16px; }
583
- .section-head--quiet { padding-block: 40px 16px; }
690
+ .section-head { padding-block: 24px 16px; }
691
+ .section-head--quiet { padding-block: 24px 16px; }
584
692
  .term { padding: 22px; }
585
693
  .term-row { flex-direction: column; align-items: stretch; gap: 14px; }
586
694
  .term-clock { order: -1; width: auto; justify-content: flex-end; gap: 14px; }
@@ -606,3 +714,87 @@ dialog::backdrop { background: rgba(0,0,0,.6); }
606
714
  * { transition: none !important; }
607
715
  .btn:active { transform: none; }
608
716
  }
717
+
718
+ /* ---- appended 2026-09-18: the timeline's repeat count -------------------- */
719
+ /* A run of identical status lines collapses to one line carrying ×N
720
+ (collapseEvents in sessions.js). The count is a quantity beside a sentence,
721
+ not part of it, so it is muted and set in the tabular face the rest of the
722
+ board counts in. */
723
+ .timeline-count { margin-left: 8px; color: var(--text-3); font-variant-numeric: tabular-nums; font-size: var(--t--1); }
724
+ /* ---- end of the 2026-09-18 block ---------------------------------------- */
725
+
726
+ /* ---- appended 2026-09-18: the new card dialog's two columns -------------- */
727
+ /* The dialog asks two questions and they are read in parallel, not in series:
728
+ WHAT the work is on the left, WHO runs it on the right. One column under
729
+ 900px, because at a phone's width two columns of selects is one column of
730
+ selects with half the room. The dialog itself widens only where there are
731
+ two columns to hold; under the breakpoint the base `dialog` rule still caps
732
+ it at the viewport. */
733
+ .dialog-cols { display: grid; gap: 4px 28px; }
734
+ .dialog-col { min-width: 0; }
735
+ /* the second label inside one .form-row: the path under the repo picker. It is
736
+ a label, not a heading, so it is quieter than the field's own. */
737
+ .sub-label { font-weight: var(--w-text); color: var(--text-3); }
738
+ .form-row .sub-label { font-size: var(--t--1); }
739
+
740
+ /* One rung of "Who runs it": a number, a provider, a model, its permissions,
741
+ its limits, and the three buttons that move or remove it. It wraps rather
742
+ than scrolls, so a narrow dialog stacks the controls of one row instead of
743
+ hiding them off the right edge. Separated from the row above by --line, the
744
+ only rule allowed inside a panel. */
745
+ .chain-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 10px 0; font-size: var(--t-0); }
746
+ .chain-row + .chain-row { border-top: 1px solid var(--line); }
747
+ .chain-num { flex: none; color: var(--text-3); font-variant-numeric: tabular-nums; }
748
+ .chain-row select { flex: 0 1 auto; min-width: 0; max-width: 100%; font-size: var(--t--1); }
749
+ .chain-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t--1); color: var(--text-3); }
750
+ .chain-turns { flex: none; width: 11ch; font-size: var(--t--1); }
751
+ .chain-fake { flex: none; width: 16ch; font-size: var(--t--1); }
752
+ .chain-row .btn { font-size: var(--t--1); }
753
+ /* the three row buttons sit apart from the row's fields: wrapped onto a line
754
+ of their own, `max turns` beside Up/Down/Remove read as one group and the
755
+ number looked like it belonged to the buttons. */
756
+ .chain-row .btn:first-of-type { margin-left: auto; }
757
+
758
+ /* A dialog taller than the window has to scroll itself, or Create card is off
759
+ the bottom of the screen with no way to reach it. Both widths did exactly
760
+ that: 960px of dialog in a 900px window. */
761
+ dialog { max-height: calc(100vh - 40px); overflow-y: auto; }
762
+
763
+ @media (min-width: 900px) {
764
+ #new-card-dialog { max-width: 980px; }
765
+ .dialog-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
766
+ }
767
+ /* ---- end of the 2026-09-18 dialog block --------------------------------- */
768
+
769
+ /* ---- the floor, 2026-09-18: stations instead of tables ------------------ */
770
+ /* The entry row is the first thing under the strip and the only control on the
771
+ page that starts work, so it sits on the raised surface the rows sit on
772
+ rather than in the page's own ground. The board's copy is inside the
773
+ Background panel and inherits its surface; this one has none to inherit. */
774
+ .floor-entry { margin-top: 20px; background: var(--e2); border: 1px solid var(--edge); border-radius: var(--r-well); padding: 20px 24px; }
775
+ .floor-entry .confirm-row { margin-top: 0; }
776
+ .floor-entry .confirm-row input[type="text"] { flex: 1 1 320px; min-width: 0; }
777
+ .floor-entry .field-help { margin-top: 10px; }
778
+ /* the count belongs to the heading it is in, one step quieter than the word */
779
+ .region-count { margin-left: 10px; font-size: var(--t-0); font-weight: var(--w-text); color: var(--text-3); font-variant-numeric: tabular-nums; }
780
+ /* the keyboard ring on a card row, the same 3px edge mark the terminal rows
781
+ carry (`.term.is-focused`) rather than a fill */
782
+ .row.is-focused { box-shadow: inset 3px 0 0 var(--focus); }
783
+
784
+ @media (max-width: 760px) {
785
+ /* R4 is a 272px block in a fixed second column: under about 390px of content
786
+ width that column alone is wider than the row, and both pages scrolled
787
+ sideways. One column, with the clock and the buttons under the sentence. */
788
+ .row { grid-template-columns: minmax(0, 1fr); padding: 22px; }
789
+ .r4 { grid-column: 1; grid-row: auto; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 10px 14px; margin-top: 14px; }
790
+ .r4 .row-actions { width: 100%; }
791
+ .floor-entry { padding: 18px; }
792
+ .floor-entry .confirm-row { align-items: stretch; flex-direction: column; }
793
+ .floor-entry .confirm-row input[type="text"] { flex: 1 1 auto; }
794
+ }
795
+
796
+ /* The heading over a GROUP of controls, not over one: "Who runs it" names the
797
+ whole chain-rows group through aria-labelledby, so it cannot be a <label> --
798
+ a label with no control focuses nothing when it is clicked, which is the one
799
+ thing a label promises. Same face as the row labels beside it. */
800
+ .form-row .form-label { font-size: var(--t-0); font-weight: var(--w-head); color: var(--text-2); }