@lovek4d/kickflip 0.1.0 → 0.4.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 (120) hide show
  1. package/CONTEXT.md +239 -104
  2. package/README.md +54 -4
  3. package/SKILL.md +63 -42
  4. package/bin/doctor.mjs +4 -3
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +103 -40
  7. package/bin/statusline.mjs +4 -3
  8. package/bin/wait-events.mjs +2 -2
  9. package/board/arch-review.mjs +5 -0
  10. package/board/auto-review.mjs +2 -0
  11. package/board/bin/agent.mjs +1 -1
  12. package/board/bin/board.mjs +371 -82
  13. package/board/bin/builder-stop-hook.mjs +4 -3
  14. package/board/domain.mjs +8 -3
  15. package/board/finish.mjs +3 -0
  16. package/board/headless.mjs +3 -0
  17. package/board/on-host.mjs +1 -0
  18. package/board/permissions.mjs +2 -0
  19. package/board/public/app-arch.js +75 -0
  20. package/board/public/app-asks.js +66 -28
  21. package/board/public/app-banner.js +2 -2
  22. package/board/public/app-cards.js +354 -77
  23. package/board/public/app-core.js +40 -8
  24. package/board/public/app-header.js +33 -8
  25. package/board/public/app-lanes.js +238 -34
  26. package/board/public/app-needs.js +2 -0
  27. package/board/public/app-snapshot.js +45 -11
  28. package/board/public/app-threads.js +20 -21
  29. package/board/public/app.html +6 -2
  30. package/board/public/app.js +114 -33
  31. package/board/public/attach.js +39 -3
  32. package/board/public/dialogs.js +161 -118
  33. package/board/public/hub-kit.js +27 -7
  34. package/board/public/hub.css +26 -33
  35. package/board/public/linear-key.js +143 -0
  36. package/board/public/log.html +48 -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 +14 -10
  42. package/board/public/rules-editor.js +4 -5
  43. package/board/public/shell.js +21 -7
  44. package/board/public/sidebar.css +51 -11
  45. package/board/public/sidebar.js +124 -19
  46. package/board/public/style.css +244 -56
  47. package/board/public/toasts.js +5 -3
  48. package/board/public/ui-cards.js +236 -66
  49. package/board/public/ui-enums.js +5 -4
  50. package/board/public/ui-header.js +23 -14
  51. package/board/public/ui-hub.js +34 -16
  52. package/board/public/ui-links.js +18 -10
  53. package/board/public/ui-markdown.js +1 -1
  54. package/board/public/ui-order.js +21 -0
  55. package/board/public/ui-page-header.js +13 -8
  56. package/board/public/ui-patch.js +53 -0
  57. package/board/public/ui-phase-bars.js +17 -0
  58. package/board/public/ui-prompts.js +8 -8
  59. package/board/public/ui-questions.js +35 -11
  60. package/board/public/ui-sidebar.js +83 -21
  61. package/board/public/ui-snapshot.js +30 -13
  62. package/board/public/ui-threads.js +38 -23
  63. package/board/public/ui.js +96 -16
  64. package/board/public/view-frame.js +1 -1
  65. package/board/public/view-hub-settings.js +67 -44
  66. package/board/public/view-hub.js +105 -13
  67. package/board/public/view-notes.js +8 -5
  68. package/board/public/view-questions.js +123 -52
  69. package/board/public/view-settings.js +235 -62
  70. package/board/review.mjs +1 -0
  71. package/board/server.mjs +18 -7
  72. package/board/store.mjs +15 -6
  73. package/board/tickets.mjs +3 -3
  74. package/board/worktree.mjs +20 -13
  75. package/docs/hub-session.md +4 -4
  76. package/hooks-plugin/hooks/codex-share-auto.mjs +7 -5
  77. package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
  78. package/hub/awake.mjs +1 -0
  79. package/hub/board-repo.mjs +1 -1
  80. package/hub/boards.mjs +4 -4
  81. package/hub/close-board.mjs +1 -1
  82. package/hub/duration.mjs +1 -0
  83. package/hub/github.mjs +3 -4
  84. package/hub/home.mjs +1 -1
  85. package/hub/hub.mjs +12 -10
  86. package/hub/incoming.mjs +2 -0
  87. package/hub/linear.mjs +2 -2
  88. package/hub/live.mjs +2 -2
  89. package/hub/new-board.mjs +2 -2
  90. package/hub/permission-denied-hook.mjs +2 -0
  91. package/hub/pr-report.mjs +3 -0
  92. package/hub/presence.mjs +1 -1
  93. package/hub/prompts.mjs +1 -1
  94. package/hub/push.mjs +2 -2
  95. package/hub/readiness.mjs +2 -0
  96. package/hub/repos.mjs +3 -2
  97. package/hub/restart-board.mjs +1 -0
  98. package/hub/routes.mjs +1 -1
  99. package/hub/schedule.mjs +1 -1
  100. package/hub/sessions.mjs +2 -2
  101. package/hub/shell.mjs +2 -1
  102. package/hub/slack.mjs +2 -0
  103. package/hub/stream.mjs +12 -12
  104. package/hub/sw.js +2 -2
  105. package/hub/trust.mjs +2 -0
  106. package/hub/try.mjs +8 -3
  107. package/hub/upgrade.mjs +1 -0
  108. package/hub/usage.mjs +3 -3
  109. package/hub/views.mjs +1 -1
  110. package/package.json +5 -1
  111. package/shared/card-prs.mjs +1 -0
  112. package/shared/colors.css +9 -6
  113. package/shared/fake-gh.mjs +105 -0
  114. package/shared/repos.mjs +1 -1
  115. package/shared/service.mjs +16 -16
  116. package/shared/skills.mjs +2 -0
  117. package/shared/usage-gate.mjs +1 -1
  118. package/starter/public/claude.js +3 -2
  119. package/bin/web-ts.mjs +0 -2
  120. package/board/public/view-polling.js +0 -87
@@ -11,10 +11,10 @@ a:hover { text-decoration: underline; }
11
11
  button { font: inherit; color: inherit; cursor: pointer; border: 1px solid var(--line); background: var(--panel);
12
12
  border-radius: 7px; padding: 6px 10px; }
13
13
  button:disabled { opacity: .5; cursor: default; }
14
- button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
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 { 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; }
@@ -30,13 +30,13 @@ textarea:focus, input:focus { outline: 2px solid var(--accent-soft); border-colo
30
30
  .brand :is(h1, h2) { font-size: 16px; margin: 0; }
31
31
  .nav { display: flex; align-items: center; gap: 6px; }
32
32
  .pill { border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px; font-size: 12px; color: var(--text); }
33
- .pill.hot { background: var(--accent); border-color: var(--accent); color: #fff; }
33
+ .pill.hot { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
34
34
 
35
35
  main { max-width: 860px; margin: 0 auto; padding: 12px 16px 80px; }
36
36
 
37
- .waiting { background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
37
+ .waiting { background: color-mix(in srgb, var(--color-notice) 12%, var(--panel)); border: 1px solid color-mix(in srgb, var(--color-notice) 40%, var(--line));
38
38
  border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; }
39
- .waiting h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; margin: 0 0 6px; color: var(--accent); }
39
+ .waiting h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; margin: 0 0 6px; color: var(--color-notice); }
40
40
  .waiting ul { margin: 0; padding-left: 18px; }
41
41
  .waiting li { margin: 3px 0; }
42
42
  .waiting li.prompt { list-style: none; margin: 6px 0 10px -18px; padding: 8px 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; }
@@ -72,19 +72,21 @@ 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
- .chip.unread { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
78
77
  .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--color-quiet); }
79
78
  .dot.pass { background: var(--color-success); } .dot.pending { background: var(--color-notice); } .dot.fail { background: var(--color-danger); }
80
79
 
81
80
  .status { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--color-quiet); display: inline-block; position: relative; }
82
81
  .status.progress { border-color: var(--color-notice); background: conic-gradient(var(--color-notice) 0 50%, transparent 50% 100%); }
83
- .status.review { border-color: var(--color-success); background: conic-gradient(var(--color-success) 0 75%, transparent 75% 100%); }
84
- .status.todo { border-style: dashed; }
82
+ .status.review { border-color: var(--color-info); background: conic-gradient(var(--color-info) 0 75%, transparent 75% 100%); }
85
83
  .sub.agent { color: var(--color-notice); }
86
- .sub.next { color: var(--color-info); }
87
- .toast-stack { position: fixed; left: 50%; bottom: calc(var(--app-bottom, 0px) + 16px + env(safe-area-inset-bottom));
84
+ .sub.next { color: var(--color-notice); }
85
+ .card-hold, .card-blocked, .card-action { color: var(--color-notice); }
86
+ .card-ready { color: var(--color-success); }
87
+ .sub.next.idle { color: var(--muted); }
88
+ .sub.refused { color: var(--color-danger); }
89
+ .toast-stack { position: fixed; left: 50%; bottom: calc(var(--app-bottom, 0px) + 12px + env(safe-area-inset-bottom));
88
90
  width: min(520px, calc(100vw - 32px)); height: 0; transform: translateX(-50%); z-index: 25; pointer-events: none; }
89
91
  .toast-row { position: absolute; top: 0; left: 0; width: 100%; display: flex; justify-content: center;
90
92
  transition: transform 200ms ease; pointer-events: none; }
@@ -93,12 +95,14 @@ button.chip { border: 0; }
93
95
  box-shadow: 0 4px 16px rgba(0,0,0,.2); touch-action: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
94
96
  .toast:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
95
97
  @media (prefers-reduced-motion: reduce) { .toast-row { transition: none; } }
96
- .status.done { border-color: var(--color-info); background: var(--color-info); }
98
+ .status.done, .status.asks { border-color: var(--color-quiet); background: var(--color-quiet); }
97
99
 
98
100
  .detail { padding: 4px 12px 14px 38px; }
99
101
  .grid { display: grid; grid-template-columns: 110px 1fr; gap: 4px 10px; font-size: 13px; margin: 6px 0 10px; }
100
102
  .grid dt { color: var(--muted); }
101
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/2026-11-03-0526-a-follow-up-is-a-work-card-with-a-parent.md). */
105
+ .grid dd a.card-link { display: inline-block; padding: 6px 4px; margin: -6px 0; text-decoration: underline; text-underline-offset: 2px; }
102
106
  .external-links-label { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
103
107
  .grid .external-links { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 0; }
104
108
  .external-link { white-space: nowrap; }
@@ -113,7 +117,7 @@ button.chip { border: 0; }
113
117
  .thread { border-top: 1px dashed var(--line); padding-top: 8px; }
114
118
  .msg { margin: 8px 0; padding: 7px 10px; border-radius: 10px; max-width: 100%; }
115
119
  .msg.claude { background: var(--claude); }
116
- .msg.love { background: var(--love); }
120
+ .msg.love { background: var(--accent-soft); }
117
121
  .msg .who { font-size: 11px; color: var(--muted); margin-bottom: 2px; }
118
122
  .msg .body { white-space: pre-wrap; overflow-wrap: anywhere; }
119
123
  .msg .reply { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--line); }
@@ -124,6 +128,8 @@ button.chip { border: 0; }
124
128
  .thread-more:active { background: var(--accent-soft); }
125
129
  .thread-more:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
126
130
  .thread-more .n { color: var(--muted); font-weight: 400; }
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
+ .list > .thread-more { width: 100%; margin: 0; border-top: 1px solid var(--line); border-radius: 0; }
127
133
 
128
134
  @media (max-width: 560px) {
129
135
  .item > summary { grid-template-columns: 16px 1fr; }
@@ -133,19 +139,50 @@ button.chip { border: 0; }
133
139
  }
134
140
  .calls ul { margin: 4px 0 8px; padding-left: 18px; font-size: 13px; }
135
141
  button.link { background: none; border: 0; color: var(--accent); padding: 8px 12px; font-size: 13px; cursor: pointer; }
136
- .waves { display: grid; gap: 6px; margin-top: 4px; }
137
- .wave { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; text-align: left; background: var(--panel);
138
- border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; color: var(--text); }
139
- .wave.on { border-color: var(--accent); background: var(--accent-soft); }
140
- .wave-name { font-weight: 600; font-size: 13px; }
141
- .wave .bar { grid-column: 1 / -1; height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; }
142
- .wave .bar span { display: block; height: 100%; background: var(--color-success); }
142
+ .wave-icon { display: inline-block; vertical-align: -2px; flex: none; }
143
+ .wv-link { border: 0; background: none; padding: 0; min-height: 0; color: var(--accent); font: inherit; cursor: pointer; }
144
+ .wv-link:disabled { color: var(--muted); cursor: default; }
145
+ .wv-busy { display: inline-flex; align-items: center; gap: 6px; color: var(--color-notice); }
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; }
143
178
  .nav a.icon { display: inline-flex; align-items: center; padding: 4px; color: var(--muted); }
144
179
  .nav a.icon:hover { color: var(--text); }
145
180
  .refresh { display: inline-flex; align-items: center; gap: 2px; line-height: 1.3; min-width: 0; }
146
181
  .refresh #refresh { padding: 4px; margin-left: -4px; flex: none; }
147
182
  .refresh #status { font-size: 12px; min-width: 0; }
148
183
  .refresh .late { color: var(--color-notice); font-weight: 600; }
184
+ .refresh .down { color: var(--color-danger); font-weight: 600; }
185
+ .refresh .asleep { color: var(--muted); font-weight: 600; }
149
186
  .lane-github { margin-left: auto; font-size: 12px; font-weight: 400; color: var(--muted); text-align: right; }
150
187
  .lane-github .late { color: var(--color-notice); }
151
188
  .msg.other { background: transparent; border: 1px solid var(--line); }
@@ -165,12 +202,20 @@ code { overflow-wrap: anywhere; }
165
202
  /* The ⌘↵ / Ctrl+↵ hint left of Send: only where there is a keyboard to press it on. */
166
203
  .send-hint { display: none; color: var(--muted); font-size: 12px; white-space: nowrap; }
167
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; }
168
213
  .pending-files { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
169
214
  .pending-files:empty { display: none; }
170
215
  .pfile { position: relative; display: inline-flex; flex-direction: column; justify-content: center; width: 64px; height: 64px;
171
216
  border: 1px solid var(--line); border-radius: 8px; background: var(--chip); overflow: hidden; font-size: 11px; }
172
217
  .pfile img { width: 100%; height: 100%; object-fit: cover; }
173
- .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; }
174
219
  .pfile.uploading img, .pfile.uploading .pname { opacity: .55; }
175
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); }
176
221
  .pfile .pbar span { display: block; height: 100%; border-radius: 2px; background: var(--color-info); transition: width .2s; }
@@ -183,19 +228,21 @@ code { overflow-wrap: anywhere; }
183
228
  .file-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
184
229
  .file-link { display: inline-flex; align-items: center; max-width: 100%; padding: 3px 8px; border-radius: 6px; background: var(--chip);
185
230
  font-size: 13px; overflow-wrap: anywhere; }
186
- .file-link::before { content: "📎"; margin-right: 4px; }
231
+ .file-link::before { content: "📎"; margin-right: 6px; }
187
232
  .msg .body:empty { display: none; }
188
233
  .note-tools { display: flex; align-items: flex-start; gap: 6px; }
189
234
  .note-tools .pending-files { flex: 1; margin-top: 4px; }
190
- .note-tools .attach { margin-left: auto; margin-top: 2px; }
235
+ .note-tools .attach { margin-top: 2px; }
191
236
 
192
237
  /* Compose button and sheet */
193
- main { padding-bottom: calc(96px + env(safe-area-inset-bottom)); }
194
- .fab { position: fixed; z-index: 6; right: calc(16px + env(safe-area-inset-right)); bottom: calc(var(--app-bottom, 0px) + 16px + env(safe-area-inset-bottom));
195
- width: 56px; height: 56px; padding: 0; border-radius: 50%; border: 0; background: var(--accent); color: #fff;
238
+ main { padding-bottom: calc(110px + env(safe-area-inset-bottom)); }
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));
242
+ width: 75px; height: 75px; padding: 0; border-radius: 50%; border: 0; background: var(--accent); color: var(--on-accent);
196
243
  display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 6px 18px rgba(0,0,0,.25); }
197
244
  .fab:active { transform: scale(.96); }
198
- .view-board .toast-stack { bottom: calc(var(--app-bottom, 0px) + 80px + env(safe-area-inset-bottom)); }
245
+ .view-board .toast-stack { bottom: calc(var(--app-bottom, 0px) + 83px + var(--fab-edge)); }
199
246
  /* A phone sheet pads its content above the keyboard (--kb, dialogs.js fitSheet) and stays on the screen's bottom
200
247
  edge while the keyboard covers the bar: iOS reports its height late, so moving the sheet up leaves a strip below. */
201
248
  .sheet { position: fixed; inset: auto 0 0 0; margin: 0 auto; width: 100%; max-width: 560px;
@@ -235,22 +282,13 @@ main { padding-bottom: calc(96px + env(safe-area-inset-bottom)); }
235
282
  @keyframes sheet-in { from { transform: translateY(100%); } }
236
283
  @keyframes sheet-out { to { transform: translateY(100%); } }
237
284
  @keyframes fade-in { from { opacity: 0; } }
238
- .confirm-sheet { position: fixed; inset: 0; margin: 0; width: 100%; max-width: none; height: 100%; max-height: none; border: 0;
239
- display: flex; flex-direction: column; justify-content: flex-end; align-items: center;
240
- gap: 8px; padding: 8px 8px calc(10px + env(safe-area-inset-bottom)); background: rgba(0,0,0,.4); animation: fade-in .18s ease-out; }
241
- .confirm-sheet .box { width: min(520px, 100%); background: var(--panel); border-radius: 14px; overflow: hidden; text-align: center;
242
- box-shadow: 0 10px 40px rgba(0,0,0,.3); animation: sheet-in .22s ease-out; }
243
- .confirm-sheet p { margin: 0; padding: 14px 16px; font-size: 13.5px; line-height: 1.4; color: var(--muted); border-bottom: 1px solid var(--line); }
244
- .confirm-sheet button { display: block; width: 100%; border: 0; border-radius: 0; background: transparent; padding: 14px 6px; font-size: 17px; color: var(--accent); }
245
- .confirm-sheet .cancel { font-weight: 600; }
246
- .confirm-sheet::backdrop { background: transparent; }
247
285
  @media (min-width: 700px) {
248
286
  .sheet { bottom: calc(var(--kb, 0px) + 24px); max-height: calc(var(--vh, 100dvh) - env(safe-area-inset-top) - 12px); padding-bottom: 0;
249
287
  border-bottom: 1px solid var(--line); border-radius: 16px; }
250
288
  .sheet form { padding-bottom: 14px; }
251
289
  }
252
290
  @media (prefers-reduced-motion: reduce) {
253
- .sheet[open], .sheet[open]::backdrop, .sheet.closing, .sheet.closing::backdrop, .confirm-sheet, .confirm-sheet .box { animation: none; }
291
+ .sheet[open], .sheet[open]::backdrop, .sheet.closing, .sheet.closing::backdrop { animation: none; }
254
292
  .fab:active { transform: none; }
255
293
  }
256
294
  /* The hub lives at the tailnet root, outside this site, so its link is root-absolute. */
@@ -270,6 +308,14 @@ main { padding-bottom: calc(96px + env(safe-area-inset-bottom)); }
270
308
  .ask-actions { float: right; display: flex; gap: 2px; margin: -6px -6px 0 8px; }
271
309
  .ask-actions .icon { padding: 6px; color: var(--muted); }
272
310
  .ask-actions .icon:disabled { opacity: .35; }
311
+ .ask-actions .icon.bumped { color: var(--color-notice); opacity: 1; }
312
+ .ask-actions .icon.bumped svg { fill: currentColor; }
313
+ .ask-actions .icon.owner-done { color: var(--color-notice); }
314
+ /* 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 */
315
+ @media (hover: hover) {
316
+ .item button[data-card-bump]:not(.bumped, [aria-busy="true"]) { opacity: 0; }
317
+ .item:hover button[data-card-bump]:not(.bumped), .item:focus-within button[data-card-bump]:not(.bumped) { opacity: 1; }
318
+ }
273
319
  .working-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--color-notice); margin-right: 5px; vertical-align: 1px; }
274
320
  @media (prefers-reduced-motion: no-preference) { .working-dot { animation: pulse 1.4s ease-in-out infinite; } }
275
321
  form[data-reply] .row .quick { margin-left: auto; margin-right: 6px; }
@@ -283,18 +329,47 @@ button.attach:disabled { opacity: .35; }
283
329
  .line1 .chip { vertical-align: 1px; margin-right: 2px; }
284
330
  .line2.meta { justify-content: flex-start; margin-top: 4px; }
285
331
  .line3 { margin-top: 3px; }
332
+ /* The active bar blinks its fill, not its opacity: an opacity animation gives it its own layer, which Safari can snap a
333
+ pixel off its siblings (docs/adr/2026-11-03-1519-a-building-cards-phase-bars-end-its-time-row.md). */
334
+ .card-time .phase-bars { display: inline-flex; gap: 2px; vertical-align: middle; position: relative; top: -1px; }
335
+ .phase-bars i { width: 9px; height: 4px; border-radius: 2px; background: var(--line); }
336
+ /* A strip dims, and its running bar stops blinking, while the card waits on something other than a builder
337
+ (docs/adr/2026-11-03-2024-every-in-progress-card-shows-its-phase-bars.md). app-cards.js sweeps bars in --bar-fill. */
338
+ .phase-bars { --bar-fill: var(--color-notice); }
339
+ .phase-bars.dim { --bar-fill: color-mix(in srgb, var(--muted) 55%, transparent); }
340
+ .phase-bars i.done, .phase-bars i.now { background: var(--bar-fill); }
341
+ /* A bar part way, to --p: a starting board's clone (ui-sidebar.js). */
342
+ .phase-bars i.fill { background: linear-gradient(to right, var(--bar-fill) var(--p), var(--line) var(--p)); }
343
+ @keyframes pulse-fill { 50% { background-color: color-mix(in srgb, var(--color-notice) 35%, transparent); } }
344
+ @media (prefers-reduced-motion: no-preference) { .phase-bars:not(.dim) i.now { animation: pulse-fill 1.4s ease-in-out infinite; } }
345
+ /* A stale build's status line, dot included, is the needs-you red, and a tap on it nudges Claude (ui-cards.js builderStatus). */
346
+ button.sub.agent.stalled { display: block; width: 100%; padding: 0; border: 0; border-radius: 0; background: none; text-align: left; color: var(--color-danger); }
347
+ .stalled .working-dot { background: currentColor; }
286
348
  .brand { display: grid; grid-template-columns: auto auto minmax(0, 1fr); column-gap: 10px; row-gap: 2px; }
287
349
  .brand .hub { grid-row: 1 / span 2; align-self: center; }
288
- .sub .working { color: var(--color-notice); }
350
+ /* 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. */
351
+ .thinking { color: var(--accent); font-size: 12px; font-weight: 600; }
352
+ .thinking .working-dot { background: currentColor; }
289
353
  .msg.link { padding: 0 10px; margin: 4px 0; font-size: 13px; }
290
354
  .msg.builder-status { padding: 0 10px; margin: 4px 0; font-size: 12px; color: var(--color-notice); overflow-wrap: anywhere; }
291
355
  .msg.builder-status .working-dot { animation: none; }
356
+ .msg.request { padding: 0 10px; margin: 4px 0; font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
292
357
  .msg.agent { padding: 0 10px; margin: 4px 0; font-size: 12px; font-weight: 600; overflow-wrap: anywhere; }
293
358
  .msg.agent.started { color: var(--color-notice); } .msg.agent.finished { color: var(--color-success); }
294
359
  .msg.agent .muted { font-weight: 400; }
360
+ /* A planner's line, dot included, is the owner's blue, on the card and in the thread; its finish stays green
361
+ (docs/adr/2026-11-03-1551-planners-and-implementers-are-two-kinds-of-agent.md). */
362
+ .sub.agent.planner, .msg.builder-status.planner, .msg.agent.started.planner { color: var(--accent); }
363
+ .planner .working-dot { background: currentColor; }
364
+ .msg.calls { padding: 0 10px; margin: 4px 0; font-size: 12px; overflow-wrap: anywhere; }
365
+ .msg.calls summary { cursor: pointer; color: var(--muted); }
366
+ .msg.calls ol { margin: 4px 0; padding-left: 20px; }
295
367
  .agent-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: currentColor; margin-right: 5px; vertical-align: 1px; }
296
368
  /* INT-30: card line order */
297
- .line5.meta { justify-content: flex-start; margin-top: 4px; }
369
+ .line6 { margin-top: 4px; }
370
+ .card-time time { font-variant-numeric: tabular-nums; }
371
+ .card-unread { color: var(--accent); font-weight: 600; }
372
+ .card-unread .sep { color: var(--muted); font-weight: 400; }
298
373
  /* Lane headers: Linear-style group bands */
299
374
  .lane > summary { display: flex; align-items: center; gap: 8px; margin: 0; min-height: 34px; padding: 5px 12px;
300
375
  background: var(--band); border: 1px solid var(--line); border-radius: 10px; font-size: 13px; font-weight: 500; color: var(--text); }
@@ -305,6 +380,7 @@ button.attach:disabled { opacity: .35; }
305
380
  .lane[open] > summary { border-bottom: 0; border-radius: 10px 10px 0 0; }
306
381
  .lane[open] > .list { border-radius: 0 0 10px 10px; }
307
382
  .lane > summary .count { color: var(--muted); font-weight: 400; }
383
+ /* Each lane's sort icon and its menu, grey (docs/adr/2026-11-01-1817-colour-says-whos-waiting.md) */
308
384
  /* INT-35: offline header */
309
385
  .view-board.offline #compose { display: none; }
310
386
  .view-board.offline main > * { opacity: .72; }
@@ -324,9 +400,21 @@ button.attach:disabled { opacity: .35; }
324
400
  .brand { flex: 1 1 auto; display: flex; align-items: center; gap: 10px; min-width: 0; }
325
401
  .brand :is(h1, h2) { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
326
402
  .top .nav { flex: none; }
403
+ /* 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)
404
+ fades in and out with the toasts' timings (toasts.js). */
405
+ .away-moon { flex: none; display: inline-flex; color: var(--muted); opacity: 0; visibility: hidden; transition: opacity 200ms ease, visibility 0s linear 200ms; }
406
+ .away-moon.on { opacity: 1; visibility: visible; transition: opacity 150ms ease, visibility 0s; }
407
+ .away-moon svg { display: block; }
408
+ @media (prefers-reduced-motion: reduce) {
409
+ .away-moon { transition: opacity 100ms ease, visibility 0s linear 100ms; }
410
+ .away-moon.on { transition: opacity 80ms ease, visibility 0s; }
411
+ }
327
412
  .lane-github { display: inline-flex; align-items: center; gap: 2px; }
328
413
  .lane-github .github { display: inline-flex; align-items: center; gap: 4px; }
329
414
  .lane-github .gh-mark { flex: none; }
415
+ .lane-github:has(.remote) { min-width: 0; }
416
+ .lane-github .remote { min-width: 0; }
417
+ .lane-github .remote-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
330
418
  .lane-github .gh-refresh { font-size: 15px; line-height: 1; padding: 2px 4px; margin-right: -6px; color: var(--muted); }
331
419
  .lane-github .gh-refresh[aria-disabled="true"] { opacity: .35; cursor: default; }
332
420
  /* INT-44: both refreshes rest and spin while they fetch */
@@ -342,28 +430,48 @@ button.attach:disabled { opacity: .35; }
342
430
  .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; }
343
431
  .top .nav #status { line-height: 1.1; white-space: nowrap; }
344
432
  .top .nav #refresh:disabled, .top .nav a.icon[aria-disabled="true"] { opacity: .35; pointer-events: none; }
433
+ /* [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 */
434
+ .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); }
435
+ .top .nav .arch-bulb:hover { color: var(--text); }
436
+ .top .nav .arch-bulb[hidden] { display: none; }
437
+ .top .nav .arch-bulb.running { color: var(--color-notice); }
438
+ .arch-bulb.running svg { filter: drop-shadow(0 0 4px currentColor); }
439
+ @media (prefers-reduced-motion: no-preference) { .arch-bulb.running svg { animation: arch-glow 1.4s ease-in-out infinite; } }
440
+ @keyframes arch-glow { 50% { filter: drop-shadow(0 0 1px currentColor); opacity: .55; } }
441
+ .arch-body { display: grid; gap: 12px; padding-bottom: 6px; }
442
+ .arch-body p { margin: 0; font-size: 13.5px; line-height: 1.45; }
443
+ .arch-last { display: grid; gap: 3px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--bg); font-size: 13.5px; }
444
+ .arch-last .muted { font-size: 12.5px; }
445
+ .arch-body .arch-waits { color: var(--color-notice); }
446
+ .arch-body .arch-run { width: 100%; min-height: 44px; font-size: 15px; }
345
447
  /* INT-64: offline, the header's time and refresh turn red */
346
448
  .view-board.offline .refresh #refresh, .refresh .link-state.offline { color: var(--color-danger); }
347
449
  .back { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 32px; height: 32px; color: var(--accent); text-decoration: none; }
348
450
  /* 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
349
- (docs/adr/0048-a-questions-takes-show-inline.md). The brand keeps that height when the rail hides the logo, so the
451
+ (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
350
452
  board's header and the sidebar's stay level. */
351
453
  .top { padding-top: max(4px, env(safe-area-inset-top)); padding-bottom: 4px; }
352
454
  .top .brand { gap: 4px; min-height: 44px; }
353
455
  .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; }
456
+ .top .hub + .back { margin-left: 0; }
354
457
  .top .back:active { background: var(--accent-soft); }
355
458
  .top .brand :is(h1, h2) { font-size: 17px; font-weight: 600; line-height: 22px; }
356
459
  /* The last visit paints dimmed until the fresh board lands; the controls ui-snapshot.js liveControls blocks rest meanwhile.
357
- A changed card, or other changed line, gets a builder-orange border that fades once (app-snapshot.js flashChanged). */
460
+ A changed line, and a card that changed lanes, gets a blue border that fades once; an updated card, open body included, takes
461
+ a blue tint that fades (app-snapshot.js, whose SIGNS holds each animation's length). */
358
462
  main > * { transition: opacity .25s ease; }
359
463
  .view-board[data-board="snapshot"]:not(.offline) main > * { opacity: .62; }
360
464
  .view-board[data-board="snapshot"] :is(.actions button, button[data-plan], button[data-bump], button[data-confirm="bump"], button[data-edit], button[data-withdraw], button[data-check-github], form[data-reply] button[type=submit], form[data-reply] button[data-quick], form[data-reply] button[data-proto], form[data-reply] button[data-confirm], .prompt button) { opacity: .45; pointer-events: none; }
361
- .flash { animation: snapshot-flash 1.6s ease-out 1; border-radius: 6px; }
362
- @keyframes snapshot-flash { from { box-shadow: inset 0 0 0 2px var(--color-notice); } to { box-shadow: inset 0 0 0 2px transparent; } }
465
+ .flash { animation: snapshot-flash 1.6s ease-out var(--sign-delay, 0s) 1; border-radius: 6px; }
466
+ @keyframes snapshot-flash { from { box-shadow: inset 0 0 0 2px var(--color-info); } to { box-shadow: inset 0 0 0 2px transparent; } }
467
+ .item.tint { animation: card-tint 2.4s ease-out var(--sign-delay, 0s) 1; border-radius: 6px; }
468
+ @keyframes card-tint { 0%, 25% { background: var(--accent-soft); } 100% { background: transparent; } }
363
469
  .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; }
364
- /* 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 */
470
+ /* [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 */
365
471
  .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; }
366
472
  .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); }
473
+ .hold-sheet .hold-lede { margin: 0 0 2px; font-size: 13.5px; line-height: 1.4; }
474
+ .hold-sheet .hub-view .dur-chips .btn { min-height: 44px; font-size: 15px; }
367
475
  .sheet .sheet-body { padding: 10px 16px max(12px, calc(env(safe-area-inset-bottom) - var(--app-bottom, 0px))); }
368
476
  .hub-links { list-style: none; margin: 0; padding: 0; }
369
477
  .hub-links li { display: flex; align-items: baseline; gap: 10px; padding: 9px 0; border-top: 1px solid var(--line); min-width: 0; }
@@ -378,7 +486,7 @@ main > * { transition: opacity .25s ease; }
378
486
  .setup-steps .dot { flex: none; }
379
487
 
380
488
  /* The board's other views (view-settings.js, view-questions.js, view-notes.js) each scope their rules by the class on their root. */
381
- .view-settings .set h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--accent); margin: 18px 0 6px; }
489
+ .view-settings .set h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin: 18px 0 6px; }
382
490
  .view-settings .who { display: flex; align-items: center; justify-content: space-between; padding: 9px 10px; border: 1px solid var(--line);
383
491
  border-radius: 9px; margin: 6px 0; background: var(--panel); }
384
492
  .view-settings .who.gone { opacity: .5; text-decoration: line-through; }
@@ -389,6 +497,15 @@ main > * { transition: opacity .25s ease; }
389
497
  .view-settings label.who { gap: 12px; cursor: pointer; }
390
498
  .view-settings label.who .sub { display: block; margin-top: 2px; }
391
499
  .view-settings input.toggle { flex: none; width: 1.4rem; height: 1.4rem; margin: 0; accent-color: var(--accent); }
500
+ .view-settings input.count { flex: none; width: 4.5em; text-align: center; }
501
+ /* A settings switch waiting on the server (ui.js waitOn) keeps its face, disabled, with a spinner beside it
502
+ (docs/adr/2026-11-03-1921-board-settings-grant-a-boards-session-permissions.md) */
503
+ .view-settings .toggle-slot { flex: none; display: inline-flex; align-items: center; gap: 8px; }
504
+ .view-settings .toggle-spin { display: none; color: var(--muted); }
505
+ .view-settings .toggle-slot:has(input.tap-waiting) .toggle-spin { display: inline-block; }
506
+ .view-settings input.toggle.tap-waiting { opacity: 1; cursor: default; }
507
+ .view-settings #permissions code { font-size: 11.5px; overflow-wrap: anywhere; }
508
+ .view-settings #permissions .refused-note { color: var(--color-danger); }
392
509
  .view-settings .close-board-row { width: 100%; text-align: left; color: var(--color-danger); }
393
510
  .view-settings .repo-row { width: 100%; gap: 12px; text-align: left; }
394
511
  .view-settings .repo-row > :first-child { min-width: 0; overflow-wrap: anywhere; }
@@ -398,7 +515,7 @@ main > * { transition: opacity .25s ease; }
398
515
  .view-settings .close-board-sheet h2 { color: var(--text); font-size: 18px; text-transform: none; letter-spacing: normal; }
399
516
  .view-settings .close-board-sheet input { display: block; width: 100%; margin-top: 8px; }
400
517
  .view-settings .close-board-sheet .danger { color: var(--color-danger); }
401
- .view-settings .close-board-warning:has(li) { border: 1px solid var(--color-notice); padding: 0 12px; border-radius: 10px; margin-bottom: 14px; }
518
+ .view-settings .close-board-warning:has(li) { border: 1px solid var(--color-danger); padding: 0 12px; border-radius: 10px; margin-bottom: 14px; }
402
519
  .view-questions .q { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 12px; margin: 12px 0; }
403
520
  .view-questions .q.urgent { border-color: var(--color-notice); }
404
521
  .view-questions .q h3 { font-size: 15px; margin: 6px 0 4px; line-height: 1.3; }
@@ -411,13 +528,13 @@ main > * { transition: opacity .25s ease; }
411
528
  .view-questions .opt .rec { color: var(--accent); font-weight: 500; font-size: 12px; margin-left: 4px; }
412
529
  .view-questions .opt .det { color: var(--muted); font-size: 13px; }
413
530
  .view-questions .note { margin-top: 4px; }
414
- .view-questions .ans { background: var(--love); border-radius: 9px; padding: 7px 10px; margin-top: 8px; font-size: 13px; }
531
+ .view-questions .ans { background: var(--accent-soft); border-radius: 9px; padding: 7px 10px; margin-top: 8px; font-size: 13px; }
415
532
  .view-questions .rep { background: var(--claude); border-radius: 9px; padding: 7px 10px; margin-top: 6px; font-size: 13px; }
416
533
  .view-questions .q details { margin-top: 8px; }
417
534
  .view-questions .q details > summary { cursor: pointer; color: var(--muted); font-size: 13px; }
418
- .view-questions .topics { display: flex; gap: 6px; overflow-x: auto; padding: 2px 0 8px; margin-bottom: 4px; }
419
- .view-questions .topics button { flex: none; border: 1px solid var(--line); border-radius: 999px; padding: 4px 11px; font-size: 13px;
420
- background: var(--panel); color: var(--text); white-space: nowrap; }
535
+ .view-questions .topics { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 0 8px; margin-bottom: 4px; }
536
+ .view-questions .topics button { min-width: 0; border: 1px solid var(--line); border-radius: 999px; padding: 4px 11px; font-size: 13px;
537
+ background: var(--panel); color: var(--text); white-space: normal; text-align: left; overflow-wrap: anywhere; }
421
538
  .view-questions .topics button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
422
539
  .view-questions .topics .n { color: var(--muted); margin-left: 4px; }
423
540
  .view-questions .topichead { margin: 2px 0 6px; }
@@ -432,6 +549,12 @@ main > * { transition: opacity .25s ease; }
432
549
  .view-questions .bar #qstatus { min-width: 0; }
433
550
  .view-questions .bar #qstatus.sent { color: var(--color-success); font-weight: 600; }
434
551
  .view-questions .solo { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: 4px 0; }
552
+ /* docs/adr/2026-11-02-1332-a-prototype-round-ends-with-a-one-take-confirm.md */
553
+ .view-questions .topichead { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
554
+ .view-questions .topichead .try-spin { width: 9px; height: 9px; margin-right: 5px; vertical-align: -1px; }
555
+ .view-questions .q .pr-open { display: inline-block; margin: 0 0 8px; }
556
+ .view-questions .pr-rule { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; font-size: 12.5px; color: var(--muted); }
557
+ .view-questions .pr-rule svg { flex: none; }
435
558
 
436
559
  /* A question's takes sit in as many columns as keep each at least 380px wide, one on a phone; each its option above its
437
560
  preview, and each row's takes top-aligned, since each frame takes its own page's height. */
@@ -484,7 +607,7 @@ main > * { transition: opacity .25s ease; }
484
607
  .view-notes .plan ul, .view-notes .plan ol { padding-left: 22px; }
485
608
  .view-notes .plan li { margin: 3px 0; }
486
609
  .view-notes .plan li.nested { margin-left: 20px; }
487
- .view-notes .plan code { font-size: 12.5px; background: var(--accent-soft); border-radius: 4px; padding: 0 3px; }
610
+ .view-notes .plan code { font-size: 12.5px; background: var(--chip); border-radius: 4px; padding: 0 3px; }
488
611
  .view-notes .plan pre { overflow-x: auto; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; }
489
612
  .view-notes .plan pre code { background: none; padding: 0; overflow-wrap: normal; }
490
613
  .view-notes .plan blockquote { border-left: 3px solid var(--line); padding-left: 10px; color: var(--muted); }
@@ -504,7 +627,6 @@ main > * { transition: opacity .25s ease; }
504
627
  .view-frame > .frame-page { flex: 1 1 auto; display: block; width: 100%; min-height: 0; border: 0; background: var(--bg); }
505
628
  .view-frame .alone { white-space: nowrap; font-size: 13px; }
506
629
 
507
- .top .phone-logo { display: none; }
508
630
  @media (max-width: 899px) {
509
631
  :root:has(body > .top, body.has-sidebar > :not(.sidebar) > .top):not(.ps-embed) {
510
632
  --app-bottom: calc(49px + max(4px, env(safe-area-inset-bottom)));
@@ -513,15 +635,13 @@ main > * { transition: opacity .25s ease; }
513
635
  position: fixed; top: auto; bottom: 0; left: 0; right: 0; z-index: 7;
514
636
  border-bottom: 0; border-top: 1px solid var(--line); padding-top: 4px; padding-bottom: max(4px, env(safe-area-inset-bottom));
515
637
  }
516
- .top .phone-logo { display: inline-flex; margin-left: 0; }
517
- .top .nav #refresh, .top .nav a.icon { min-width: 44px; min-height: 44px; }
638
+ .top .nav #refresh, .top .nav a.icon, .top .nav .arch-bulb { min-width: 44px; min-height: 44px; }
518
639
  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)); }
519
640
  .view-frame:not(.own-header) { padding-bottom: var(--app-bottom, 0px); }
520
641
  .sheet { bottom: max(0px, calc(var(--app-bottom, 0px) - var(--kb, 0px)));
521
642
  max-height: calc(var(--vh, 100dvh) + var(--kb, 0px) - max(var(--app-bottom, 0px), var(--kb, 0px)) - env(safe-area-inset-top) - 12px); }
522
643
  .sheet.keyboard-sheet:not(.kb)::after { height: 0; }
523
644
  .sheet.keyboard-sheet { --sheet-limit: max(80px, calc(var(--vh, 100dvh) + var(--kb, 0px) - max(0px, calc(var(--app-bottom, 0px) - var(--kb, 0px))) - max(12px, env(safe-area-inset-top)))); }
524
- .confirm-sheet { padding-bottom: calc(10px + var(--app-bottom, 0px) + env(safe-area-inset-bottom)); }
525
645
  }
526
646
 
527
647
  .settings-modal { position: fixed; inset: max(12px, env(safe-area-inset-top)) 0 calc(12px + max(var(--kb, 0px), var(--app-bottom, 0px))); margin: auto; width: min(680px, calc(100% - 24px));
@@ -536,11 +656,25 @@ main > * { transition: opacity .25s ease; }
536
656
  .settings-body { overflow-y: auto; overscroll-behavior: contain; min-height: 0; padding: 0 20px 16px; }
537
657
  .settings-body input { font-size: 16px; }
538
658
  .settings-footer { display: flex; justify-content: flex-end; padding: 12px 20px; border-top: 1px solid var(--line); }
659
+ .lk-checking { display: inline-flex; align-items: center; gap: 6px; }
660
+ /* linear-key.js's modal over a page sits in .hub-view main only for hub.css's selectors. */
661
+ .lk-over, .lk-over > main { display: contents; }
539
662
 
540
663
  .tk-section { margin: 10px 0; }
664
+ /* The Tickets source row's live state, and Hub's key field opened under it (dialogs.js ticketsControl keyState). */
665
+ .tk-status { display: flex; align-items: center; gap: 8px; margin: 8px 0 0; font-size: 13px; color: var(--muted); }
666
+ .tk-status b { font-weight: 600; color: var(--text); }
667
+ .tk-status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--line); flex: none; }
668
+ .tk-status.on .dot { background: var(--accent); }
669
+ .tk-status.on b { color: var(--accent); }
670
+ .tk-inline { margin: 6px 0 10px; border: 1px solid var(--accent); border-radius: 10px; padding: 8px 10px; background: var(--accent-soft); }
671
+ .tk-inline .tk-ask { margin: 2px 0 6px; font-size: 13px; }
672
+ .tk-inline .hub-view main { padding: 0; }
541
673
  .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); }
542
674
  .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; }
543
675
  .tk-seg button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 600; }
676
+ .ask-seg { grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 10px 0 0; }
677
+ .ask-choice-help { margin: 4px 2px 0; }
544
678
  .tk-seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
545
679
  .tk-grid { display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr); }
546
680
  @media (min-width: 560px) { .tk-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); } }
@@ -560,11 +694,65 @@ main > * { transition: opacity .25s ease; }
560
694
  .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; }
561
695
  @keyframes try-spin { to { transform: rotate(360deg); } }
562
696
  @media (prefers-reduced-motion: reduce) { .try-spin { animation: none; } }
697
+ /* 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,
698
+ 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
699
+ 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
700
+ its arrow and pulses orange until the card moves; a toggle, select or field keeps its face with a spinner beside it */
701
+ button[aria-busy="true"], button[aria-busy="true"]:disabled, .ask-actions .icon[aria-busy="true"]:disabled { cursor: default; opacity: 1; }
702
+ :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; }
703
+ :is(button[aria-busy="true"]:not([data-card-bump], [data-bump], .sb-update-row), .sb-update-row[aria-busy="true"] .sb-go) > * { visibility: hidden; }
704
+ :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;
705
+ 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; }
706
+ @keyframes wait-orange { 50% { color: var(--color-notice); opacity: 1; } }
707
+ button[aria-busy="true"]:is([data-card-bump], [data-bump]) { color: var(--muted); animation: wait-orange 1s ease-in-out infinite; }
708
+ button[aria-busy="true"]:is([data-card-bump], [data-bump]) svg { fill: currentColor; }
709
+ .try-spin.tap-beside { width: 12px; height: 12px; border-width: 2px; margin: 0 8px 0 auto; color: var(--muted); }
710
+ .tk-field > span > .try-spin.tap-beside { margin: 0 0 0 6px; vertical-align: -2px; }
711
+ @media (prefers-reduced-motion: reduce) {
712
+ :is(button[aria-busy="true"], .sb-update-row[aria-busy="true"] .sb-go)::after { animation: none; }
713
+ button[aria-busy="true"]:is([data-card-bump], [data-bump]) { animation: none; color: var(--color-notice); }
714
+ }
563
715
  .install { display: inline-flex; align-items: center; gap: 4px; }
564
716
  .external-links .install.tone-ready { color: var(--color-success); }
565
717
  .external-links .install.tone-danger { color: var(--color-danger); }
566
718
  .actions button[aria-disabled="true"] { opacity: .5; cursor: default; }
567
719
  .tb-main { margin-top: 14px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 9px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); font-size: 13px; }
568
- .tb-main svg { flex: none; color: var(--muted); }
720
+ .tb-main > svg { flex: none; color: var(--muted); }
569
721
  .tb-main-text { flex: 1 1 200px; min-width: 0; overflow-wrap: anywhere; }
570
722
  .tb-main .meta { justify-content: flex-end; margin-left: auto; }
723
+ /* #60: Main's refresh icon right of its timestamp; a failed pull's reason shows on hover, or on a tap on touch */
724
+ .tb-main .main-pull { position: relative; display: inline-flex; align-items: center; vertical-align: middle; margin-left: 6px; }
725
+ .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; }
726
+ .tb-main .gh-refresh[aria-disabled="true"] { opacity: .45; cursor: default; }
727
+ .tb-main .gh-refresh.failed { color: var(--color-danger); }
728
+ .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; }
729
+ .main-pull-why.open { visibility: visible; }
730
+ @media (hover: hover) { .main-pull:hover .main-pull-why { visibility: visible; } }
731
+ .main-log-source { margin: 0 0 8px; overflow-wrap: anywhere; }
732
+ .main-log-lines { margin: 0; padding: 8px 10px; border-radius: 8px; background: var(--chip); color: var(--text);
733
+ font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11.5px; line-height: 1.5;
734
+ max-height: calc(12 * 1.5 * 11.5px + 16px); white-space: pre-wrap; overflow-wrap: anywhere; overflow-y: auto; }
735
+ .main-log-actions { display: flex; justify-content: flex-end; margin-top: 12px; }
736
+ .main-log-actions button { display: inline-flex; align-items: center; gap: 7px; min-height: 40px; padding: 6px 18px; }
737
+ #mainLogReason { color: var(--color-notice); overflow-wrap: anywhere; }
738
+
739
+ /* The Auto Review lane (ui-cards.js reviewLane): its cards read like the board's, and Posted earlier's lines like a fold's. */
740
+ .rv-card, .rv-line { border-top: 1px solid var(--line); }
741
+ .rv-card:first-child, .rv-line:first-child { border-top: 0; }
742
+ .rv-card > summary, .rv-line > summary { list-style: none; cursor: pointer; }
743
+ .rv-card > summary::-webkit-details-marker, .rv-line > summary::-webkit-details-marker { display: none; }
744
+ .rv-card > summary:hover, .rv-line > summary:hover { background: color-mix(in srgb, var(--chip) 60%, transparent); }
745
+ .rv-card > summary { display: grid; grid-template-columns: 18px 1fr; gap: 8px; align-items: start; padding: 9px 12px; }
746
+ .rv-card > summary .status { margin-top: 2px; }
747
+ .rv-line > summary { padding: 8px 12px; font-size: 13px; }
748
+ .status.rv-needs { border-color: var(--color-danger); background: var(--color-danger); }
749
+ .rv-word.s-needs, .lane-github .rv-late { color: var(--color-danger); font-weight: 600; }
750
+ .rv-detail { display: grid; gap: 10px; padding: 0 12px 12px 38px; font-size: 14px; min-width: 0; }
751
+ .rv-line .rv-detail { padding-left: 12px; }
752
+ .rv-detail p { margin: 0; }
753
+ .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; }
754
+ .rv-review > * { margin: 0; }
755
+ .rv-links { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 13px; }
756
+ .rv-links a { color: var(--accent); text-decoration: none; }
757
+ .rv-acts { display: flex; flex-wrap: wrap; gap: 8px; }
758
+ .view-questions .bar #qstatus .try-spin { color: var(--accent); margin-right: 6px; }
@@ -1,12 +1,14 @@
1
1
  (function () {
2
+ // How long a toast fades in and out.
3
+ const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
4
+ const enterTime = () => reduced() ? 80 : 150;
5
+ const exitTime = () => reduced() ? 100 : 200;
6
+
2
7
  function create(root) {
3
8
  const stack = document.createElement('div');
4
9
  stack.setAttribute('class', 'toast-stack');
5
10
  stack.setAttribute('aria-label', 'Notifications');
6
11
  root.appendChild(stack);
7
- const reduced = matchMedia('(prefers-reduced-motion: reduce)');
8
- const enterTime = () => reduced.matches ? 80 : 150;
9
- const exitTime = () => reduced.matches ? 100 : 200;
10
12
  let entries = [];
11
13
  let pending = [];
12
14
  let frame = 0;