toolroll 0.5.0 → 0.7.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 (51) hide show
  1. package/README.md +38 -24
  2. package/dist/accent-colors.d.ts +6 -3
  3. package/dist/accent-colors.js +9 -4
  4. package/dist/browser/workspace.css +2 -2
  5. package/dist/browser/workspace.js +23 -23
  6. package/dist/browser-workspace.d.ts +46 -0
  7. package/dist/builder.d.ts +18 -1
  8. package/dist/builder.js +177 -5
  9. package/dist/chat-delivery.js +6 -4
  10. package/dist/claim.d.ts +15 -1
  11. package/dist/claim.js +30 -2
  12. package/dist/control-ui.js +1 -1
  13. package/dist/daemon.d.ts +9 -0
  14. package/dist/daemon.js +21 -0
  15. package/dist/decision.d.ts +6 -0
  16. package/dist/decision.js +12 -0
  17. package/dist/desktop-update.d.ts +1 -1
  18. package/dist/dispatch.d.ts +3 -1
  19. package/dist/dispatch.js +13 -0
  20. package/dist/dispose.js +11 -7
  21. package/dist/exhaustion.d.ts +14 -0
  22. package/dist/exhaustion.js +35 -0
  23. package/dist/install-method.d.ts +17 -0
  24. package/dist/install-method.js +61 -0
  25. package/dist/invoke.js +26 -0
  26. package/dist/lead-status.d.ts +7 -0
  27. package/dist/lead-status.js +15 -2
  28. package/dist/mate.js +5 -2
  29. package/dist/operate.d.ts +5 -0
  30. package/dist/operate.js +109 -15
  31. package/dist/provider-auth.d.ts +138 -0
  32. package/dist/provider-auth.js +291 -0
  33. package/dist/provider.d.ts +2 -0
  34. package/dist/provider.js +8 -1
  35. package/dist/releases.d.ts +76 -0
  36. package/dist/releases.js +225 -0
  37. package/dist/serve.d.ts +4 -0
  38. package/dist/serve.js +207 -76
  39. package/dist/setup-guide.d.ts +2 -2
  40. package/dist/setup-guide.js +2 -0
  41. package/dist/skills.d.ts +6 -0
  42. package/dist/skills.js +8 -0
  43. package/dist/store.d.ts +2 -2
  44. package/dist/store.js +13 -4
  45. package/dist/subscription-chat.js +3 -0
  46. package/dist/update-notice.d.ts +5 -0
  47. package/dist/update-notice.js +4 -0
  48. package/dist/work-index.js +22 -4
  49. package/dist/worktree.d.ts +13 -0
  50. package/dist/worktree.js +55 -29
  51. package/package.json +1 -1
package/dist/serve.js CHANGED
@@ -120,10 +120,12 @@ import { projectAuthority } from "./project-access.js";
120
120
  * repo, only runs whose task belongs to that repo (or to no repo yet).
121
121
  */
122
122
  import { createConnectionChecker } from "./provider-connection.js";
123
+ import { liftAuthPause, noteSignInProbe, signInNotices } from "./provider-auth.js";
123
124
  import { openRouterModelsCache, openRouterPickerScript } from "./openrouter-models.js";
124
125
  import { checkModels, isNewModel, livePin, modelOptions, modelWords, RUNTIME_TOOLS, runtimeStates, seenModels, setWatch, updateRuntime, watchState } from "./model-catalog.js";
125
126
  import { MODELS_CSS, modelsHtml, modelsScript } from "./models-ui.js";
126
127
  import { ASSISTANTS, modelChoices, detectPreparation, previewProjectInstructions, addProjectInstructions } from "./setup-guide.js";
128
+ import { projectSkillInstalled } from "./skills.js";
127
129
  import { previewSetup, approveSetup } from "./control-setup.js";
128
130
  import { controlSetupHtml, setupPreviewHtml, connectionHtml, connectionWords, hiddenFields } from "./control-ui.js";
129
131
  import { composerSchedule, scheduleEditorHtml, scheduleEditorScript } from "./task-composer.js";
@@ -229,6 +231,10 @@ import { verifyApproverByPassword, verifyApproverStanding } from "./principal.js
229
231
  import { runMateTurn, MATE_MESSAGE_MAX_CHARS } from "./mate.js";
230
232
  import { confirmCoordinatorProposal, confirmMateProposal, dismissCoordinatorProposal, dismissMateProposal } from "./mate-doors.js";
231
233
  import { envValue } from "./names.js";
234
+ import { cachedRelease, isNewer, runnerVersions, setUpdateChecks, updateChecksOff } from "./releases.js";
235
+ import { installMethod } from "./install-method.js";
236
+ import { PACKAGE_VERSION } from "./version.js";
237
+ import { updateNoticeWords } from "./update-notice.js";
232
238
  const SESSION_COOKIE = "standing-orders_session";
233
239
  const BODY_CAP = 16 * 1024;
234
240
  // URL encoding can triple UTF-8 bytes. Admit the existing bounded task
@@ -384,11 +390,46 @@ export function createDecisionServer(options) {
384
390
  }
385
391
  }
386
392
  const clock = options.clock ?? (() => new Date());
393
+ const install = installMethod(options.installBin);
394
+ /** What the last daily check found (the cache only, never the network), or nothing while checks are off. */
395
+ const updateFacts = () => {
396
+ if (options.configDir === undefined)
397
+ return null;
398
+ const switched = updateChecksOff(process.env, options.configDir);
399
+ const release = switched.off ? null : cachedRelease(options.configDir);
400
+ return { release, newer: release !== null && isNewer(release.version, PACKAGE_VERSION), on: !switched.off, byEnv: switched.byEnv };
401
+ };
402
+ /** Settings → Updates, for everyone who can open Settings; only an operator may flip the switch. */
403
+ const settingsUpdates = (actor, csrf) => {
404
+ const facts = updateFacts();
405
+ if (facts === null)
406
+ return null;
407
+ const known = new Map(runnerVersions(options.configDir).map(one => [one.runner, one.version]));
408
+ return {
409
+ current: PACKAGE_VERSION,
410
+ latest: facts.release === null ? null : { version: facts.release.version, newer: facts.newer, security: facts.release.security, notes: facts.release.notes, url: facts.release.url },
411
+ updateCommand: install.updateCommand,
412
+ check: { on: facts.on, byEnv: facts.byEnv, canManage: csrf !== "" && store.isInstanceOperator(actor) },
413
+ workers: store.listRunners().filter(one => one.retiredAt === null).map(one => {
414
+ const version = known.get(one.name) ?? null;
415
+ return { name: one.name, version, older: version !== null && isNewer(PACKAGE_VERSION, version) };
416
+ }),
417
+ };
418
+ };
387
419
  const workspaceRevision = prepareWorkspaceRevision(store);
388
420
  const workspaceIncarnation = randomBytes(16).toString('hex');
389
421
  const workspaceValidators = new WorkspaceValidatorCache();
390
422
  const providerHome = options.connectionHome ?? homedir();
391
- const connectionCheck = createConnectionChecker({ home: providerHome, clock, ...(options.connectionProbe === undefined ? {} : { probe: options.connectionProbe }) });
423
+ const checkConnection = createConnectionChecker({ home: providerHome, clock, ...(options.connectionProbe === undefined ? {} : { probe: options.connectionProbe }) });
424
+ // Every sign-in check the console makes also answers a sign-in pause.
425
+ const connectionCheck = async (provider, fresh) => {
426
+ const value = await checkConnection(provider, fresh);
427
+ try {
428
+ noteSignInProbe(store, provider, value.state, clock());
429
+ }
430
+ catch { /* the pause keeps its own state */ }
431
+ return value;
432
+ };
392
433
  const modelCatalog = openRouterModelsCache(options.modelCatalogFetcher);
393
434
  const modelSeams = {
394
435
  home: providerHome,
@@ -1338,6 +1379,7 @@ export function createDecisionServer(options) {
1338
1379
  const requestFacts = {
1339
1380
  theme: pinnedTheme(request.headers.cookie),
1340
1381
  accent: pinnedAccent(request.headers.cookie),
1382
+ updateSeen: /(?:^|;\s*)so-update-seen=([0-9A-Za-z.-]{1,60})(?:;|$)/.exec(request.headers.cookie ?? "")?.[1] ?? null,
1341
1383
  actor: who.name,
1342
1384
  csrf: who.via === "cookie" ? who.session.csrf : "",
1343
1385
  returnTo: safeReturn(url.pathname + url.search),
@@ -3752,7 +3794,7 @@ export function createDecisionServer(options) {
3752
3794
  const googleView = { connected: googleConnected(options.configDir ?? null)?.address ?? null, clientId: google?.clientId ?? "", redirect: origin === null ? null : `${origin}${GOOGLE_CALLBACK}` };
3753
3795
  return email === null ? { set: false, host: "", port: 587, secure: false, user: "", from: "", imapHost: "", imapPort: 993, google: googleView }
3754
3796
  : { set: true, host: email.host, port: email.port, secure: email.secure, user: email.user, from: email.from, imapHost: email.imap?.host ?? "", imapPort: email.imap?.port ?? 993, google: googleView };
3755
- })() : null, telegramDeliveryWords(store, loadBotToken(process.env, options.telegramTokenFile)), settingsWorkers(store, now)));
3797
+ })() : null, telegramDeliveryWords(store, loadBotToken(process.env, options.telegramTokenFile)), settingsWorkers(store, now), settingsUpdates(who.name, csrf)));
3756
3798
  }
3757
3799
  if (url.pathname === "/push/key") {
3758
3800
  // Session-gated: the key is not secret, but strangers get nothing.
@@ -3804,7 +3846,7 @@ export function createDecisionServer(options) {
3804
3846
  return null;
3805
3847
  const repos = admissionList() ?? [];
3806
3848
  const setupDone = repos.filter(one => store.liveWorktreeSetup(one) !== null).length;
3807
- const skillDone = repos.filter(one => ["toolroll", "standing-orders"].some(name => existsSync(join(one, ".claude", "skills", name, "SKILL.md")))).length;
3849
+ const skillDone = repos.filter(projectSkillInstalled).length;
3808
3850
  const counted = (done) => repos.length <= 1 ? "" : ` (${done} of ${repos.length} repos)`;
3809
3851
  return [
3810
3852
  {
@@ -4429,6 +4471,8 @@ export function createDecisionServer(options) {
4429
4471
  navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
4430
4472
  chats,
4431
4473
  ...(s.refreshSeconds === undefined ? {} : { refreshSeconds: Math.max(5, Math.floor(s.refreshSeconds)) }),
4474
+ ...(s.chrome.signIn === undefined ? {} : { signIn: s.chrome.signIn }),
4475
+ ...(s.chrome.update === undefined ? {} : { update: s.chrome.update }),
4432
4476
  };
4433
4477
  if (requestFacts.workspaceRead) {
4434
4478
  const validator = requestFacts.workspaceValidator;
@@ -4544,6 +4588,16 @@ export function createDecisionServer(options) {
4544
4588
  inboxSaturated: badge.saturated,
4545
4589
  settings: true,
4546
4590
  ...(store.isDemo() ? { demo: true } : {}),
4591
+ ...(() => { const signIn = signInNotices(store); return signIn.length === 0 ? {} : { signIn }; })(),
4592
+ ...(() => {
4593
+ // Operators only (only someone at this computer can update it), and gone once this version is dismissed.
4594
+ if (!facts?.csrf || !actor || !store.isInstanceOperator(actor))
4595
+ return {};
4596
+ const update = updateFacts();
4597
+ if (update === null || !update.newer || update.release === null || facts.updateSeen === update.release.version)
4598
+ return {};
4599
+ return { update: { version: update.release.version, security: update.release.security, href: "/settings#updates", dismissHref: "/settings/updates/dismiss" } };
4600
+ })(),
4547
4601
  ...(liveMode === null || liveModeTerms === null
4548
4602
  ? {}
4549
4603
  : {
@@ -5729,6 +5783,41 @@ export function createDecisionServer(options) {
5729
5783
  const denied = authorizeMutation(request, who, body);
5730
5784
  if (denied !== null)
5731
5785
  return refuse(response, who, denied.status, denied.message);
5786
+ // A person says a provider's sign-in works again: its sign-in pause
5787
+ // lifts, its tasks may start, and one short message says so.
5788
+ const resumeProvider = /^\/providers\/([a-z]+)\/resume$/.exec(url.pathname);
5789
+ if (resumeProvider !== null) {
5790
+ const provider = resumeProvider[1];
5791
+ if (!isProviderId(provider))
5792
+ return refuse(response, who, 400, "unknown provider", "/work");
5793
+ if (who.role !== "approver")
5794
+ return refuse(response, who, 403, "Only an operator can resume an agent's work.", "/work");
5795
+ liftAuthPause(store, provider, "person", who.name, clock());
5796
+ return redirect(response, "/work");
5797
+ }
5798
+ // The update notice is dismissed per browser and per version: a newer release shows it again.
5799
+ if (url.pathname === "/settings/updates/dismiss") {
5800
+ const version = body.get("version") ?? "";
5801
+ if (!/^[0-9A-Za-z.-]{1,60}$/.test(version))
5802
+ return refuse(response, who, 400, "Choose a version to dismiss.", "/settings#updates");
5803
+ response.setHeader("Set-Cookie", `so-update-seen=${version}; SameSite=Lax; Path=/; Max-Age=31536000`);
5804
+ if (body.get("quiet") === "1") {
5805
+ response.writeHead(204);
5806
+ response.end();
5807
+ return;
5808
+ }
5809
+ return redirect(response, safeReturn(body.get("return") ?? "/settings#updates"));
5810
+ }
5811
+ // The daily check's switch: an installation setting, so an operator's.
5812
+ if (url.pathname === "/settings/updates") {
5813
+ if (options.configDir === undefined)
5814
+ return refuse(response, who, 404, "Updates are not set up on this console.", "/settings");
5815
+ if (!store.isInstanceOperator(who.name))
5816
+ return refuse(response, who, 403, "Only an operator can change update checks.", "/settings#updates");
5817
+ const on = body.get("check") === "on";
5818
+ setUpdateChecks(options.configDir, on);
5819
+ return redirect(response, `/settings?said=${encodeURIComponent(on ? "Toolroll looks for a newer version once a day." : "Update checks are off.")}#updates`);
5820
+ }
5732
5821
  if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
5733
5822
  const wantsJson = request.headers.accept?.includes('application/json') === true;
5734
5823
  const fail = (status, message, delivery = 'rejected', sessionId) => wantsJson
@@ -6634,7 +6723,7 @@ export function createDecisionServer(options) {
6634
6723
  if (accentField !== null) {
6635
6724
  const accent = normalHex(accentField);
6636
6725
  if (accent === null)
6637
- return refuse(response, who, 400, "Choose a colour as six hex digits, like #c0267e.", "/settings");
6726
+ return refuse(response, who, 400, "Choose a colour as six hex digits, like #6e56cf.", "/settings");
6638
6727
  response.setHeader("Set-Cookie", accent === DEFAULT_ACCENT
6639
6728
  ? "so-accent=; SameSite=Lax; Path=/; Max-Age=0"
6640
6729
  : `so-accent=${accent.slice(1)}; SameSite=Lax; Path=/; Max-Age=31536000`);
@@ -10641,7 +10730,7 @@ function page(response, status, html, nonce, fetches) {
10641
10730
  * can start a sign-in and have someone else finish it into their project.
10642
10731
  */
10643
10732
  /** A moment's page on the way to or back from another site's sign-in: no stylesheet may load there, so the palette rides inline. */
10644
- const HANDOFF_STYLE = `<style>body{font:15px/1.5 "Geist",system-ui,sans-serif;margin:2rem;background:#efefef;color:#171717}a{color:#171717;text-decoration-color:#c0267e;text-underline-offset:3px}@media(prefers-color-scheme:dark){body{background:#0b0b0b;color:#ededed}a{color:#ededed;text-decoration-color:#ff6fb5}}</style>`;
10733
+ const HANDOFF_STYLE = `<style>body{font:15px/1.5 "Geist",system-ui,sans-serif;margin:2rem;background:#efefef;color:#171717}a{color:#171717;text-decoration-color:#8f8f8f;text-underline-offset:3px}@media(prefers-color-scheme:dark){body{background:#0b0b0b;color:#ededed}a{color:#ededed;text-decoration-color:#ff6fb5}}</style>`;
10645
10734
  const SIGN_IN_COOKIE = "so-sign-in";
10646
10735
  function startedHere(request, state) {
10647
10736
  const held = new RegExp(`(?:^|;\\s*)${SIGN_IN_COOKIE}=([A-Za-z0-9_-]{16,128})`).exec(request.headers.cookie ?? "")?.[1];
@@ -11255,7 +11344,7 @@ const THEME_LIGHT = `
11255
11344
  --so-ink: #171717; --so-muted: #666666; --so-line: #e6e6e6; --so-input-line: #d4d4d4;
11256
11345
  --so-accent: #171717; --so-accent-hover: #383838; --so-accent-text: #171717; --so-on-accent: #ffffff; --so-soft: #f2f2f2;
11257
11346
  --so-nav-ink: #525252; --so-nav-hover: #e4e4e4; --so-nav-current: #ffffff; --so-nav-current-ink: #171717;
11258
- --so-signal: #c0267e; --so-signal-hover: #a81f6d; --so-on-signal: #ffffff; --so-signal-soft: #fbeaf3;
11347
+ --so-signal: #171717; --so-signal-hover: #383838; --so-on-signal: #ffffff; --so-signal-soft: #e8e8e8;
11259
11348
  --so-danger: #c4320a; --so-danger-soft: #feebe7; --so-success: #218358; --so-success-soft: #e6f6eb;
11260
11349
  --so-warning: #ab6400; --so-warning-soft: #fff4d5; --so-info: #0d74ce; --so-info-soft: #e6f4fe;
11261
11350
  --so-neutral-ink: #525252; --so-neutral-soft: #f0f0f0; --so-live: #0d74ce;
@@ -11263,13 +11352,13 @@ const THEME_LIGHT = `
11263
11352
  --so-sheet-shadow: 0 0 0 1px rgb(0 0 0 / .06), 0 1px 2px rgb(0 0 0 / .04), 0 4px 12px -6px rgb(0 0 0 / .06);
11264
11353
  --so-pill-shadow: 0 0 0 1px rgb(0 0 0 / .06), 0 1px 2px rgb(0 0 0 / .06);
11265
11354
  --so-overlay: rgb(0 0 0 / .32); --so-shadow-overlay: 0 0 0 1px rgb(0 0 0 / .08), 0 24px 48px -12px rgb(0 0 0 / .22);
11266
- --so-selection: #f6d3e6; --so-scroll: #cfcfcf; --so-code-bg: #fafafa; --so-user-bubble: #f2f2f2;`;
11355
+ --so-selection: #d4d4d4; --so-scroll: #cfcfcf; --so-code-bg: #fafafa; --so-user-bubble: #f2f2f2;`;
11267
11356
  const THEME_DARK = `
11268
11357
  --so-ground: #0b0b0b; --so-paper: #161616; --so-sidebar: #0b0b0b; --so-raised: #1c1c1c;
11269
11358
  --so-ink: #ededed; --so-muted: #a1a1a1; --so-line: #262626; --so-input-line: #363636;
11270
11359
  --so-accent: #ededed; --so-accent-hover: #ffffff; --so-accent-text: #ededed; --so-on-accent: #0a0a0a; --so-soft: #1f1f1f;
11271
11360
  --so-nav-ink: #a1a1a1; --so-nav-hover: #171717; --so-nav-current: #1f1f1f; --so-nav-current-ink: #ededed;
11272
- --so-signal: #ff6fb5; --so-signal-hover: #ff8cc4; --so-on-signal: #0a0a0a; --so-signal-soft: rgb(255 111 181 / .13);
11361
+ --so-signal: #ededed; --so-signal-hover: #ffffff; --so-on-signal: #0a0a0a; --so-signal-soft: #2e2e2e;
11273
11362
  --so-danger: #ff977d; --so-danger-soft: rgb(255 151 125 / .12); --so-success: #3dd68c; --so-success-soft: rgb(61 214 140 / .12);
11274
11363
  --so-warning: #ffca16; --so-warning-soft: rgb(255 202 22 / .12); --so-info: #70b8ff; --so-info-soft: rgb(112 184 255 / .12);
11275
11364
  --so-neutral-ink: #b4b4b4; --so-neutral-soft: #1f1f1f; --so-live: #70b8ff;
@@ -11277,7 +11366,7 @@ const THEME_DARK = `
11277
11366
  --so-sheet-shadow: 0 0 0 1px #262626, 0 1px 2px rgb(0 0 0 / .4);
11278
11367
  --so-pill-shadow: 0 0 0 1px #2a2a2a, 0 1px 2px rgb(0 0 0 / .5);
11279
11368
  --so-overlay: rgb(0 0 0 / .6); --so-shadow-overlay: 0 0 0 1px #2e2e2e, 0 24px 48px -12px rgb(0 0 0 / .7);
11280
- --so-selection: #4a2138; --so-scroll: #333333; --so-code-bg: #111111; --so-user-bubble: #202020;`;
11369
+ --so-selection: #3a3a3a; --so-scroll: #333333; --so-code-bg: #111111; --so-user-bubble: #202020;`;
11281
11370
  /** The console's original token names, now views onto the palette. */
11282
11371
  const THEME_MAPPING = `
11283
11372
  --background: var(--so-ground); --foreground: var(--so-ink); --card: var(--so-paper);
@@ -11296,7 +11385,7 @@ const STYLE = `
11296
11385
  /* The Console — the design system, v4 "Signal" (2026-09-27). A neutral
11297
11386
  frame with paper sheets inset into it (Arc, Linear), compact Raycast
11298
11387
  density, Geist for words and Geist Mono for machine facts. Ink is every
11299
- act a person can take; chart magenta means "waits on you" and nothing
11388
+ act a person can take; the accent (ink by default) means "waits on you" and nothing
11300
11389
  else; blue means live. Flat surfaces: hairlines and one soft sheet
11301
11390
  shadow, no glass. Zero dependencies, zero page JS beyond the nonce'd
11302
11391
  chrome layer. */
@@ -11334,6 +11423,7 @@ const STYLE = `
11334
11423
  ${THEME_LIGHT}
11335
11424
  ${THEME_MAPPING}
11336
11425
  --radius: 0.625rem;
11426
+ --so-ease-out: cubic-bezier(.23, 1, .32, 1);
11337
11427
  --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
11338
11428
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
11339
11429
  }
@@ -11348,6 +11438,11 @@ ${THEME_DARK}
11348
11438
  ${THEME_DARK}
11349
11439
  }
11350
11440
  * { box-sizing: border-box; }
11441
+ /* Touch: no grey flash on a tap, no double-tap zoom wait on a control,
11442
+ and a long press never selects a control's label. Text stays selectable. */
11443
+ html { -webkit-tap-highlight-color: transparent; }
11444
+ a, button, summary, [role=button], [role=tab] { touch-action: manipulation; }
11445
+ button, [role=button], [role=tab], .task-context-chip, .approval-chip, .verdict-chip { -webkit-user-select: none; user-select: none; }
11351
11446
  ::selection { background: var(--so-selection); color: var(--so-ink); }
11352
11447
  ::placeholder { color: var(--muted-foreground); }
11353
11448
  body {
@@ -11378,7 +11473,7 @@ ${THEME_DARK}
11378
11473
  color: var(--muted-foreground); text-decoration: none; font-size: 0.8125rem; font-weight: 500;
11379
11474
  display: flex; align-items: center; padding: 0 .625rem; transition: color .15s;
11380
11475
  }
11381
- .topbar nav a:hover { color: var(--foreground); }
11476
+ @media (hover: hover) and (pointer: fine) { .topbar nav a:hover { color: var(--foreground); } }
11382
11477
  main { max-width: 44rem; margin-inline: auto; padding: 1.75rem 1.25rem 4rem; }
11383
11478
  h1 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 .25rem; line-height: 1.3; }
11384
11479
  h1 .meta { font-weight: 400; letter-spacing: 0; }
@@ -11389,7 +11484,7 @@ ${THEME_DARK}
11389
11484
  color: var(--muted-foreground); margin: 2rem 0 .5rem; font-family: var(--font-sans);
11390
11485
  }
11391
11486
  a { color: var(--foreground); text-decoration: underline; text-decoration-color: var(--border); text-underline-offset: 3px; }
11392
- a:hover { text-decoration-color: var(--muted-foreground); }
11487
+ @media (hover: hover) and (pointer: fine) { a:hover { text-decoration-color: var(--muted-foreground); } }
11393
11488
  p { margin: .4rem 0; }
11394
11489
  code { background: var(--muted); border-radius: .3rem; padding: .1rem .35rem; font-family: var(--font-mono); font-size: .8125rem; }
11395
11490
  .mono { font-family: var(--font-mono); font-size: .8125rem; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
@@ -11497,7 +11592,7 @@ ${THEME_DARK}
11497
11592
  .row:last-of-type { border-bottom: none; }
11498
11593
  .row .right { margin-left: auto; }
11499
11594
  a.row { text-decoration: none; }
11500
- a.row:hover { background: var(--muted); }
11595
+ @media (hover: hover) and (pointer: fine) { a.row:hover { background: var(--muted); } }
11501
11596
 
11502
11597
  /* A parked decision: the question in full weight, the whole card the
11503
11598
  tap target, the neutral border — the "needs you" header above it
@@ -11508,38 +11603,38 @@ ${THEME_DARK}
11508
11603
  background: var(--card); padding: .875rem 1.125rem; margin: .625rem 0;
11509
11604
  text-decoration: none; transition: border-color .15s;
11510
11605
  }
11511
- .decide-card:hover { border-color: color-mix(in srgb, var(--border) 55%, var(--muted-foreground)); }
11606
+ @media (hover: hover) and (pointer: fine) { .decide-card:hover { border-color: color-mix(in srgb, var(--border) 55%, var(--muted-foreground)); } }
11512
11607
  .decide-card .q { font-weight: 600; margin: 0 0 .25rem; }
11513
11608
 
11514
11609
  /* Buttons: secondary by default (paper + hairline); a form's one
11515
11610
  submit is primary (ink on paper, paper on ink); the approve act is
11516
- magenta; danger is red and outlined. 2.125rem at a desk, 2.75rem to a thumb. */
11611
+ the accent; danger is red and outlined. 2.125rem at a desk, 2.75rem to a thumb. */
11517
11612
  button {
11518
11613
  font: 500 0.8125rem/1.4 var(--font-sans); cursor: pointer; border-radius: calc(var(--radius) - 3px);
11519
11614
  border: 1px solid var(--input); background: var(--card); color: var(--foreground);
11520
11615
  padding: .35rem .75rem; min-height: 2.125rem;
11521
- transition: background .12s ease-out, border-color .12s ease-out, color .12s ease-out;
11616
+ transition: background .12s var(--so-ease-out), border-color .12s var(--so-ease-out), color .12s var(--so-ease-out);
11522
11617
  }
11523
- button:hover { background: var(--so-soft); }
11618
+ @media (hover: hover) and (pointer: fine) { button:hover { background: var(--so-soft); } }
11524
11619
  button:active { background: var(--so-nav-hover); }
11525
11620
  form.card > button[type=submit], .sticky-actions button[type=submit], form.card .sticky-actions button {
11526
11621
  background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600;
11527
11622
  }
11528
- form.card > button[type=submit]:hover, .sticky-actions button[type=submit]:hover {
11623
+ @media (hover: hover) and (pointer: fine) { form.card > button[type=submit]:hover, .sticky-actions button[type=submit]:hover {
11529
11624
  background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: color-mix(in srgb, var(--primary) 85%, var(--background));
11530
- }
11625
+ } }
11531
11626
  /* The approve act is the one magenta verb: it resolves what waits on you.
11532
11627
  The ceremony's frame is neutral so the button is the only coloured thing
11533
11628
  in it; a danger act stays red even inside one. */
11534
11629
  .approve-form button[type=submit], .approve-form .sticky-actions button[type=submit] {
11535
11630
  background: var(--brand); color: var(--brand-foreground); border-color: var(--brand); font-weight: 600;
11536
11631
  }
11537
- .approve-form button[type=submit]:hover, .approve-form .sticky-actions button[type=submit]:hover { background: color-mix(in srgb, var(--brand) 85%, var(--foreground)); border-color: color-mix(in srgb, var(--brand) 85%, var(--foreground)); }
11632
+ @media (hover: hover) and (pointer: fine) { .approve-form button[type=submit]:hover, .approve-form .sticky-actions button[type=submit]:hover { background: color-mix(in srgb, var(--brand) 85%, var(--foreground)); border-color: color-mix(in srgb, var(--brand) 85%, var(--foreground)); } }
11538
11633
  button.danger, .approve-form button[type=submit].danger {
11539
11634
  color: var(--destructive); border-color: color-mix(in srgb, var(--destructive) 50%, transparent);
11540
11635
  background: transparent; font-weight: 500;
11541
11636
  }
11542
- button.danger:hover, .approve-form button[type=submit].danger:hover { background: var(--destructive-soft); }
11637
+ @media (hover: hover) and (pointer: fine) { button.danger:hover, .approve-form button[type=submit].danger:hover { background: var(--destructive-soft); } }
11543
11638
  /* Removing a credential takes a second, deliberate tap. */
11544
11639
  details.confirm-remove { display: inline-block; margin: 0 0 0 .5rem; vertical-align: top; }
11545
11640
  details.confirm-remove > summary { cursor: pointer; color: var(--destructive); font-size: .8125rem; min-height: 2.5rem; display: inline-flex; align-items: center; }
@@ -11553,7 +11648,7 @@ ${THEME_DARK}
11553
11648
  border: 1px solid var(--input); border-radius: calc(var(--radius) - 3px);
11554
11649
  transition: border-color .15s, box-shadow .15s;
11555
11650
  }
11556
- input:hover, textarea:hover, select:hover { border-color: var(--muted-foreground); }
11651
+ @media (hover: hover) and (pointer: fine) { input:hover, textarea:hover, select:hover { border-color: var(--muted-foreground); } }
11557
11652
  input[type=number] { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
11558
11653
  input[type=radio], input[type=checkbox] { accent-color: var(--ring); }
11559
11654
  .permission-field { border: 0; padding: 0; margin: 1rem 0 0; min-width: 0; }
@@ -11565,7 +11660,7 @@ ${THEME_DARK}
11565
11660
  border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--card) 66%, transparent);
11566
11661
  cursor: pointer; transition: border-color .15s, background .15s, box-shadow .15s;
11567
11662
  }
11568
- .permission-choice:hover { background: var(--muted); }
11663
+ @media (hover: hover) and (pointer: fine) { .permission-choice:hover { background: var(--muted); } }
11569
11664
  .permission-choice:has(input:checked) {
11570
11665
  border-color: color-mix(in srgb, var(--foreground) 42%, var(--border));
11571
11666
  background: var(--card); box-shadow: 0 0 0 1px color-mix(in srgb, var(--foreground) 8%, transparent), var(--shadow);
@@ -11597,7 +11692,8 @@ ${THEME_DARK}
11597
11692
  min-height: 8.5rem; max-height: 18rem; margin: 0; padding: .9rem 1rem; resize: vertical;
11598
11693
  border: 0; background: transparent; box-shadow: none; font-size: 1.05rem; line-height: 1.55;
11599
11694
  }
11600
- .task-prompt textarea:hover, .task-prompt textarea:focus-visible { border: 0; box-shadow: none; }
11695
+ .task-prompt textarea:focus-visible { border: 0; box-shadow: none; }
11696
+ @media (hover: hover) and (pointer: fine) { .task-prompt textarea:hover { border: 0; box-shadow: none; } }
11601
11697
  .task-repo { margin: .25rem .45rem .7rem; }
11602
11698
  .task-composer-footer { display: flex; align-items: center; gap: .5rem; padding: .25rem; }
11603
11699
  .task-context { display: flex; align-items: center; gap: .4rem; flex: 1 1 auto; min-width: 0; }
@@ -11616,7 +11712,7 @@ ${THEME_DARK}
11616
11712
  flex: none; min-height: 2.45rem; padding: .45rem .95rem; border-radius: 999px;
11617
11713
  background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600;
11618
11714
  }
11619
- .task-submit:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: transparent; }
11715
+ @media (hover: hover) and (pointer: fine) { .task-submit:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: transparent; } }
11620
11716
  details.task-options {
11621
11717
  margin: .65rem .25rem 0; padding: .15rem .5rem 0; border: 0; border-top: 1px solid var(--glass-border);
11622
11718
  border-radius: 0; background: transparent;
@@ -11703,7 +11799,7 @@ ${THEME_DARK}
11703
11799
  .receipt-visuals { display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 14rem)); gap: .55rem; margin-top: .75rem; }
11704
11800
  .receipt-shot { display: grid; gap: .35rem; color: var(--muted-foreground); font-size: .7rem; text-decoration: none; }
11705
11801
  .receipt-shot img { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: var(--muted); }
11706
- .receipt-shot:hover { color: var(--foreground); }
11802
+ @media (hover: hover) and (pointer: fine) { .receipt-shot:hover { color: var(--foreground); } }
11707
11803
  .receipt-caveats, .receipt-coverage { margin-top: .8rem; padding: .7rem .8rem; border-left: 1px solid var(--border); border-radius: 0 calc(var(--radius) - 3px) calc(var(--radius) - 3px) 0; background: color-mix(in srgb, var(--muted) 62%, transparent); font-size: .78rem; }
11708
11804
  .receipt-caveats ul, .receipt-coverage ul { margin: .3rem 0 0; padding-left: 1.15rem; }
11709
11805
  /* Secondary receipt detail (concise pass, 2026-09-13): native
@@ -11735,7 +11831,7 @@ ${THEME_DARK}
11735
11831
  display: grid; gap: .2rem; padding: .55rem .65rem; margin-bottom: .2rem; min-width: 0;
11736
11832
  border: 1px solid transparent; border-radius: calc(var(--radius) - 3px); text-decoration: none; color: var(--foreground);
11737
11833
  }
11738
- .cockpit-row:hover { background: var(--card); }
11834
+ @media (hover: hover) and (pointer: fine) { .cockpit-row:hover { background: var(--card); } }
11739
11835
  .cockpit-row.current { background: var(--muted); border-color: color-mix(in srgb, var(--foreground) 12%, var(--border)); }
11740
11836
  .cockpit-row-head { display: flex; align-items: flex-start; justify-content: space-between; gap: .5rem; min-width: 0; }
11741
11837
  .cockpit-row-head strong { min-width: 0; overflow-wrap: anywhere; font-size: .8125rem; font-weight: 550; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
@@ -11812,7 +11908,7 @@ ${THEME_DARK}
11812
11908
  min-height: 1.75rem; padding: .2rem .7rem; border: 0; border-radius: 999px;
11813
11909
  background: transparent; box-shadow: none; color: var(--muted-foreground); font-size: .7rem;
11814
11910
  }
11815
- .diff-modes button:hover { transform: none; color: var(--foreground); }
11911
+ @media (hover: hover) and (pointer: fine) { .diff-modes button:hover { transform: none; color: var(--foreground); } }
11816
11912
  .diff-modes button[aria-pressed="true"] { background: var(--card); color: var(--foreground); box-shadow: 0 1px 3px color-mix(in srgb, var(--background) 20%, transparent); }
11817
11913
  .diff-review-help {
11818
11914
  margin: 0; padding: .62rem .85rem; border-bottom: 1px solid var(--glass-border);
@@ -11849,7 +11945,7 @@ ${THEME_DARK}
11849
11945
  display: grid; grid-template-columns: 2rem 3.2rem 3.2rem minmax(max-content, 1fr); align-items: stretch;
11850
11946
  min-width: max-content; min-height: 1.8rem; font: 400 .72rem/1.55 var(--font-mono);
11851
11947
  }
11852
- .diff-line:hover { background: color-mix(in srgb, var(--foreground) 4%, transparent); }
11948
+ @media (hover: hover) and (pointer: fine) { .diff-line:hover { background: color-mix(in srgb, var(--foreground) 4%, transparent); } }
11853
11949
  .diff-line code { display: flex; min-width: 0; padding: .3rem .75rem .3rem .6rem; color: inherit; white-space: pre; }
11854
11950
  .diff-line code b { display: inline-block; width: 1rem; flex: none; font-weight: 500; opacity: .72; }
11855
11951
  .diff-gutter {
@@ -11865,7 +11961,7 @@ ${THEME_DARK}
11865
11961
  }
11866
11962
  .diff-annotate { padding: 0; background: color-mix(in srgb, var(--card) 94%, transparent); color: var(--muted-foreground); box-shadow: none; opacity: .35; }
11867
11963
  .diff-annotate svg { width: .78rem; height: .78rem; }
11868
- .diff-annotate:hover { transform: none; background: color-mix(in srgb, var(--running) 15%, var(--card)); color: var(--running); opacity: 1; }
11964
+ @media (hover: hover) and (pointer: fine) { .diff-annotate:hover { transform: none; background: color-mix(in srgb, var(--running) 15%, var(--card)); color: var(--running); opacity: 1; } }
11869
11965
  .diff-annotate-space { background: color-mix(in srgb, var(--card) 94%, transparent); }
11870
11966
  .diff-review[data-mode="view"] .diff-line { grid-template-columns: 0 3.2rem 3.2rem minmax(max-content, 1fr); }
11871
11967
  .diff-review[data-mode="view"] .diff-annotate,
@@ -11935,11 +12031,11 @@ ${THEME_DARK}
11935
12031
  details[open] { padding-bottom: .875rem; }
11936
12032
  details.arm-danger { border-color: color-mix(in srgb, var(--destructive) 30%, transparent); }
11937
12033
  summary { padding: .5rem 0; cursor: pointer; font-weight: 500; font-size: 0.8125rem; color: var(--muted-foreground); min-height: 2.25rem; }
11938
- summary:hover { color: var(--foreground); }
12034
+ @media (hover: hover) and (pointer: fine) { summary:hover { color: var(--foreground); } }
11939
12035
  details form.option { border: none; padding: .25rem 0 0; margin: 0; }
11940
12036
  .evidence { margin-top: 1.5rem; font-size: 0.8125rem; }
11941
12037
  .evidence a { display: block; padding: .55rem 0; border-bottom: 1px solid var(--border); text-decoration: none; }
11942
- .evidence a:hover { color: var(--muted-foreground); }
12038
+ @media (hover: hover) and (pointer: fine) { .evidence a:hover { color: var(--muted-foreground); } }
11943
12039
  .evidence strong { display: block; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: .08em; color: var(--muted-foreground); font-family: var(--font-mono); }
11944
12040
 
11945
12041
  .filters { font-size: 0.8125rem; color: var(--muted-foreground); }
@@ -11948,7 +12044,7 @@ ${THEME_DARK}
11948
12044
  color: var(--muted-foreground); font-weight: 500;
11949
12045
  }
11950
12046
  .filters strong { background: var(--muted); color: var(--foreground); }
11951
- .filters a:hover { color: var(--foreground); }
12047
+ @media (hover: hover) and (pointer: fine) { .filters a:hover { color: var(--foreground); } }
11952
12048
 
11953
12049
  /* A picked tournament result: marked in the built green, not magenta. */
11954
12050
  .card.picked { border-color: color-mix(in srgb, var(--success) 40%, var(--border)); }
@@ -11976,7 +12072,7 @@ ${THEME_DARK}
11976
12072
  display: grid; place-items: center; flex: 0 0 2rem; width: 2rem; min-height: 2rem; padding: 0;
11977
12073
  border-color: transparent; background: transparent; color: var(--muted-foreground); box-shadow: none;
11978
12074
  }
11979
- .side-toggle:hover { background: var(--glass); color: var(--foreground); transform: none; }
12075
+ @media (hover: hover) and (pointer: fine) { .side-toggle:hover { background: var(--glass); color: var(--foreground); transform: none; } }
11980
12076
  .side-toggle svg { width: 1rem; height: 1rem; }
11981
12077
  /* The scope bar: one hairline row, the single scope truth on every
11982
12078
  * screen; its name is the switcher. Magenta never appears here except
@@ -12012,7 +12108,7 @@ ${THEME_DARK}
12012
12108
  border-right: 1.75px solid var(--foreground); border-bottom: 1.75px solid var(--foreground);
12013
12109
  transform: rotate(45deg) translateY(-.125rem);
12014
12110
  }
12015
- .switcher-menu button:hover { background: var(--muted); }
12111
+ @media (hover: hover) and (pointer: fine) { .switcher-menu button:hover { background: var(--muted); } }
12016
12112
  .switcher-menu .manage {
12017
12113
  display: block; margin-top: .25rem; padding: .625rem .75rem; border-top: 1px solid var(--border);
12018
12114
  font-size: .75rem; color: var(--muted-foreground); text-decoration: none;
@@ -12024,14 +12120,14 @@ ${THEME_DARK}
12024
12120
  }
12025
12121
  .scope-status .hot { color: var(--brand); font-weight: 500; }
12026
12122
  .scope-status a { color: inherit; text-decoration: none; }
12027
- .scope-status a:hover { text-decoration: underline; }
12123
+ @media (hover: hover) and (pointer: fine) { .scope-status a:hover { text-decoration: underline; } }
12028
12124
  /* A project card's name and counts are forms or links dressed as text and chips. */
12029
12125
  .project-card button.project-name, .project-card a.project-name {
12030
12126
  all: unset; cursor: pointer; font-weight: 600; color: var(--foreground);
12031
12127
  }
12032
- .project-card a.project-name:hover, .project-card button.project-name:hover { text-decoration: underline; }
12128
+ @media (hover: hover) and (pointer: fine) { .project-card a.project-name:hover, .project-card button.project-name:hover { text-decoration: underline; } }
12033
12129
  .project-card button.badge { min-height: auto; box-shadow: none; cursor: pointer; }
12034
- .project-card button.badge:hover, .project-card a.badge:hover { border-color: var(--input); }
12130
+ @media (hover: hover) and (pointer: fine) { .project-card button.badge:hover, .project-card a.badge:hover { border-color: var(--input); } }
12035
12131
  /* Adding a project is the page's primary job, not badge-sized metadata.
12036
12132
  * Two roomy action tiles make both roads obvious on a desk and give each
12037
12133
  * one a generous thumb target on a phone. The manual path stays tertiary. */
@@ -12054,11 +12150,11 @@ ${THEME_DARK}
12054
12150
 
12055
12151
  transition: transform .15s ease, background .15s, border-color .15s, box-shadow .15s;
12056
12152
  }
12057
- .project-add-action:hover {
12153
+ @media (hover: hover) and (pointer: fine) { .project-add-action:hover {
12058
12154
  border-color: color-mix(in srgb, var(--border) 50%, var(--muted-foreground));
12059
12155
  background: var(--card); text-decoration: none;
12060
12156
  box-shadow: var(--shadow);
12061
- }
12157
+ } }
12062
12158
  .project-add-action:active { transform: translateY(0); }
12063
12159
  .project-add-icon {
12064
12160
  display: grid; place-items: center; width: 2.75rem; height: 2.75rem;
@@ -12078,7 +12174,7 @@ ${THEME_DARK}
12078
12174
  display: flex; align-items: center; min-height: 2.75rem; width: fit-content;
12079
12175
  color: var(--muted-foreground); cursor: pointer; font-size: .8125rem; font-weight: 500;
12080
12176
  }
12081
- .project-add-more > summary:hover { color: var(--foreground); }
12177
+ @media (hover: hover) and (pointer: fine) { .project-add-more > summary:hover { color: var(--foreground); } }
12082
12178
  .project-add-more > .card { margin: 0 0 .25rem; }
12083
12179
  .side nav { display: flex; flex-direction: column; gap: .125rem; }
12084
12180
  /* Inline decision options: neutral buttons — the card's magenta outline is
@@ -12101,7 +12197,7 @@ ${THEME_DARK}
12101
12197
  font-family: var(--font-sans);
12102
12198
  }
12103
12199
  .nav-group > summary::-webkit-details-marker { display: none; }
12104
- .nav-group > summary:hover { background: var(--glass); color: var(--foreground); }
12200
+ @media (hover: hover) and (pointer: fine) { .nav-group > summary:hover { background: var(--glass); color: var(--foreground); } }
12105
12201
  .nav-group > summary .chevron { width: .875rem; height: .875rem; flex: none; transition: transform .15s; }
12106
12202
  .nav-group[open] > summary .chevron { transform: rotate(180deg); }
12107
12203
  .nav-group .nav-group-items { display: flex; flex-direction: column; gap: .125rem; margin: .125rem 0 .25rem; }
@@ -12115,7 +12211,8 @@ ${THEME_DARK}
12115
12211
  }
12116
12212
  .side nav a .glyph { display: inline-flex; width: 1rem; height: 1rem; color: var(--muted-foreground); flex: none; }
12117
12213
  .side nav a .glyph svg { width: 1rem; height: 1rem; }
12118
- .side nav a:hover { background: var(--glass); color: var(--foreground); transform: translateX(2px); }
12214
+ @media (hover: hover) and (pointer: fine) { .side nav a:hover { background: var(--glass); color: var(--foreground); transform: translateX(2px); } }
12215
+ .side nav a:active { background: var(--glass); color: var(--foreground); }
12119
12216
  .app.sidebar-collapsed { grid-template-columns: 64px minmax(0, 1fr); }
12120
12217
  .app.sidebar-collapsed .side { padding-inline: .625rem; }
12121
12218
  .app.sidebar-collapsed .side-head { flex-direction: column; gap: .2rem; margin-bottom: .55rem; }
@@ -12148,14 +12245,14 @@ ${THEME_DARK}
12148
12245
  border-radius: calc(var(--radius) - 3px); padding: .525rem; margin: .75rem 0 .125rem;
12149
12246
  box-shadow: 0 10px 28px -18px rgb(255 255 255 / .5);
12150
12247
  }
12151
- .side .new-task:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); }
12248
+ @media (hover: hover) and (pointer: fine) { .side .new-task:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); } }
12152
12249
  /* The same action link outside the sidebar reads as a real button. */
12153
12250
  .content .new-task {
12154
12251
  display: inline-block; text-decoration: none; font-weight: 500; font-size: .8125rem;
12155
12252
  background: var(--secondary); color: var(--foreground); border: 1px solid var(--border);
12156
12253
  border-radius: calc(var(--radius) - 2px); padding: .5rem .875rem;
12157
12254
  }
12158
- .content .new-task:hover { background: color-mix(in srgb, var(--secondary) 70%, var(--border)); }
12255
+ @media (hover: hover) and (pointer: fine) { .content .new-task:hover { background: color-mix(in srgb, var(--secondary) 70%, var(--border)); } }
12159
12256
  .content { min-width: 0; }
12160
12257
  .content > main { max-width: 54rem; margin: 0; padding: 2rem 2.5rem 4rem; }
12161
12258
 
@@ -12166,6 +12263,7 @@ ${THEME_DARK}
12166
12263
  }
12167
12264
  .banner .badge { margin-right: .5rem; }
12168
12265
  .banner a { color: var(--muted-foreground); }
12266
+ .sign-in-banner form { display: inline; margin: 0 0 0 .5rem; padding: 0; border: 0; background: transparent; }
12169
12267
 
12170
12268
  .split { display: grid; grid-template-columns: minmax(250px, 320px) minmax(0, 1fr); min-height: 100vh; }
12171
12269
  .list-pane {
@@ -12177,7 +12275,7 @@ ${THEME_DARK}
12177
12275
  display: block; padding: .5rem .625rem; border-radius: calc(var(--radius) - 4px);
12178
12276
  text-decoration: none; font-size: .8125rem; margin-bottom: .125rem;
12179
12277
  }
12180
- .list-pane a.item:hover { background: var(--card); }
12278
+ @media (hover: hover) and (pointer: fine) { .list-pane a.item:hover { background: var(--card); } }
12181
12279
  .list-pane a.item.current { background: var(--muted); }
12182
12280
  .list-pane a.item .t { display: block; font-weight: 500; color: var(--foreground);
12183
12281
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -12206,13 +12304,13 @@ ${THEME_DARK}
12206
12304
  min-width: 4.6rem; padding: .4rem .72rem; border-radius: .52rem; color: var(--muted-foreground);
12207
12305
  font-size: .72rem; font-weight: 550; text-align: center; text-decoration: none;
12208
12306
  }
12209
- .task-view-switch a:hover { color: var(--foreground); background: color-mix(in srgb, var(--muted) 70%, transparent); }
12307
+ @media (hover: hover) and (pointer: fine) { .task-view-switch a:hover { color: var(--foreground); background: color-mix(in srgb, var(--muted) 70%, transparent); } }
12210
12308
  .task-view-switch a.active { color: var(--foreground); background: var(--glass-strong); box-shadow: 0 1px 5px rgb(0 0 0 / .1); }
12211
12309
  .acts-bar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: .75rem 0 .25rem; }
12212
12310
  .acts-bar form.inline { margin: 0; display: inline-flex; align-items: center; gap: .375rem; }
12213
12311
  .acts-bar form.inline button { width: auto; }
12214
12312
  .acts-bar .primary button { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }
12215
- .acts-bar .primary button:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); }
12313
+ @media (hover: hover) and (pointer: fine) { .acts-bar .primary button:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); } }
12216
12314
  .acts-bar .act-hold input[type=text] { width: 10rem; min-height: 2.25rem; margin: 0; font-size: .8125rem; }
12217
12315
  .acts-why { margin: 0 0 .5rem; }
12218
12316
  .dispatch-status { padding: .8rem .9rem; border-radius: var(--radius); overflow: hidden; }
@@ -12491,7 +12589,7 @@ ${THEME_DARK}
12491
12589
  transform: rotate(45deg) translateY(-.125rem); transition: transform .15s;
12492
12590
  }
12493
12591
  details.section[open] > summary::after { transform: rotate(225deg) translateY(-.125rem); }
12494
- details.section > summary:hover h2 { color: var(--foreground); }
12592
+ @media (hover: hover) and (pointer: fine) { details.section > summary:hover h2 { color: var(--foreground); } }
12495
12593
  details.more-agents { margin: .75rem 0 0; }
12496
12594
  .task-layout > .task-main { min-width: 0; }
12497
12595
  .task-rail { position: sticky; top: 1rem; }
@@ -12558,7 +12656,7 @@ ${THEME_DARK}
12558
12656
  background: var(--card); box-shadow: var(--shadow);
12559
12657
  }
12560
12658
  .work-tools-menu a { display: block; padding: .5rem .625rem; border-radius: .5rem; text-decoration: none; color: var(--foreground); font-size: .875rem; }
12561
- .work-tools-menu a:hover { background: var(--muted); }
12659
+ @media (hover: hover) and (pointer: fine) { .work-tools-menu a:hover { background: var(--muted); } }
12562
12660
  /* The result panel (workspace package 3): one presentation of a finished
12563
12661
  result for the run page, the review cockpit, and the chat's result
12564
12662
  detail — the deliverable first, problems ahead of readiness words,
@@ -12584,7 +12682,7 @@ ${THEME_DARK}
12584
12682
  .result-notes > summary::-webkit-details-marker, .result-details > summary::-webkit-details-marker { display: none; }
12585
12683
  .result-notes > summary::before, .result-details > summary::before { content: "▸"; }
12586
12684
  .result-notes[open] > summary::before, .result-details[open] > summary::before { content: "▾"; }
12587
- .result-notes > summary:hover, .result-details > summary:hover { color: var(--foreground); }
12685
+ @media (hover: hover) and (pointer: fine) { .result-notes > summary:hover, .result-details > summary:hover { color: var(--foreground); } }
12588
12686
  .result-details > summary .meta { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); font-size: .68rem; font-weight: 400; }
12589
12687
  .result-notes .recap { margin: .25rem 0 .4rem; font-size: .875rem; }
12590
12688
  .result-attention { margin: .75rem 0 .25rem; padding: .7rem .85rem; border-left: 1px solid var(--warning); border-radius: 0 calc(var(--radius) - 3px) calc(var(--radius) - 3px) 0; background: var(--warning-soft); font-size: .8125rem; }
@@ -12695,9 +12793,9 @@ ${THEME_DARK}
12695
12793
  .work-details > summary::-webkit-details-marker { display: none; }
12696
12794
  .work-details > summary::before { content: "▸"; }
12697
12795
  .work-details[open] > summary::before { content: "▾"; }
12698
- .work-details > summary:hover { color: var(--foreground); }
12796
+ @media (hover: hover) and (pointer: fine) { .work-details > summary:hover { color: var(--foreground); } }
12699
12797
  .work-title { display: block; font-weight: 600; font-size: .9375rem; line-height: 1.35; color: var(--foreground); text-decoration: none; overflow-wrap: anywhere; }
12700
- .work-title:hover { text-decoration: underline; }
12798
+ @media (hover: hover) and (pointer: fine) { .work-title:hover { text-decoration: underline; } }
12701
12799
  .work-meta { display: flex; flex-wrap: wrap; gap: .25rem .625rem; margin: .25rem 0 0; font-size: .75rem; color: var(--muted-foreground); }
12702
12800
  .work-meta .mono { overflow-wrap: anywhere; }
12703
12801
  .project-label { display: inline-block; padding: 0 .4rem; border: 1px solid var(--border); border-radius: 999px; font-size: .6875rem; line-height: 1.5; color: var(--foreground); }
@@ -12852,7 +12950,7 @@ ${THEME_DARK}
12852
12950
  border: 1px solid var(--border); border-radius: var(--radius); background: var(--card);
12853
12951
  padding: .75rem .875rem; color: var(--foreground); min-height: 44px; justify-content: center;
12854
12952
  }
12855
- .menu-row:hover { border-color: color-mix(in srgb, var(--border) 60%, var(--muted-foreground)); }
12953
+ @media (hover: hover) and (pointer: fine) { .menu-row:hover { border-color: color-mix(in srgb, var(--border) 60%, var(--muted-foreground)); } }
12856
12954
 
12857
12955
  .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(13.5rem, 1fr)); gap: .625rem; margin: .5rem 0; }
12858
12956
  .stat-card {
@@ -12887,7 +12985,7 @@ ${THEME_DARK}
12887
12985
  width: 100%; margin-top: 1.25rem;
12888
12986
  background: var(--so-accent); color: var(--so-on-accent); border-color: var(--so-accent); font-weight: 600;
12889
12987
  }
12890
- .login-shell button:hover { background: var(--so-accent-hover); border-color: var(--so-accent-hover); }
12988
+ @media (hover: hover) and (pointer: fine) { .login-shell button:hover { background: var(--so-accent-hover); border-color: var(--so-accent-hover); } }
12891
12989
  .login-foot { text-align: center; margin: 1.5rem 0 0; font-size: 0.75rem; color: var(--muted-foreground); line-height: 1.9; }
12892
12990
  .login-foot code { background: none; padding: 0; color: var(--muted-foreground); white-space: nowrap; }
12893
12991
  .so-wordmark { display: flex; align-items: center; gap: 9px; font-size: 13px; font-weight: 600; text-decoration: none; white-space: nowrap; letter-spacing: -.02em; color: var(--foreground); }
@@ -12899,7 +12997,7 @@ ${THEME_DARK}
12899
12997
  .sso-step-up[data-sso-step-up="confirmed"] { color: var(--so-success); }
12900
12998
  .sso-step-up[data-sso-step-up="confirmed"]::before { content: "✓"; margin-right: 6px; }
12901
12999
  .login-shell .login-sso { display: flex; justify-content: center; align-items: center; min-height: 44px; border-radius: 8px; background: var(--so-accent); color: var(--so-on-accent); font-weight: 600; text-decoration: none; margin: 0 0 1rem; }
12902
- .login-shell .login-sso:hover { background: var(--so-accent-hover); }
13000
+ @media (hover: hover) and (pointer: fine) { .login-shell .login-sso:hover { background: var(--so-accent-hover); } }
12903
13001
  .login-shell .login-password > summary { text-align: center; font-size: .875rem; color: var(--muted-foreground); cursor: pointer; margin: 0 0 .75rem; }
12904
13002
  .focus-page { max-width: 40rem; margin: 0 auto; padding: 2rem 1.25rem 3rem; }
12905
13003
  .focus-page .focus-brand { display: inline-flex; margin: 0 0 2.5rem; }
@@ -12952,7 +13050,7 @@ ${THEME_DARK}
12952
13050
  .lane > summary::-webkit-details-marker { display: none; }
12953
13051
  .lane h2 { display: flex; align-items: center; gap: .4rem; margin: 0; font-size: .6875rem; }
12954
13052
  .lane h2 a { color: inherit; text-decoration: none; }
12955
- .lane h2 a:hover { text-decoration: underline; }
13053
+ @media (hover: hover) and (pointer: fine) { .lane h2 a:hover { text-decoration: underline; } }
12956
13054
  .lane .hint { margin-top: 0; }
12957
13055
  .lane-count { color: var(--muted-foreground); font-weight: 400; font-variant-numeric: tabular-nums; }
12958
13056
  .lane h2::before {
@@ -12967,7 +13065,7 @@ ${THEME_DARK}
12967
13065
  background: var(--card); border: 1px solid var(--border);
12968
13066
  border-radius: calc(var(--radius) - 2px); padding: .5rem .625rem; margin-top: .5rem;
12969
13067
  }
12970
- .lane-card:hover { border-color: color-mix(in srgb, var(--border) 55%, var(--muted-foreground)); }
13068
+ @media (hover: hover) and (pointer: fine) { .lane-card:hover { border-color: color-mix(in srgb, var(--border) 55%, var(--muted-foreground)); } }
12971
13069
  .lane-card .id { display: block; font-family: var(--font-mono); font-size: .6875rem; color: var(--muted-foreground); margin-bottom: .125rem; overflow-wrap: anywhere; }
12972
13070
  .lane-card .t { display: block; font-size: .8125rem; font-weight: 500; line-height: 1.35; }
12973
13071
  .lane-card .dot { margin-right: .4rem; }
@@ -13165,7 +13263,7 @@ ${THEME_DARK}
13165
13263
  border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px);
13166
13264
  background: color-mix(in srgb, var(--glass-strong) 64%, transparent); text-decoration: none;
13167
13265
  }
13168
- .chat-overview-stat:hover { background: var(--glass-strong); }
13266
+ @media (hover: hover) and (pointer: fine) { .chat-overview-stat:hover { background: var(--glass-strong); } }
13169
13267
  .chat-overview-stat b { color: var(--foreground); font: 600 1.2rem/1.2 var(--font-mono); font-variant-numeric: tabular-nums; }
13170
13268
  .chat-overview-stat span { overflow: hidden; color: var(--muted-foreground); font-size: .65rem; text-overflow: ellipsis; white-space: nowrap; }
13171
13269
  .chat-overview-stat.attention b { color: var(--warning); }
@@ -13175,7 +13273,7 @@ ${THEME_DARK}
13175
13273
  display: grid; grid-template-columns: 1.75rem minmax(0, 1fr) auto; align-items: center; gap: .6rem;
13176
13274
  padding: .48rem .55rem; border-radius: calc(var(--radius) - 4px); color: inherit; text-decoration: none;
13177
13275
  }
13178
- .chat-overview-item:hover { background: color-mix(in srgb, var(--muted) 72%, transparent); }
13276
+ @media (hover: hover) and (pointer: fine) { .chat-overview-item:hover { background: color-mix(in srgb, var(--muted) 72%, transparent); } }
13179
13277
  .chat-overview-icon { display: grid; place-items: center; width: 1.75rem; height: 1.75rem; border-radius: .55rem; background: var(--muted); color: var(--muted-foreground); }
13180
13278
  .chat-overview-icon svg { width: .9rem; height: .9rem; }
13181
13279
  .chat-overview-item.decision .chat-overview-icon { color: var(--warning); background: var(--warning-soft); }
@@ -13202,10 +13300,10 @@ ${THEME_DARK}
13202
13300
  background: color-mix(in srgb, var(--muted) 58%, transparent);
13203
13301
  transition: transform .16s ease, background .16s, border-color .16s, box-shadow .16s;
13204
13302
  }
13205
- .chat-project-card:hover {
13303
+ @media (hover: hover) and (pointer: fine) { .chat-project-card:hover {
13206
13304
  border-color: var(--glass-border); background: color-mix(in srgb, var(--muted) 82%, transparent);
13207
13305
  box-shadow: 0 12px 28px -24px rgb(0 0 0 / .8);
13208
- }
13306
+ } }
13209
13307
  .chat-project-name { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: .45rem; }
13210
13308
  .chat-project-name strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .8125rem; }
13211
13309
  .chat-project-name .mono { color: var(--running); font-size: .6875rem; }
@@ -13277,7 +13375,7 @@ ${THEME_DARK}
13277
13375
  .proposal-actions form { margin: 0; }
13278
13376
  /* The confirm is the card's one primary; dismiss stays quiet (UI polish 2026-09-13). */
13279
13377
  .proposal-actions .acts form:first-child button[type=submit] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }
13280
- .proposal-actions .acts form:first-child button[type=submit]:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: color-mix(in srgb, var(--primary) 85%, var(--background)); }
13378
+ @media (hover: hover) and (pointer: fine) { .proposal-actions .acts form:first-child button[type=submit]:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: color-mix(in srgb, var(--primary) 85%, var(--background)); } }
13281
13379
  .proposal-actions .done, .proposal-actions .refused, .proposal-wait { margin: 0; padding: .55rem .65rem; border-radius: calc(var(--radius) - 5px); font-size: .72rem; }
13282
13380
  .proposal-actions .done { color: var(--success); background: var(--success-soft); }
13283
13381
  .proposal-actions .refused { color: var(--destructive); background: var(--destructive-soft); }
@@ -13358,7 +13456,8 @@ ${THEME_DARK}
13358
13456
  width: 100%; min-height: 3.5rem; max-height: 13rem; margin: 0; padding: .75rem .85rem;
13359
13457
  resize: vertical; border: 0; background: transparent; box-shadow: none; font-size: 1rem;
13360
13458
  }
13361
- .composer textarea:hover, .composer textarea:focus-visible { border: 0; box-shadow: none; }
13459
+ .composer textarea:focus-visible { border: 0; box-shadow: none; }
13460
+ @media (hover: hover) and (pointer: fine) { .composer textarea:hover { border: 0; box-shadow: none; } }
13362
13461
  .composer button {
13363
13462
  flex: 0 0 2.75rem; width: 2.75rem; min-height: 2.75rem; padding: 0; border-radius: 999px;
13364
13463
  font-size: 0; box-shadow: 0 10px 24px -16px rgb(255 255 255 / .65);
@@ -13536,7 +13635,7 @@ ${THEME_DARK}
13536
13635
  .mate-mint form { border-top: 1px solid var(--glass-border); padding-top: .5rem; }
13537
13636
  /* The start act is the page's one primary (UI polish 2026-09-13). */
13538
13637
  .mate-mint form > button[type=submit] { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }
13539
- .mate-mint form > button[type=submit]:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: color-mix(in srgb, var(--primary) 85%, var(--background)); }
13638
+ @media (hover: hover) and (pointer: fine) { .mate-mint form > button[type=submit]:hover { background: color-mix(in srgb, var(--primary) 85%, var(--background)); border-color: color-mix(in srgb, var(--primary) 85%, var(--background)); } }
13540
13639
  .mate-terms { display: flex; flex-wrap: wrap; gap: 1rem; align-items: baseline; padding: .35rem 0; }
13541
13640
  .mate-terms .inline-field { white-space: nowrap; }
13542
13641
  button.quiet { background: transparent; color: var(--fg-muted); border-color: var(--border); }
@@ -13587,7 +13686,7 @@ ${THEME_DARK}
13587
13686
  display: block; padding: .58rem .65rem; margin: .16rem 0; border: 1px solid transparent;
13588
13687
  border-radius: calc(var(--radius) - 4px); color: inherit; text-decoration: none;
13589
13688
  }
13590
- .wb-row:hover { background: var(--card); border-color: var(--border); }
13689
+ @media (hover: hover) and (pointer: fine) { .wb-row:hover { background: var(--card); border-color: var(--border); } }
13591
13690
  .wb-row.wb-selected { background: var(--muted); border-color: var(--border); }
13592
13691
  .wb-row .wb-title { display: block; font-size: .8125rem; font-weight: 500; line-height: 1.35; }
13593
13692
  .wb-row .wb-meta { display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; margin-top: .22rem; }
@@ -13649,7 +13748,7 @@ ${THEME_DARK}
13649
13748
  .icon-button svg { width: 1rem; height: 1rem; }
13650
13749
  .queue-card p { margin: 0; }
13651
13750
  .queue-card a { text-decoration: none; }
13652
- .queue-card a:hover { text-decoration: underline; }
13751
+ @media (hover: hover) and (pointer: fine) { .queue-card a:hover { text-decoration: underline; } }
13653
13752
  .queue-card .row + .row { margin-top: .125rem; border-bottom: none; }
13654
13753
  .queue-card p.row { padding: 0; border-bottom: none; }
13655
13754
  .tracks { margin-top: 1.5rem; }
@@ -13658,7 +13757,7 @@ ${THEME_DARK}
13658
13757
  .track-row p { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
13659
13758
  .track-row p .right { margin-left: auto; }
13660
13759
  .track-row a { text-decoration: none; }
13661
- .track-row a:hover { text-decoration: underline; }
13760
+ @media (hover: hover) and (pointer: fine) { .track-row a:hover { text-decoration: underline; } }
13662
13761
  .track-strip { display: inline-flex; gap: .3rem; align-items: center; vertical-align: middle; }
13663
13762
  .fire {
13664
13763
  display: inline-block; width: .65rem; height: .65rem; border-radius: 50%;
@@ -13777,7 +13876,7 @@ button { min-height: 44px; }
13777
13876
  padding: 0 .875rem; border: 1px solid var(--border); border-radius: 999px;
13778
13877
  text-decoration: none; color: var(--foreground); background: var(--card);
13779
13878
  }
13780
- .next-pager .skip:hover { border-color: color-mix(in srgb, var(--border) 60%, var(--muted-foreground)); }
13879
+ @media (hover: hover) and (pointer: fine) { .next-pager .skip:hover { border-color: color-mix(in srgb, var(--border) 60%, var(--muted-foreground)); } }
13781
13880
 
13782
13881
  /* Motion: only where a human caused the change — navigation, presses,
13783
13882
  overlays. Liveness swaps stay instant; the pulse dot is the one "alive"
@@ -13806,7 +13905,10 @@ button { min-height: 44px; }
13806
13905
  @media (prefers-reduced-motion: no-preference) {
13807
13906
  .tabbar a, .side nav a { transition: color .15s, background .15s; }
13808
13907
  button:active { transform: scale(.985); }
13809
- .palette, .kbd-help { animation: rise 120ms ease-out; }
13908
+ /* Anchor buttons press like buttons; nav links fill instead. The palette
13909
+ and shortcuts overlay are keyboard-summoned, so they appear at once. */
13910
+ .button-link, .side .new-task, .content .new-task, .result-feedback-link, .next-pager .skip { transition: transform 160ms var(--so-ease-out); }
13911
+ :is(.button-link, .side .new-task, .content .new-task, .result-feedback-link, .next-pager .skip):active { transform: scale(.985); }
13810
13912
  .chat-workspace.projects-open .chat-projects { animation: rise 200ms ease-out; }
13811
13913
  .chat-workspace.projects-open::before { animation: fade 180ms ease-out; }
13812
13914
  .switcher[open] .switcher-menu { animation: rise 160ms ease-out; }
@@ -13823,7 +13925,8 @@ button { min-height: 44px; }
13823
13925
  @media (prefers-reduced-motion: reduce) {
13824
13926
  *, *::before, *::after { animation: none !important; transition: none !important; }
13825
13927
  ::view-transition-group(*), ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
13826
- button:active, button:hover, .side nav a:hover, .chat-project-card:hover, .lane-card:hover, .decide-card:hover, .menu-row:hover { transform: none; }
13928
+ button:active, :is(.button-link, .side .new-task, .content .new-task, .result-feedback-link, .next-pager .skip):active { transform: none; }
13929
+ @media (hover: hover) and (pointer: fine) { button:hover, .side nav a:hover, .chat-project-card:hover, .lane-card:hover, .decide-card:hover, .menu-row:hover { transform: none; } }
13827
13930
  }
13828
13931
 
13829
13932
  /* The shortcuts overlay: display-only, toggled by the chrome layer, absent
@@ -13878,7 +13981,9 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
13878
13981
  }
13879
13982
  `;
13880
13983
  /** Appearance: a three-way segmented switch, one tap per choice. */
13881
- const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}.result-action .result-feedback-link:hover{background:var(--so-raised)}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem;margin:0 0 2rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}.settings-tiles svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}.theme-switch .theme-choice:hover{color:var(--so-ink)}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}`;
13984
+ const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem;margin:0 0 2rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
13985
+ /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
13986
+ export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
13882
13987
  const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + LIMITS_CSS + MONITORING_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + LEAD_CONTEXT_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
13883
13988
  /**
13884
13989
  * The board's liveness: fetch this page's own fragment on a timer and swap
@@ -14126,8 +14231,8 @@ export const SENSITIVE_INPUT = /<input\b[^>]*[\s"']type\s*=\s*["']?password/i;
14126
14231
  /** The design contract every page carries (impeccable direction, 2026-09-27). */
14127
14232
  const DESIGN_CONTRACT = `<!-- THESIS: a control plane that stays quiet until a person is needed; it refuses the dashboard default of coloured status everywhere and an accent on every button.
14128
14233
  OWN-WORLD: a neutral grey frame with paper sheets inset into it (Arc, Linear), Geist for words and Geist Mono for machine facts, ink for every act a person can take, one chart magenta only for what waits on a person plus focus and selection; hairlines and one soft sheet shadow, no glass, no gradients.
14129
- STORY: glance, see the magenta count, open the one thing that needs you, act with the one magenta verb, leave.
14130
- FIRST VIEWPORT: sidebar on the frame (magenta mark, ink New task, the current page as a raised pill, the magenta needs-you count); the main sheet with a 52px header over compact 13px rows; the Crew sheet beside it.
14234
+ STORY: glance, see the accent count, open the one thing that needs you, act with the one accent verb, leave.
14235
+ FIRST VIEWPORT: sidebar on the frame (accent mark, ink New task, the current page as a raised pill, the accent needs-you count); the main sheet with a 52px header over compact 13px rows; the Crew sheet beside it.
14131
14236
  FORM: the Raycast and Arc canon, user-pinned; seed 9c849086.
14132
14237
  FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md -->`;
14133
14238
  /** The shortcuts overlay: display-only, toggled by the chrome layer,
@@ -14284,7 +14389,13 @@ function shell(title, body, options = {}) {
14284
14389
  : "") +
14285
14390
  (chrome.modeBanner === undefined
14286
14391
  ? ""
14287
- : `<div class="banner"><span class="badge badge-running">mode</span>${escape(chrome.modeBanner.words)} \u00b7 <a href="/mode">the terms \u00b7 end it</a></div>`);
14392
+ : `<div class="banner"><span class="badge badge-running">mode</span>${escape(chrome.modeBanner.words)} \u00b7 <a href="/mode">the terms \u00b7 end it</a></div>`) +
14393
+ (chrome.signIn ?? []).map(one => `<div class="banner sign-in-banner" data-sign-in="${escape(one.provider)}"><strong>${escape(one.title)}</strong> \u00b7 run <code>${escape(one.command)}</code> on this computer, then resume.${one.detail === "" ? "" : ` ${escape(one.detail)}`}` +
14394
+ `<form method="post" action="${escape(one.resumeHref)}" class="inline"><input type="hidden" name="csrf" value="${escape(chrome.csrf ?? "")}"><button type="submit">${escape(one.resumeLabel)}</button></form></div>`).join("") +
14395
+ (chrome.update === undefined
14396
+ ? ""
14397
+ : `<div class="banner update-banner" data-update="${escape(chrome.update.version)}">${escape(updateNoticeWords(chrome.update))} \u00b7 <a href="${escape(chrome.update.href)}">What's new</a>` +
14398
+ `<form method="post" action="${escape(chrome.update.dismissHref)}" class="inline"><input type="hidden" name="csrf" value="${escape(chrome.csrf ?? "")}"><input type="hidden" name="version" value="${escape(chrome.update.version)}"><button type="submit">Dismiss</button></form></div>`);
14288
14399
  // The scope bar sits between the banners and the main/split body, so it
14289
14400
  // can never disappear with a responsive pane (portfolio arc §1).
14290
14401
  const content = chrome.listPane === undefined
@@ -19744,6 +19855,7 @@ const REASON_WORDS = {
19744
19855
  agent: "the agent failed",
19745
19856
  "agent-reported": "the agent reported it could not finish",
19746
19857
  "no-op": "nothing changed when something should have",
19858
+ "no-handoff": "the agent stopped before handing off; its work was kept and it is being resumed",
19747
19859
  "moved-head": "the branch moved underneath the build",
19748
19860
  "moved-branch": "the branch moved underneath the build",
19749
19861
  timeout: "ran out of time",
@@ -21301,7 +21413,7 @@ function telegramDeliveryWords(store, bot) {
21301
21413
  return `Telegram pushes new messages to ${state.url}${state.problem === null ? "." : `, but ${state.problem.charAt(0).toLowerCase()}${state.problem.slice(1)}.`}`;
21302
21414
  return `Toolroll asks Telegram for new messages every few seconds${state?.problem ? ` (${state.problem})` : ""}.`;
21303
21415
  }
21304
- function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null) {
21416
+ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null) {
21305
21417
  const permissionCard = permissionDefault === null
21306
21418
  ? ""
21307
21419
  : [
@@ -21497,6 +21609,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
21497
21609
  telegram: { state: hasEnv ? "from the environment" : existing === null ? "not set" : "saved", current, delivery: telegramDelivery },
21498
21610
  email,
21499
21611
  workers,
21612
+ updates,
21500
21613
  };
21501
21614
  return screen("Settings", [
21502
21615
  "<h1>Settings</h1>",
@@ -21506,6 +21619,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
21506
21619
  qualityCard,
21507
21620
  keysCard,
21508
21621
  workersCard(workers),
21622
+ updatesCard(updates, csrf),
21509
21623
  pushCard === "" && messagingCard === "" && digestCard === "" ? "" : `<h2>Notifications</h2>`,
21510
21624
  messagingCard,
21511
21625
  pushCard,
@@ -21560,6 +21674,23 @@ function workersCard(workers) {
21560
21674
  `<p class="meta">To change how many a worker runs at once: <code>toolroll runner capacity &lt;name&gt; &lt;n&gt;</code></p>`) +
21561
21675
  `</section>`;
21562
21676
  }
21677
+ /** Settings → Updates, for browsers without the app script. */
21678
+ function updatesCard(updates, csrf) {
21679
+ if (updates === null)
21680
+ return "";
21681
+ const latest = updates.latest;
21682
+ return `<section id="updates" aria-labelledby="updates-title"><h2 id="updates-title">Updates</h2>` +
21683
+ `<p class="row">This version <span class="mono">${escape(updates.current)}</span> \u00b7 ${!updates.check.on ? "Update checks are off" : latest === null ? "Not checked yet" : latest.newer ? `Latest <span class="mono">${escape(latest.version)}</span>` : "Up to date"}</p>` +
21684
+ (latest !== null && latest.newer
21685
+ ? `<p>Update with <code>${escape(updates.updateCommand)}</code> \u00b7 <a href="${escape(latest.url)}">Release notes</a></p>` +
21686
+ (latest.notes === "" ? "" : `<details class="settings-more"><summary>What's new in ${escape(latest.version)}</summary><pre class="update-notes">${escape(latest.notes)}</pre></details>`)
21687
+ : "") +
21688
+ (updates.check.canManage && !updates.check.byEnv
21689
+ ? `<form method="post" action="/settings/updates" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="check" value="${updates.check.on ? "off" : "on"}"><button type="submit">${updates.check.on ? "Turn off daily check" : "Turn on daily check"}</button></form>`
21690
+ : updates.check.byEnv ? `<p class="meta">Off by TOOLROLL_NO_UPDATE_CHECK.</p>` : "") +
21691
+ (updates.workers.length === 0 ? "" : `<ul>${updates.workers.map(one => `<li data-worker-version="${escape(one.name)}">${escape(one.name)} <span class="mono">${escape(one.version ?? "unknown")}</span>${one.older ? " \u00b7 older" : ""}</li>`).join("")}</ul>`) +
21692
+ `</section>`;
21693
+ }
21563
21694
  /** Choices save the moment they change; without the script the Save
21564
21695
  * button stays and the form works the same. */
21565
21696
  const SETTINGS_AUTOSAVE_SCRIPT = `(function(){document.querySelectorAll('form[data-autosave]').forEach(function(form){form.classList.add('js-autosave');form.addEventListener('change',function(ev){var t=ev.target;if(t&&(t.type==='radio'||t.tagName==='SELECT')){if(form.requestSubmit)form.requestSubmit();else form.submit();}});});})();`;