@lovek4d/kickflip 0.3.1 → 0.5.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 (111) hide show
  1. package/CONTEXT.md +170 -118
  2. package/README.md +23 -14
  3. package/SKILL.md +40 -38
  4. package/bin/doctor.mjs +4 -4
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +34 -26
  7. package/bin/wait-events.mjs +2 -2
  8. package/board/arch-review.mjs +1 -1
  9. package/board/auto-review.mjs +2 -0
  10. package/board/bin/board.mjs +213 -74
  11. package/board/domain.mjs +8 -5
  12. package/board/finish.mjs +4 -0
  13. package/board/headless.mjs +4 -0
  14. package/board/merge3.mjs +1 -0
  15. package/board/on-host.mjs +1 -1
  16. package/board/permissions.mjs +2 -0
  17. package/board/public/app-arch.js +2 -2
  18. package/board/public/app-asks.js +39 -103
  19. package/board/public/app-banner.js +2 -2
  20. package/board/public/app-cards.js +166 -73
  21. package/board/public/app-core.js +2 -23
  22. package/board/public/app-header.js +12 -5
  23. package/board/public/app-lanes.js +201 -47
  24. package/board/public/app-needs.js +2 -1
  25. package/board/public/app-reader.js +174 -0
  26. package/board/public/app-snapshot.js +2 -2
  27. package/board/public/app-threads.js +53 -42
  28. package/board/public/app.html +4 -1
  29. package/board/public/app.js +78 -96
  30. package/board/public/attach.js +39 -3
  31. package/board/public/dialogs.js +59 -14
  32. package/board/public/hub-kit.js +9 -6
  33. package/board/public/hub.css +6 -18
  34. package/board/public/linear-key.js +6 -4
  35. package/board/public/log.html +48 -0
  36. package/board/public/new-task.js +121 -0
  37. package/board/public/prompts.js +9 -8
  38. package/board/public/prototype-kit.js +6 -6
  39. package/board/public/prototype-switcher.js +13 -5
  40. package/board/public/prototype.html +1 -0
  41. package/board/public/router.js +8 -7
  42. package/board/public/rules-editor.js +3 -4
  43. package/board/public/shell.js +24 -6
  44. package/board/public/sidebar.css +22 -1
  45. package/board/public/sidebar.js +64 -31
  46. package/board/public/style.css +245 -64
  47. package/board/public/toasts.js +2 -2
  48. package/board/public/ui-cards.js +205 -75
  49. package/board/public/ui-enums.js +6 -5
  50. package/board/public/ui-header.js +10 -10
  51. package/board/public/ui-hub.js +4 -4
  52. package/board/public/ui-markdown.js +1 -1
  53. package/board/public/ui-order.js +9 -6
  54. package/board/public/ui-page-header.js +10 -7
  55. package/board/public/ui-patch.js +53 -0
  56. package/board/public/ui-phase-bars.js +17 -0
  57. package/board/public/ui-prompts.js +8 -8
  58. package/board/public/ui-questions.js +61 -29
  59. package/board/public/ui-sidebar.js +54 -24
  60. package/board/public/ui-snapshot.js +20 -9
  61. package/board/public/ui-threads.js +40 -21
  62. package/board/public/ui.js +116 -18
  63. package/board/public/view-frame.js +2 -1
  64. package/board/public/view-hub-settings.js +25 -18
  65. package/board/public/view-hub.js +53 -12
  66. package/board/public/view-notes.js +1 -1
  67. package/board/public/view-questions.js +142 -62
  68. package/board/public/view-settings.js +189 -52
  69. package/board/review.mjs +1 -1
  70. package/board/server.mjs +20 -12
  71. package/board/store.mjs +15 -12
  72. package/board/tickets.mjs +3 -3
  73. package/board/worktree.mjs +21 -15
  74. package/docs/hub-session.md +4 -4
  75. package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
  76. package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
  77. package/hub/board-repo.mjs +1 -1
  78. package/hub/boards.mjs +4 -4
  79. package/hub/close-board.mjs +1 -1
  80. package/hub/front-door.mjs +20 -0
  81. package/hub/github.mjs +4 -4
  82. package/hub/home.mjs +1 -1
  83. package/hub/hub.mjs +11 -14
  84. package/hub/incoming.mjs +2 -0
  85. package/hub/linear.mjs +2 -2
  86. package/hub/live.mjs +2 -2
  87. package/hub/new-board.mjs +2 -2
  88. package/hub/owner-checks.mjs +1 -0
  89. package/hub/permission-denied-hook.mjs +2 -0
  90. package/hub/pr-report.mjs +3 -1
  91. package/hub/presence.mjs +1 -1
  92. package/hub/prompts.mjs +1 -1
  93. package/hub/push.mjs +2 -2
  94. package/hub/readiness.mjs +2 -2
  95. package/hub/repos.mjs +3 -3
  96. package/hub/restart-board.mjs +1 -1
  97. package/hub/schedule.mjs +1 -1
  98. package/hub/sessions.mjs +2 -2
  99. package/hub/sw.js +1 -1
  100. package/hub/try.mjs +8 -4
  101. package/hub/usage.mjs +3 -3
  102. package/hub/views.mjs +1 -1
  103. package/package.json +5 -1
  104. package/shared/card-prs.mjs +1 -0
  105. package/shared/colors.css +5 -4
  106. package/shared/fake-gh.mjs +59 -8
  107. package/shared/owner-check.mjs +3 -0
  108. package/shared/repos.mjs +1 -1
  109. package/shared/tailscale.mjs +1 -0
  110. package/shared/usage-gate.mjs +1 -1
  111. package/board/public/view-polling.js +0 -87
@@ -14,7 +14,7 @@ button:disabled { opacity: .5; cursor: default; }
14
14
  button.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
15
15
  a.button { display: inline-block; border: 1px solid var(--line); border-radius: 7px; padding: 6px 10px; background: var(--panel); color: var(--text); text-decoration: none; }
16
16
  /* resolves: the button that clears a card's Action needed (board/domain.mjs resolvedBy), in that badge's notice tone. */
17
- :is(a, button).resolves, button.build, button.unhold, button.closes { border-color: var(--color-notice); background: var(--color-notice); color: #1c1407; }
17
+ :is(a, button).resolves, button.build.ready, button.unhold, button.closes { border-color: var(--color-notice); background: var(--color-notice); color: #1c1407; }
18
18
  button.ghost { border-color: transparent; background: transparent; font-size: 18px; padding: 2px 8px; }
19
19
  textarea, input { font: inherit; color: inherit; background: var(--panel); border: 1px solid var(--line);
20
20
  border-radius: 8px; padding: 8px 10px; width: 100%; resize: vertical; }
@@ -72,7 +72,7 @@ main { max-width: 860px; margin: 0 auto; padding: 12px 16px 80px; }
72
72
  padding: 1px 7px; font-size: 12px; white-space: nowrap; color: var(--text); }
73
73
  button.chip { border: 0; }
74
74
  .chip.tone-quiet { color: var(--color-quiet); } .chip.tone-normal { color: var(--color-info); } .chip.tone-notice { color: var(--color-notice); }
75
- .chip.tone-danger { color: var(--color-danger); } .chip.tone-ready { color: var(--color-success); }
75
+ .chip.tone-danger { color: var(--color-danger); } .chip.tone-ready { color: var(--color-success); } .chip.tone-merged { color: var(--color-merged); }
76
76
  /* Action needed keeps the notice orange; unread takes the accent so the two never read alike. */
77
77
  .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--color-quiet); }
78
78
  .dot.pass { background: var(--color-success); } .dot.pending { background: var(--color-notice); } .dot.fail { background: var(--color-danger); }
@@ -86,7 +86,7 @@ button.chip { border: 0; }
86
86
  .card-ready { color: var(--color-success); }
87
87
  .sub.next.idle { color: var(--muted); }
88
88
  .sub.refused { color: var(--color-danger); }
89
- .toast-stack { position: fixed; left: 50%; bottom: calc(var(--app-bottom, 0px) + 16px + env(safe-area-inset-bottom));
89
+ .toast-stack { position: fixed; left: 50%; bottom: calc(var(--app-bottom, 0px) + 12px + env(safe-area-inset-bottom));
90
90
  width: min(520px, calc(100vw - 32px)); height: 0; transform: translateX(-50%); z-index: 25; pointer-events: none; }
91
91
  .toast-row { position: absolute; top: 0; left: 0; width: 100%; display: flex; justify-content: center;
92
92
  transition: transform 200ms ease; pointer-events: none; }
@@ -101,7 +101,7 @@ button.chip { border: 0; }
101
101
  .grid { display: grid; grid-template-columns: 110px 1fr; gap: 4px 10px; font-size: 13px; margin: 6px 0 10px; }
102
102
  .grid dt { color: var(--muted); }
103
103
  .grid dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
104
- /* A link row's labels open their cards, with room to tap (docs/adr/0156-a-follow-up-is-a-work-card-with-a-parent.md). */
104
+ /* A link row's labels open their cards, with room to tap (docs/adr/2026-11-03-0526-a-follow-up-is-a-work-card-with-a-parent.md). */
105
105
  .grid dd a.card-link { display: inline-block; padding: 6px 4px; margin: -6px 0; text-decoration: underline; text-underline-offset: 2px; }
106
106
  .external-links-label { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
107
107
  .grid .external-links { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 0; }
@@ -128,7 +128,7 @@ button.chip { border: 0; }
128
128
  .thread-more:active { background: var(--accent-soft); }
129
129
  .thread-more:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
130
130
  .thread-more .n { color: var(--muted); font-weight: 400; }
131
- /* ADR 0144: Done's and Asks' Show more, the thread's look as a row of the lane's list */
131
+ /* [answered asks have their own section below Done, and Done and Asks show 10 cards at a time](docs/adr/2026-11-03-0259-answered-asks-have-their-own-section-below-done.md): Done's and Asks' Show more, the thread's look as a row of the lane's list */
132
132
  .list > .thread-more { width: 100%; margin: 0; border-top: 1px solid var(--line); border-radius: 0; }
133
133
 
134
134
  @media (max-width: 560px) {
@@ -140,23 +140,41 @@ button.chip { border: 0; }
140
140
  .calls ul { margin: 4px 0 8px; padding-left: 18px; font-size: 13px; }
141
141
  button.link { background: none; border: 0; color: var(--accent); padding: 8px 12px; font-size: 13px; cursor: pointer; }
142
142
  .wave-icon { display: inline-block; vertical-align: -2px; flex: none; }
143
- .wv-row { padding: 10px 12px; border-top: 1px solid var(--line); min-width: 0; cursor: pointer; }
144
- .wv-row:first-child { border-top: 0; }
145
- .wv-row.on { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
146
- .wv-id { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 8px; min-width: 0; }
147
- .wv-name { display: inline-flex; align-items: center; gap: 5px; min-height: 30px; min-width: 0; padding: 0; border: 0; background: none;
148
- font: inherit; font-weight: 600; color: var(--text); text-align: left; overflow-wrap: anywhere; cursor: pointer; }
149
- .wv-name .wave-icon { color: var(--color-quiet); }
150
- .wv-row.on .wv-name, .wv-row.on .wv-name .wave-icon { color: var(--accent); }
151
- .wv-id .muted { white-space: nowrap; }
152
- .wv-note { margin-top: 4px; font-size: 13px; color: var(--muted); overflow-wrap: anywhere; }
153
- .wv-note b { color: var(--text); font-weight: 600; }
154
- .wv-links { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 0; margin-top: 6px; font-size: 13px; }
155
143
  .wv-link { border: 0; background: none; padding: 0; min-height: 0; color: var(--accent); font: inherit; cursor: pointer; }
156
- .wv-end { margin-left: auto; padding-left: 8px; font-size: 13px; white-space: nowrap; }
144
+ .wv-link:disabled { color: var(--muted); cursor: default; }
157
145
  .wv-busy { display: inline-flex; align-items: center; gap: 6px; color: var(--color-notice); }
158
- .wv-showing { margin-top: 14px; }
159
- .wv-all { border: 0; background: none; padding: 0; color: var(--accent); font: inherit; cursor: pointer; }
146
+ /* docs/adr/2026-11-03-1840-waves-are-bands-in-todo.md: each wave is a lane-style band inside Todo's list. */
147
+ .wb + .wb, .wb-loose { border-top: 1px solid var(--line); }
148
+ .wb-head { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; min-height: 34px; padding: 5px 12px;
149
+ background: var(--band); font-size: 13px; font-weight: 500; cursor: pointer; user-select: none; }
150
+ .wb-head:hover { background: color-mix(in srgb, var(--band), var(--text) 5%); }
151
+ .wb-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
152
+ .wb-head .wave-icon { color: var(--muted); }
153
+ .wb-head .count { color: var(--muted); font-weight: 400; }
154
+ .wb[data-open] > .wb-head { border-bottom: 1px solid var(--line); }
155
+ .wb-label { flex: 1 1 12em; min-width: 0; overflow-wrap: anywhere; }
156
+ .wb-name { -webkit-touch-callout: none; }
157
+ .wb-end { display: flex; align-items: baseline; gap: 10px; margin-left: auto; font-weight: 400; }
158
+ .wb-links { display: inline-flex; align-items: baseline; white-space: nowrap; }
159
+ .wb-next { color: var(--muted); white-space: nowrap; }
160
+ .wb-head .wv-link { font-weight: 500; }
161
+ .wb-pop { position: absolute; left: 34px; right: 12px; top: calc(100% - 2px); z-index: 6; padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px;
162
+ background: var(--panel); box-shadow: 0 6px 18px rgba(0,0,0,.18); color: var(--text); font-weight: 400; line-height: 1.35; overflow-wrap: anywhere; cursor: default; }
163
+ .wb-done { padding: 8px 12px; font-size: 12.5px; color: var(--muted); }
164
+ .wb-done a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
165
+ .wb-loosehead { cursor: default; }
166
+ .wb-loosehead:hover { background: var(--band); }
167
+ /* A wave card's step number, inside its id chip, and its ⋯ menu. */
168
+ .wn { color: var(--muted); font-variant-numeric: tabular-nums; padding-right: 5px; margin-right: 4px; border-right: 1px solid var(--line); }
169
+ .wb .item, .wb .item > summary .title-wrap { position: relative; }
170
+ .wb-dots[aria-expanded="true"] { color: var(--text); background: var(--chip); }
171
+ .wb-dots svg { display: block; }
172
+ .wb-menu { position: absolute; right: 0; top: calc(100% + 4px); z-index: 5; display: grid; min-width: 160px; padding: 4px;
173
+ border: 1px solid var(--line); border-radius: 10px; background: var(--panel); box-shadow: 0 6px 18px rgba(0,0,0,.18); }
174
+ .wb-menu button { border: 0; background: none; text-align: left; padding: 8px 10px; min-height: 0; border-radius: 6px; font: inherit; font-size: 13px; color: var(--text); cursor: pointer; }
175
+ .wb-menu button:hover:not(:disabled) { background: var(--chip); }
176
+ .wb-menu button:disabled { color: var(--muted); cursor: default; }
177
+ .lane[data-lane="todo"] > .list:has(.wb) { overflow: visible; }
160
178
  .nav a.icon { display: inline-flex; align-items: center; padding: 4px; color: var(--muted); }
161
179
  .nav a.icon:hover { color: var(--text); }
162
180
  .refresh { display: inline-flex; align-items: center; gap: 2px; line-height: 1.3; min-width: 0; }
@@ -184,12 +202,20 @@ code { overflow-wrap: anywhere; }
184
202
  /* The ⌘↵ / Ctrl+↵ hint left of Send: only where there is a keyboard to press it on. */
185
203
  .send-hint { display: none; color: var(--muted); font-size: 12px; white-space: nowrap; }
186
204
  @media (hover: hover) and (pointer: fine) { .send-hint { display: inline; } }
205
+ /* A composer's text box (BoardUI.dropZone): while files are dragged over the composer, attach.js marks it .dragging and
206
+ the box gets a dashed accent outline with Drop to attach over it. */
207
+ .dz { position: relative; clear: both; border-radius: 8px; }
208
+ .dz > textarea { display: block; }
209
+ .dz-hint { display: none; position: absolute; inset: 0; align-items: center; justify-content: center; border-radius: 8px; pointer-events: none;
210
+ background: color-mix(in srgb, var(--panel) 80%, transparent); color: var(--accent); font-weight: 600; font-size: 15px; }
211
+ [data-composer].dragging .dz { outline: 2px dashed var(--accent); outline-offset: 3px; }
212
+ [data-composer].dragging .dz-hint { display: flex; }
187
213
  .pending-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
188
214
  .pending-files:empty { display: none; }
189
215
  .pfile { position: relative; display: inline-flex; flex-direction: column; justify-content: center; width: 64px; height: 64px;
190
216
  border: 1px solid var(--line); border-radius: 8px; background: var(--chip); overflow: hidden; font-size: 11px; }
191
217
  .pfile img { width: 100%; height: 100%; object-fit: cover; }
192
- .pfile .pname { padding: 4px 6px; overflow-wrap: anywhere; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
218
+ .pfile .pname { padding: 4px 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
193
219
  .pfile.uploading img, .pfile.uploading .pname { opacity: .55; }
194
220
  .pfile .pbar { position: absolute; left: 4px; right: 4px; bottom: 4px; height: 4px; border-radius: 2px; background: color-mix(in srgb, var(--panel) 70%, transparent); }
195
221
  .pfile .pbar span { display: block; height: 100%; border-radius: 2px; background: var(--color-info); transition: width .2s; }
@@ -202,7 +228,7 @@ code { overflow-wrap: anywhere; }
202
228
  .file-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
203
229
  .file-link { display: inline-flex; align-items: center; max-width: 100%; padding: 3px 8px; border-radius: 6px; background: var(--chip);
204
230
  font-size: 13px; overflow-wrap: anywhere; }
205
- .file-link::before { content: "📎"; margin-right: 4px; }
231
+ .file-link::before { content: "📎"; margin-right: 6px; }
206
232
  .msg .body:empty { display: none; }
207
233
  .note-tools { display: flex; align-items: flex-start; gap: 6px; }
208
234
  .note-tools .pending-files { flex: 1; margin-top: 4px; }
@@ -210,11 +236,13 @@ code { overflow-wrap: anywhere; }
210
236
 
211
237
  /* Compose button and sheet */
212
238
  main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
213
- .fab { position: fixed; z-index: 6; right: calc((16px + max(env(safe-area-inset-right), env(safe-area-inset-bottom))) * 2 / 3); bottom: calc(var(--app-bottom, 0px) + (16px + env(safe-area-inset-bottom)) * 2 / 3);
239
+ /* The + sits two thirds of (16px + the safe area) from the corner, and at least 32px where there is no safe area. */
240
+ :root { --fab-edge: max(32px, calc((16px + env(safe-area-inset-bottom)) * 2 / 3)); }
241
+ .fab { position: fixed; z-index: 6; right: max(32px, calc((16px + max(env(safe-area-inset-right), env(safe-area-inset-bottom))) * 2 / 3)); bottom: calc(var(--app-bottom, 0px) + var(--fab-edge));
214
242
  width: 75px; height: 75px; padding: 0; border-radius: 50%; border: 0; background: var(--accent); color: var(--on-accent);
215
243
  display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 6px 18px rgba(0,0,0,.25); }
216
244
  .fab:active { transform: scale(.96); }
217
- .view-board .toast-stack { bottom: calc(var(--app-bottom, 0px) + 94px + env(safe-area-inset-bottom)); }
245
+ .view-board .toast-stack { bottom: calc(var(--app-bottom, 0px) + 83px + var(--fab-edge)); }
218
246
  /* A phone sheet pads its content above the keyboard (--kb, dialogs.js fitSheet) and stays on the screen's bottom
219
247
  edge while the keyboard covers the bar: iOS reports its height late, so moving the sheet up leaves a strip below. */
220
248
  .sheet { position: fixed; inset: auto 0 0 0; margin: 0 auto; width: 100%; max-width: 560px;
@@ -229,6 +257,10 @@ main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
229
257
  .sheet::backdrop { background: rgba(0,0,0,.35); touch-action: none; }
230
258
  /* A phone sheet sits above the bottom bar, which already pads the safe area (--app-bottom). */
231
259
  .sheet form { padding: 10px 16px max(12px, calc(env(safe-area-inset-bottom) - var(--app-bottom, 0px))); }
260
+ /* Modals never scroll sideways: content wraps at the modal's width, and overflow-x hidden is only the last guard. */
261
+ .sheet, .settings-modal { overflow-x: hidden; overflow-wrap: anywhere; }
262
+ .settings-body { overflow-x: hidden; overflow-wrap: anywhere; }
263
+ .sheet input, .sheet select, .sheet textarea, .settings-modal input, .settings-modal select, .settings-modal textarea { max-width: 100%; min-width: 0; }
232
264
  /* A sheet's last row of buttons stays in view at the foot of its scrolling content. Sticking stops at the scroll
233
265
  area's bottom padding, so the shadow covers what scrolls through that padding. */
234
266
  .sheet-actions { position: sticky; bottom: 0; z-index: 1; padding-top: 8px; background: var(--panel); box-shadow: 0 40px 0 var(--panel); }
@@ -282,19 +314,10 @@ main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
282
314
  .ask-actions .icon:disabled { opacity: .35; }
283
315
  .ask-actions .icon.bumped { color: var(--color-notice); opacity: 1; }
284
316
  .ask-actions .icon.bumped svg { fill: currentColor; }
285
- /* A tap's bump fades the icon's fill in, and an unbump fades it out, on a toast's timing, which app.js fadeBump sets as --bump-fade */
286
- .ask-actions .icon { --bump-fade: 0ms; }
287
- @keyframes bump-in { from { color: var(--muted); } }
288
- @keyframes bump-fill-in { from { fill: transparent; } }
289
- @keyframes bump-out { from { color: var(--color-notice); opacity: 1; } }
290
- @keyframes bump-fill-out { from { fill: var(--color-notice); } to { fill: transparent; } }
291
- .ask-actions .icon.bump-in { animation: bump-in var(--bump-fade) linear; }
292
- .ask-actions .icon.bump-in svg { animation: bump-fill-in var(--bump-fade) linear; }
293
- .ask-actions .icon.bump-out { animation: bump-out var(--bump-fade) linear; }
294
- .ask-actions .icon.bump-out svg { animation: bump-fill-out var(--bump-fade) linear; }
317
+ .ask-actions .icon.owner-done { color: var(--color-notice); }
295
318
  /* A work card's empty bump icon always shows on touch screens; with a hover pointer it shows while the card is hovered or holds focus */
296
319
  @media (hover: hover) {
297
- .item button[data-card-bump]:not(.bumped) { opacity: 0; }
320
+ .item button[data-card-bump]:not(.bumped, [aria-busy="true"]) { opacity: 0; }
298
321
  .item:hover button[data-card-bump]:not(.bumped), .item:focus-within button[data-card-bump]:not(.bumped) { opacity: 1; }
299
322
  }
300
323
  .working-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--color-notice); margin-right: 5px; vertical-align: 1px; }
@@ -311,31 +334,75 @@ button.attach:disabled { opacity: .35; }
311
334
  .line2.meta { justify-content: flex-start; margin-top: 4px; }
312
335
  .line3 { margin-top: 3px; }
313
336
  /* The active bar blinks its fill, not its opacity: an opacity animation gives it its own layer, which Safari can snap a
314
- pixel off its siblings (docs/adr/0160-a-building-cards-phase-bars-end-its-time-row.md). */
337
+ pixel off its siblings (docs/adr/2026-11-03-1519-a-building-cards-phase-bars-end-its-time-row.md). */
315
338
  .card-time .phase-bars { display: inline-flex; gap: 2px; vertical-align: middle; position: relative; top: -1px; }
316
339
  .phase-bars i { width: 9px; height: 4px; border-radius: 2px; background: var(--line); }
317
- .phase-bars i.done, .phase-bars i.now { background: var(--color-notice); }
340
+ /* A strip dims, and its running bar stops blinking, while the card waits on something other than a builder
341
+ (docs/adr/2026-11-03-2024-every-in-progress-card-shows-its-phase-bars.md). app-cards.js sweeps bars in --bar-fill. */
342
+ .phase-bars { --bar-fill: var(--color-notice); }
343
+ .phase-bars.dim { --bar-fill: color-mix(in srgb, var(--muted) 55%, transparent); }
344
+ .phase-bars i.done, .phase-bars i.now { background: var(--bar-fill); }
345
+ /* A bar part way, to --p: a starting board's clone (ui-sidebar.js). */
346
+ .phase-bars i.fill { background: linear-gradient(to right, var(--bar-fill) var(--p), var(--line) var(--p)); }
318
347
  @keyframes pulse-fill { 50% { background-color: color-mix(in srgb, var(--color-notice) 35%, transparent); } }
319
- @media (prefers-reduced-motion: no-preference) { .phase-bars i.now { animation: pulse-fill 1.4s ease-in-out infinite; } }
348
+ @media (prefers-reduced-motion: no-preference) { .phase-bars:not(.dim) i.now { animation: pulse-fill 1.4s ease-in-out infinite; } }
349
+ /* A stale build's status line, dot included, is the needs-you red, and a tap on it nudges Claude (ui-cards.js builderStatus). */
350
+ /* A status line that is a button is drawn as the div it replaces: this one reset, then only a colour per state. The font
351
+ shorthand stays out so .sub's 12px wins. */
352
+ button.sub { display: block; width: 100%; margin-inline: 0; margin-bottom: 0; padding: 0; min-height: 0; border: 0; border-radius: 0; background: none;
353
+ font-family: inherit; font-weight: inherit; letter-spacing: inherit; line-height: inherit; text-align: left; appearance: none; -webkit-appearance: none; }
354
+ button.sub.agent.stalled { color: var(--color-danger); }
355
+ .stalled .working-dot { background: currentColor; }
320
356
  .brand { display: grid; grid-template-columns: auto auto minmax(0, 1fr); column-gap: 10px; row-gap: 2px; }
321
357
  .brand .hub { grid-row: 1 / span 2; align-self: center; }
322
- .sub .working { color: var(--color-notice); }
358
+ /* The session's working line, "thinking", is the owner's blue with one font, size, weight and pulse on the card and in the thread. */
359
+ .thinking { color: var(--accent); font-size: 12px; font-weight: 600; }
360
+ .thinking .working-dot { background: currentColor; }
323
361
  .msg.link { padding: 0 10px; margin: 4px 0; font-size: 13px; }
324
362
  .msg.builder-status { padding: 0 10px; margin: 4px 0; font-size: 12px; color: var(--color-notice); overflow-wrap: anywhere; }
325
363
  .msg.builder-status .working-dot { animation: none; }
364
+ .msg.request { padding: 0 10px; margin: 4px 0; font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
326
365
  .msg.agent { padding: 0 10px; margin: 4px 0; font-size: 12px; font-weight: 600; overflow-wrap: anywhere; }
327
366
  .msg.agent.started { color: var(--color-notice); } .msg.agent.finished { color: var(--color-success); }
328
367
  .msg.agent .muted { font-weight: 400; }
329
368
  /* A planner's line, dot included, is the owner's blue, on the card and in the thread; its finish stays green
330
- (docs/adr/0167-planners-and-implementers-are-two-kinds-of-agent.md). */
369
+ (docs/adr/2026-11-03-1551-planners-and-implementers-are-two-kinds-of-agent.md). */
331
370
  .sub.agent.planner, .msg.builder-status.planner, .msg.agent.started.planner { color: var(--accent); }
332
371
  .planner .working-dot { background: currentColor; }
372
+ /* Every status line on an In Progress card is the phase-bar orange, a planner's too, and every build or plan line on a
373
+ Todo card the owner's blue, dot included; a Todo card's planner dashes are that blue, the running one pulsing
374
+ (docs/adr/2026-11-04-2300-each-lane-is-a-queue-for-the-orchestrator.md). */
375
+ .sub.agent.in-progress { color: var(--color-notice); }
376
+ .sub.agent.in-todo { color: var(--accent); }
377
+ .sub.agent.in-progress .working-dot, .sub.agent.in-todo .working-dot { background: currentColor; }
378
+ .phase-bars.plan { --bar-fill: var(--accent); }
379
+ @keyframes pulse-fill-plan { 50% { background-color: color-mix(in srgb, var(--accent) 35%, transparent); } }
380
+ @media (prefers-reduced-motion: no-preference) { .phase-bars.plan:not(.dim) i.now { animation-name: pulse-fill-plan; } }
333
381
  .msg.calls { padding: 0 10px; margin: 4px 0; font-size: 12px; overflow-wrap: anywhere; }
334
- .msg.calls summary { cursor: pointer; color: var(--muted); }
335
- .msg.calls ol { margin: 4px 0; padding-left: 20px; }
382
+ .msg.calls summary { cursor: pointer; color: var(--muted); list-style: none; }
383
+ .msg.calls summary::-webkit-details-marker { display: none; }
384
+ .msg.calls[open] summary .mark-icon { transform: rotate(90deg); }
385
+ .msg.calls ol { margin: 4px 0; padding-left: 23px; }
336
386
  .agent-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: currentColor; margin-right: 5px; vertical-align: 1px; }
387
+ /* Every thread status line's leading mark, icon or dot, sits centred in one 18px box, so the text after it always starts
388
+ at the same place; the box is 12px tall so line heights stay
389
+ (docs/adr/2026-11-04-2130-a-button-tap-is-a-status-line-or-a-message-footer.md). */
390
+ .mark { display: inline-block; position: relative; width: 18px; height: 12px; margin-right: 5px; vertical-align: -1px; flex: none; }
391
+ .mark > * { position: absolute; inset: 0; margin: auto; }
392
+ .mark .working-dot, .mark .agent-dot { margin: auto; vertical-align: 0; }
393
+ .mark-icon { width: 12px; height: 12px; }
394
+ /* New questions' plain question mark is a size up from its siblings. */
395
+ .mark-icon.big { width: 18px; height: 18px; }
396
+ .msg.new-questions a { color: inherit; }
397
+ /* A message's footer: the tap it came from, then Claude's pick-up to its right. */
398
+ .msg .reply { display: flex; flex-wrap: wrap; align-items: center; column-gap: 12px; row-gap: 2px; }
399
+ .msg-foot .foot-status { display: inline-flex; align-items: center; }
400
+ /* The thread's eye and fire are toggles: pressed reads as chosen, and the hint under the box says what Send posts. */
401
+ form[data-reply] .tools { display: inline-flex; flex: none; }
402
+ button.attach[aria-pressed="true"] { color: var(--accent); background: var(--chip); }
403
+ .flag-hint { margin: 4px 2px 0; min-height: 1.2em; color: var(--muted); font-size: 12px; }
404
+ .flag-hint.on { color: var(--accent); }
337
405
  /* INT-30: card line order */
338
- .line5.meta { justify-content: flex-start; margin-top: 4px; }
339
406
  .line6 { margin-top: 4px; }
340
407
  .card-time time { font-variant-numeric: tabular-nums; }
341
408
  .card-unread { color: var(--accent); font-weight: 600; }
@@ -350,7 +417,7 @@ button.attach:disabled { opacity: .35; }
350
417
  .lane[open] > summary { border-bottom: 0; border-radius: 10px 10px 0 0; }
351
418
  .lane[open] > .list { border-radius: 0 0 10px 10px; }
352
419
  .lane > summary .count { color: var(--muted); font-weight: 400; }
353
- /* Each lane's sort icon and its menu, grey (docs/adr/0096-colour-says-whos-waiting.md) */
420
+ /* Each lane's sort icon and its menu, grey (docs/adr/2026-11-01-1817-colour-says-whos-waiting.md) */
354
421
  /* INT-35: offline header */
355
422
  .view-board.offline #compose { display: none; }
356
423
  .view-board.offline main > * { opacity: .72; }
@@ -370,6 +437,15 @@ button.attach:disabled { opacity: .35; }
370
437
  .brand { flex: 1 1 auto; display: flex; align-items: center; gap: 10px; min-width: 0; }
371
438
  .brand :is(h1, h2) { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
372
439
  .top .nav { flex: none; }
440
+ /* The away moon right of the board title (docs/adr/2026-11-04-0226-the-hub-checks-every-minute-while-youre-here-and-every-5-when-away.md)
441
+ fades in and out with the toasts' timings (toasts.js). */
442
+ .away-moon { flex: none; display: inline-flex; color: var(--muted); opacity: 0; visibility: hidden; transition: opacity 200ms ease, visibility 0s linear 200ms; }
443
+ .away-moon.on { opacity: 1; visibility: visible; transition: opacity 150ms ease, visibility 0s; }
444
+ .away-moon svg { display: block; }
445
+ @media (prefers-reduced-motion: reduce) {
446
+ .away-moon { transition: opacity 100ms ease, visibility 0s linear 100ms; }
447
+ .away-moon.on { transition: opacity 80ms ease, visibility 0s; }
448
+ }
373
449
  .lane-github { display: inline-flex; align-items: center; gap: 2px; }
374
450
  .lane-github .github { display: inline-flex; align-items: center; gap: 4px; }
375
451
  .lane-github .gh-mark { flex: none; }
@@ -391,7 +467,7 @@ button.attach:disabled { opacity: .35; }
391
467
  .top .nav #refresh, .top .nav a.icon { display: inline-flex; align-items: center; justify-content: center; margin: 0; padding: 4px; border: 0; line-height: 1; }
392
468
  .top .nav #status { line-height: 1.1; white-space: nowrap; }
393
469
  .top .nav #refresh:disabled, .top .nav a.icon[aria-disabled="true"] { opacity: .35; pointer-events: none; }
394
- /* ADR 0125: the bulb sits between refresh and the gear, and glows orange while an architecture review runs */
470
+ /* [the bulb runs an architecture review of the board's repo](docs/adr/2026-11-02-1813-the-bulb-runs-an-architecture-review-of-the-boards-repo.md): the bulb sits between refresh and the gear, and glows orange while an architecture review runs */
395
471
  .top .nav .arch-bulb { display: inline-flex; align-items: center; justify-content: center; margin: 0; padding: 4px; border: 0; line-height: 1; color: var(--muted); }
396
472
  .top .nav .arch-bulb:hover { color: var(--text); }
397
473
  .top .nav .arch-bulb[hidden] { display: none; }
@@ -409,11 +485,12 @@ button.attach:disabled { opacity: .35; }
409
485
  .view-board.offline .refresh #refresh, .refresh .link-state.offline { color: var(--color-danger); }
410
486
  .back { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 32px; height: 32px; color: var(--accent); text-decoration: none; }
411
487
  /* The lead slot, logo or back, is one 44px tap box on the page's left edge, so every page's header lines up the same
412
- (docs/adr/0048-a-questions-takes-show-inline.md). The brand keeps that height when the rail hides the logo, so the
488
+ (docs/adr/2026-10-29-1639-a-questions-takes-show-inline.md). The brand keeps that height when the rail hides the logo, so the
413
489
  board's header and the sidebar's stay level. */
414
490
  .top { padding-top: max(4px, env(safe-area-inset-top)); padding-bottom: 4px; }
415
491
  .top .brand { gap: 4px; min-height: 44px; }
416
492
  .top .hub, .top .back { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 44px; height: 44px; margin-left: -10px; border-radius: 10px; }
493
+ .top .hub + .back { margin-left: 0; }
417
494
  .top .back:active { background: var(--accent-soft); }
418
495
  .top .brand :is(h1, h2) { font-size: 17px; font-weight: 600; line-height: 22px; }
419
496
  /* The last visit paints dimmed until the fresh board lands; the controls ui-snapshot.js liveControls blocks rest meanwhile.
@@ -427,7 +504,7 @@ main > * { transition: opacity .25s ease; }
427
504
  .item.tint { animation: card-tint 2.4s ease-out var(--sign-delay, 0s) 1; border-radius: 6px; }
428
505
  @keyframes card-tint { 0%, 25% { background: var(--accent-soft); } 100% { background: transparent; } }
429
506
  .msg .quote { margin: 4px 0 6px; padding: 2px 0 2px 8px; border-left: 2px solid var(--line); color: var(--muted); white-space: pre-wrap; overflow-wrap: anywhere; }
430
- /* ADR 0040: the GitHub mark opens the Hub links sheet, with a red dot while a link is broken; a new board's setup checklist */
507
+ /* [one board registry for every hub feature](docs/adr/2026-10-29-1639-one-board-registry-for-every-hub-feature.md): the GitHub mark opens the Hub links sheet, with a red dot while a link is broken; a new board's setup checklist */
431
508
  .lane-github .gh-links { position: relative; display: inline-flex; align-items: center; padding: 4px; margin: -4px -2px -4px -4px; font-size: inherit; line-height: 1; color: inherit; }
432
509
  .lane-github .links-dot { position: absolute; top: 1px; right: 0; width: 7px; height: 7px; border-radius: 50%; background: var(--color-danger); box-shadow: 0 0 0 1.5px var(--bg); }
433
510
  .hold-sheet .hold-lede { margin: 0 0 2px; font-size: 13.5px; line-height: 1.4; }
@@ -457,6 +534,15 @@ main > * { transition: opacity .25s ease; }
457
534
  .view-settings label.who { gap: 12px; cursor: pointer; }
458
535
  .view-settings label.who .sub { display: block; margin-top: 2px; }
459
536
  .view-settings input.toggle { flex: none; width: 1.4rem; height: 1.4rem; margin: 0; accent-color: var(--accent); }
537
+ .view-settings input.count { flex: none; width: 4.5em; text-align: center; }
538
+ /* A settings switch waiting on the server (ui.js waitOn) keeps its face, disabled, with a spinner beside it
539
+ (docs/adr/2026-11-03-1921-board-settings-grant-a-boards-session-permissions.md) */
540
+ .view-settings .toggle-slot { flex: none; display: inline-flex; align-items: center; gap: 8px; }
541
+ .view-settings .toggle-spin { display: none; color: var(--muted); }
542
+ .view-settings .toggle-slot:has(input.tap-waiting) .toggle-spin { display: inline-block; }
543
+ .view-settings input.toggle.tap-waiting { opacity: 1; cursor: default; }
544
+ .view-settings #permissions code { font-size: 11.5px; overflow-wrap: anywhere; }
545
+ .view-settings #permissions .refused-note { color: var(--color-danger); }
460
546
  .view-settings .close-board-row { width: 100%; text-align: left; color: var(--color-danger); }
461
547
  .view-settings .repo-row { width: 100%; gap: 12px; text-align: left; }
462
548
  .view-settings .repo-row > :first-child { min-width: 0; overflow-wrap: anywhere; }
@@ -478,6 +564,9 @@ main > * { transition: opacity .25s ease; }
478
564
  .view-questions .opt .lbl { font-weight: 600; }
479
565
  .view-questions .opt .rec { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
480
566
  .view-questions .opt .det { color: var(--muted); font-size: 13px; }
567
+ .view-questions .opt .suggested { margin-left: 6px; white-space: nowrap; }
568
+ .view-questions .opt .suggested-why { margin-top: 4px; color: var(--muted); font-size: 13px; line-height: 1.35; overflow-wrap: anywhere; }
569
+ .view-questions .opt .suggested-why a { color: var(--accent); }
481
570
  .view-questions .note { margin-top: 4px; }
482
571
  .view-questions .ans { background: var(--accent-soft); border-radius: 9px; padding: 7px 10px; margin-top: 8px; font-size: 13px; }
483
572
  .view-questions .rep { background: var(--claude); border-radius: 9px; padding: 7px 10px; margin-top: 6px; font-size: 13px; }
@@ -494,24 +583,30 @@ main > * { transition: opacity .25s ease; }
494
583
  .view-questions details.done button.ghost { font-size: 14px; text-align: left; padding: 6px 2px; }
495
584
  .view-questions details.done .trow button { min-width: 0; }
496
585
  .view-questions details.done .trow span { flex: none; }
586
+ .view-questions details.resolved-group > summary { list-style: none; min-height: 44px; display: flex; align-items: center; gap: 8px; font-size: 14px; margin: 12px 0; }
587
+ .view-questions details.resolved-group > summary::-webkit-details-marker { display: none; }
588
+ .view-questions details.resolved-group > summary .caret { transition: transform .15s; }
589
+ .view-questions details.resolved-group[open] > summary .caret { transform: rotate(90deg); }
590
+ .view-questions details.resolved-group > summary .names { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
591
+ .view-questions .q.resolved button.chip { cursor: pointer; font: inherit; font-size: 12px; min-height: 28px; }
592
+ .view-questions .q.resolved button.chip:disabled { cursor: default; }
593
+ .view-questions .q.resolved .resolved-line { margin: 8px 0; }
594
+ .view-questions .q.resolved .opt { cursor: default; }
595
+ .view-questions .q.resolved .opt .det { display: none; }
596
+ .view-questions .q.resolved .opt:has(input:checked) .det { display: block; }
597
+ .view-questions .q.resolved .opt:not(:has(input:checked)) { opacity: .6; }
598
+ .view-questions .q.resolved .reask-line { margin-top: 10px; }
497
599
  .view-questions .bar { position: sticky; bottom: var(--app-bottom, 0px); padding: 10px 0 max(10px, env(safe-area-inset-bottom)); background: var(--bg);
498
600
  display: flex; justify-content: space-between; align-items: center; gap: 8px; border-top: 1px solid var(--line); }
499
601
  .view-questions .bar .send-hint { margin-left: auto; }
602
+ /* The + rides above the sticky Send bar rather than over its Send. */
603
+ .view-questions:has(.bar) .fab { bottom: calc(var(--app-bottom, 0px) + var(--fab-edge) + 72px); }
500
604
  .view-questions .bar #qstatus { min-width: 0; }
501
605
  .view-questions .bar #qstatus.sent { color: var(--color-success); font-weight: 600; }
502
606
  .view-questions .solo { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: 4px 0; }
503
- /* docs/adr/0111-a-prototype-round-ends-with-a-one-take-confirm.md */
607
+ /* docs/adr/2026-11-02-1332-a-prototype-round-ends-with-a-one-take-confirm.md */
504
608
  .view-questions .topichead { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
505
- .view-questions .topichead .try-spin, .view-questions .q .chip .try-spin { width: 9px; height: 9px; margin-right: 5px; vertical-align: -1px; }
506
- /* A requested question: an accent tint and an accent border drawn inside the card, so the card keeps its size. */
507
- .view-questions .q.pr-busy { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 9%, var(--panel)); box-shadow: inset 0 0 0 1px var(--accent); }
508
- .view-questions .q.pr-busy .opt { opacity: .55; cursor: default; }
509
- .view-questions .q.pr-busy .opt:has(input:checked) { opacity: 1; }
510
- /* The source line is as tall as the state badge beside it, which a requested card leaves out. */
511
- .view-questions .q > .row:first-child > .muted { padding: 1px 0; }
512
- .view-questions .q.pr-busy textarea { opacity: .7; }
513
- .view-questions .q.pr-busy .proto-ask { color: var(--accent); }
514
- .view-questions .note-tools .chip { align-self: center; margin-top: 2px; }
609
+ .view-questions .topichead .try-spin { width: 9px; height: 9px; margin-right: 5px; vertical-align: -1px; }
515
610
  .view-questions .q .pr-open { display: inline-block; margin: 0 0 8px; }
516
611
  .view-questions .pr-rule { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; font-size: 12.5px; color: var(--muted); }
517
612
  .view-questions .pr-rule svg { flex: none; }
@@ -587,7 +682,6 @@ main > * { transition: opacity .25s ease; }
587
682
  .view-frame > .frame-page { flex: 1 1 auto; display: block; width: 100%; min-height: 0; border: 0; background: var(--bg); }
588
683
  .view-frame .alone { white-space: nowrap; font-size: 13px; }
589
684
 
590
- .top .phone-logo { display: none; }
591
685
  @media (max-width: 899px) {
592
686
  :root:has(body > .top, body.has-sidebar > :not(.sidebar) > .top):not(.ps-embed) {
593
687
  --app-bottom: calc(49px + max(4px, env(safe-area-inset-bottom)));
@@ -596,7 +690,6 @@ main > * { transition: opacity .25s ease; }
596
690
  position: fixed; top: auto; bottom: 0; left: 0; right: 0; z-index: 7;
597
691
  border-bottom: 0; border-top: 1px solid var(--line); padding-top: 4px; padding-bottom: max(4px, env(safe-area-inset-bottom));
598
692
  }
599
- .top .phone-logo { display: inline-flex; margin-left: 0; }
600
693
  .top .nav #refresh, .top .nav a.icon, .top .nav .arch-bulb { min-width: 44px; min-height: 44px; }
601
694
  body > main, body.has-sidebar > :not(.sidebar) > main { padding-top: max(16px, env(safe-area-inset-top)); padding-bottom: calc(96px + var(--app-bottom, 0px) + env(safe-area-inset-bottom)); }
602
695
  .view-frame:not(.own-header) { padding-bottom: var(--app-bottom, 0px); }
@@ -618,15 +711,25 @@ main > * { transition: opacity .25s ease; }
618
711
  .settings-body { overflow-y: auto; overscroll-behavior: contain; min-height: 0; padding: 0 20px 16px; }
619
712
  .settings-body input { font-size: 16px; }
620
713
  .settings-footer { display: flex; justify-content: flex-end; padding: 12px 20px; border-top: 1px solid var(--line); }
621
- .settings-footer button[aria-busy="true"] { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
622
- .tk-saving, .lk-checking { display: inline-flex; align-items: center; gap: 6px; }
714
+ .lk-checking { display: inline-flex; align-items: center; gap: 6px; }
623
715
  /* linear-key.js's modal over a page sits in .hub-view main only for hub.css's selectors. */
624
716
  .lk-over, .lk-over > main { display: contents; }
625
717
 
626
718
  .tk-section { margin: 10px 0; }
719
+ /* The Tickets source row's live state, and Hub's key field opened under it (dialogs.js ticketsControl keyState). */
720
+ .tk-status { display: flex; align-items: center; gap: 8px; margin: 8px 0 0; font-size: 13px; color: var(--muted); }
721
+ .tk-status b { font-weight: 600; color: var(--text); }
722
+ .tk-status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--line); flex: none; }
723
+ .tk-status.on .dot { background: var(--accent); }
724
+ .tk-status.on b { color: var(--accent); }
725
+ .tk-inline { margin: 6px 0 10px; border: 1px solid var(--accent); border-radius: 10px; padding: 8px 10px; background: var(--accent-soft); }
726
+ .tk-inline .tk-ask { margin: 2px 0 6px; font-size: 13px; }
727
+ .tk-inline .hub-view main { padding: 0; }
627
728
  .tk-seg { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; padding: 4px; margin: 8px 0; border: 1px solid var(--line); border-radius: 9px; background: var(--chip); }
628
729
  .tk-seg button { min-width: 0; min-height: 44px; padding: 6px 4px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--muted); font: inherit; font-size: 13px; cursor: pointer; }
629
730
  .tk-seg button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 600; }
731
+ .ask-seg { grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 10px 0 0; }
732
+ .ask-choice-help { margin: 4px 2px 0; }
630
733
  .tk-seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
631
734
  .tk-grid { display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr); }
632
735
  @media (min-width: 560px) { .tk-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); } }
@@ -646,6 +749,24 @@ main > * { transition: opacity .25s ease; }
646
749
  .try-spin { display: inline-block; width: 10px; height: 10px; border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: try-spin .8s linear infinite; flex: none; }
647
750
  @keyframes try-spin { to { transform: rotate(360deg); } }
648
751
  @media (prefers-reduced-motion: reduce) { .try-spin { animation: none; } }
752
+ /* A pending control waits in one style (ui.js waitOn and BUSY, docs/adr/2026-11-03-1850-a-tap-waits-for-the-server-to-record-it.md,
753
+ docs/adr/2026-11-03-1915-every-pending-control-waits-in-one-style.md): a busy button keeps its own colours and width, disabled, and
754
+ its content gives way to a spinner in its text colour; the New version row's spinner sits in its pill; a bump icon keeps
755
+ its arrow and pulses orange until the card moves; a toggle, select or field keeps its face with a spinner beside it */
756
+ button[aria-busy="true"], button[aria-busy="true"]:disabled, .ask-actions .icon[aria-busy="true"]:disabled { cursor: default; opacity: 1; }
757
+ :is(button[aria-busy="true"]:not([data-card-bump], [data-bump], .sb-update-row), .sb-update-row[aria-busy="true"] .sb-go) { position: relative; -webkit-text-fill-color: transparent; }
758
+ :is(button[aria-busy="true"]:not([data-card-bump], [data-bump], .sb-update-row), .sb-update-row[aria-busy="true"] .sb-go) > * { visibility: hidden; }
759
+ :is(button[aria-busy="true"]:not([data-card-bump], [data-bump], .sb-update-row), .sb-update-row[aria-busy="true"] .sb-go)::after { content: ''; position: absolute; inset: 0; margin: auto;
760
+ width: 12px; height: 12px; box-sizing: border-box; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: try-spin .8s linear infinite; }
761
+ @keyframes wait-orange { 50% { color: var(--color-notice); opacity: 1; } }
762
+ button[aria-busy="true"]:is([data-card-bump], [data-bump]) { color: var(--muted); animation: wait-orange 1s ease-in-out infinite; }
763
+ button[aria-busy="true"]:is([data-card-bump], [data-bump]) svg { fill: currentColor; }
764
+ .try-spin.tap-beside { width: 12px; height: 12px; border-width: 2px; margin: 0 8px 0 auto; color: var(--muted); }
765
+ .tk-field > span > .try-spin.tap-beside { margin: 0 0 0 6px; vertical-align: -2px; }
766
+ @media (prefers-reduced-motion: reduce) {
767
+ :is(button[aria-busy="true"], .sb-update-row[aria-busy="true"] .sb-go)::after { animation: none; }
768
+ button[aria-busy="true"]:is([data-card-bump], [data-bump]) { animation: none; color: var(--color-notice); }
769
+ }
649
770
  .install { display: inline-flex; align-items: center; gap: 4px; }
650
771
  .external-links .install.tone-ready { color: var(--color-success); }
651
772
  .external-links .install.tone-danger { color: var(--color-danger); }
@@ -654,12 +775,72 @@ main > * { transition: opacity .25s ease; }
654
775
  .tb-main > svg { flex: none; color: var(--muted); }
655
776
  .tb-main-text { flex: 1 1 200px; min-width: 0; overflow-wrap: anywhere; }
656
777
  .tb-main .meta { justify-content: flex-end; margin-left: auto; }
778
+ /* #60: Main's refresh icon right of its timestamp; a failed pull's reason shows on hover, or on a tap on touch */
779
+ .tb-main .main-pull { position: relative; display: inline-flex; align-items: center; vertical-align: middle; margin-left: 6px; }
780
+ .tb-main .gh-refresh { display: inline-flex; align-items: center; justify-content: center; font-size: 15px; line-height: 1; padding: 2px 4px; color: var(--muted); min-width: 28px; min-height: 28px; }
781
+ .tb-main .gh-refresh[aria-disabled="true"] { opacity: .45; cursor: default; }
782
+ .tb-main .gh-refresh.failed { color: var(--color-danger); }
783
+ .main-pull-why { position: absolute; top: calc(100% + 6px); left: 0; z-index: 5; width: max-content; max-width: min(240px, calc(100vw - 32px)); padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); color: var(--text); font-size: 13px; line-height: 1.35; box-shadow: 0 6px 20px rgba(0,0,0,.25); visibility: hidden; pointer-events: none; }
784
+ .main-pull-why.open { visibility: visible; }
785
+ @media (hover: hover) { .main-pull:hover .main-pull-why { visibility: visible; } }
657
786
  .main-log-source { margin: 0 0 8px; overflow-wrap: anywhere; }
658
787
  .main-log-lines { margin: 0; padding: 8px 10px; border-radius: 8px; background: var(--chip); color: var(--text);
659
788
  font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11.5px; line-height: 1.5;
660
789
  max-height: calc(12 * 1.5 * 11.5px + 16px); white-space: pre-wrap; overflow-wrap: anywhere; overflow-y: auto; }
661
790
  .main-log-actions { display: flex; justify-content: flex-end; margin-top: 12px; }
662
791
  .main-log-actions button { display: inline-flex; align-items: center; gap: 7px; min-height: 40px; padding: 6px 18px; }
663
- .main-log-actions .main-log-going { border-color: color-mix(in srgb, var(--color-notice) 45%, var(--line));
664
- background: color-mix(in srgb, var(--color-notice) 12%, var(--panel)); color: var(--color-notice); cursor: default; opacity: 1; }
665
792
  #mainLogReason { color: var(--color-notice); overflow-wrap: anywhere; }
793
+ /* The reader sheet (app-reader.js): a builder's log or notes, 85% of the height from the bottom. Its body scrolls
794
+ down only: every line wraps (#252). */
795
+ .reader-sheet { height: 85dvh; max-height: 85dvh; padding: 0; overflow: hidden; }
796
+ .reader-sheet[open] { display: flex; flex-direction: column; }
797
+ .reader-head { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 14px; border-bottom: 1px solid var(--line); }
798
+ .reader-head h2 { flex: none; margin: 0; font-size: 15px; }
799
+ .reader-ref { flex: 1; min-width: 0; font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
800
+ .reader-tools { flex: none; display: inline-flex; align-items: center; gap: 6px; }
801
+ .reader-tools:empty { display: none; }
802
+ .reader-tools button { height: 32px; padding: 0 12px; border-radius: 999px; font-size: 13px; }
803
+ .reader-tools button[aria-pressed="true"] { color: var(--accent); background: var(--chip); }
804
+ .reader-status { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--muted); }
805
+ .reader-status::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
806
+ .reader-status.on { color: var(--accent); }
807
+ .reader-status.on::before { background: var(--accent); animation: reader-pulse 1.4s ease-in-out infinite; }
808
+ @keyframes reader-pulse { 50% { opacity: .35; } }
809
+ .reader-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; padding: 6px 0 max(12px, env(safe-area-inset-bottom)); }
810
+ .reader-body > .muted { padding: 8px 14px; margin: 0; }
811
+ .reader-row { padding: 5px 14px; font-size: 13.5px; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; min-width: 0; }
812
+ .reader-row.text { padding-top: 9px; }
813
+ .reader-row.end { margin: 6px 14px 0; padding: 9px 11px; border: 1px solid var(--line); border-radius: 10px; background: var(--chip); }
814
+ .reader-row.sys, .reader-row.out { color: var(--muted); font-size: 12.5px; }
815
+ .reader-row.out { padding-left: 28px; font: 11.5px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
816
+ .reader-row.tool { font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
817
+ .reader-tag { display: inline-block; margin-right: 6px; padding: 0 6px; border-radius: 6px; background: var(--chip); color: var(--accent); font-size: 11px; }
818
+ .reader-raw, .reader-text { margin: 0; padding: 2px 14px 8px; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
819
+ .reader-md { padding: 4px 16px 12px; overflow-wrap: anywhere; line-height: 1.5; }
820
+ .reader-md h1 { font-size: 17px; margin: 10px 0 6px; }
821
+ .reader-md h2, .reader-md h3 { font-size: 15px; }
822
+ .reader-md pre, .reader-md code { white-space: pre-wrap; overflow-wrap: anywhere; }
823
+ .reader-md table { display: block; max-width: 100%; overflow-x: hidden; }
824
+ .reader-new { position: absolute; left: 50%; bottom: max(14px, env(safe-area-inset-bottom)); transform: translateX(-50%); border: 0; border-radius: 999px; padding: 7px 14px; background: var(--accent); color: var(--on-accent); font-size: 13px; box-shadow: 0 4px 14px rgba(0,0,0,.25); }
825
+ .reader-new[hidden] { display: none; }
826
+
827
+ /* The Auto Review lane (ui-cards.js reviewLane): its cards read like the board's, and Posted earlier's lines like a fold's. */
828
+ .rv-card, .rv-line { border-top: 1px solid var(--line); }
829
+ .rv-card:first-child, .rv-line:first-child { border-top: 0; }
830
+ .rv-card > summary, .rv-line > summary { list-style: none; cursor: pointer; }
831
+ .rv-card > summary::-webkit-details-marker, .rv-line > summary::-webkit-details-marker { display: none; }
832
+ .rv-card > summary:hover, .rv-line > summary:hover { background: color-mix(in srgb, var(--chip) 60%, transparent); }
833
+ .rv-card > summary { display: grid; grid-template-columns: 18px 1fr; gap: 8px; align-items: start; padding: 9px 12px; }
834
+ .rv-card > summary .status { margin-top: 2px; }
835
+ .rv-line > summary { padding: 8px 12px; font-size: 13px; }
836
+ .status.rv-needs { border-color: var(--color-danger); background: var(--color-danger); }
837
+ .rv-word.s-needs, .lane-github .rv-late { color: var(--color-danger); font-weight: 600; }
838
+ .rv-detail { display: grid; gap: 10px; padding: 0 12px 12px 38px; font-size: 14px; min-width: 0; }
839
+ .rv-line .rv-detail { padding-left: 12px; }
840
+ .rv-detail p { margin: 0; }
841
+ .rv-review { border: 1px solid var(--line); border-radius: 10px; background: var(--bg); padding: 10px 12px; min-width: 0; overflow-wrap: anywhere; display: grid; gap: 8px; }
842
+ .rv-review > * { margin: 0; }
843
+ .rv-links { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 13px; }
844
+ .rv-links a { color: var(--accent); text-decoration: none; }
845
+ .rv-acts { display: flex; flex-wrap: wrap; gap: 8px; }
846
+ .view-questions .bar #qstatus .try-spin { color: var(--accent); margin-right: 6px; }
@@ -1,5 +1,5 @@
1
1
  (function () {
2
- // How long a toast fades in and out; the board's bump icon fades its fill on the same timing (app.js fadeBump).
2
+ // How long a toast fades in and out.
3
3
  const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
4
4
  const enterTime = () => reduced() ? 80 : 150;
5
5
  const exitTime = () => reduced() ? 100 : 200;
@@ -177,5 +177,5 @@
177
177
  }
178
178
  return { toast, destroy };
179
179
  }
180
- globalThis.BoardToasts = { create, enterTime, exitTime };
180
+ globalThis.BoardToasts = { create };
181
181
  })();