@leftoff/cli 0.9.17-beta.5 → 0.9.17-beta.7

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leftoff",
3
- "version": "0.9.17-beta.5",
3
+ "version": "0.9.17-beta.7",
4
4
  "description": "Leftoff (formerly Checkpoints): WTF was I working on? Tracks every Claude Code session on this machine, writes a 3-line checkpoint when one goes quiet, and adds /cp (checkpoint now), /cp-name and a /leftoff pane. Talks to the local Leftoff daemon on 127.0.0.1:7429 and falls back to writing markdown in ~/checkpoints.",
5
5
  "author": {
6
6
  "name": "One Silly App"
package/mod/hooks/core.js CHANGED
@@ -683,7 +683,7 @@ const SESSION_ORDER = ['id', 'title', 'title_source', 'machine', 'cwd', 'repo',
683
683
  export const SESSION_EXTRA = ['inTurn', 'needsInput', 'backgroundTasks', 'turnsSinceCheckpoint', 'dormantStretch',
684
684
  'finalCheckpointDone', 'firstPrompt', 'lastPreview', 'lastNotification', 'lastEventAt', 'statusChanges', 'folder',
685
685
  'pid', 'tty', 'pidStartedAt', 'pidSource', 'term', 'closedAt', 'answers', 'lastNotificationMessage', 'needsInputAt', 'question', 'questionAt',
686
- 'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files', 'startRepo', 'editedRepos', 'parked', 'nonInteractive', 'transcriptCwd', 'doubtedCheckpoints', 'liveLink', 'transcriptTurns', 'wasLive', 'headless', 'startedFrom', 'startProject', 'backgroundList', 'doneAt', 'recap', 'reviewed', 'wtLook']
686
+ 'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files', 'startRepo', 'editedRepos', 'parked', 'nonInteractive', 'transcriptCwd', 'doubtedCheckpoints', 'liveLink', 'transcriptTurns', 'wasLive', 'headless', 'startedFrom', 'startProject', 'backgroundList', 'doneAt', 'recap', 'reviewed', 'wtLook', 'prShort']
687
687
 
688
688
  export const SESSION_FIELDS = ['id', 'title', 'titleSource', 'machine', 'cwd', 'repo', 'transcriptPath', 'model', 'surface', 'capture',
689
689
  'status', 'startedAt', 'lastActivityAt', 'lastHeartbeatAt', 'endedAt', 'endReason', 'turns', 'lastCheckpointId',
@@ -1622,8 +1622,13 @@ export const NOTHING_TO_CHECKPOINT = 'Nothing to checkpoint yet — no messages
1622
1622
  // clip). The board and the daemon use ui/cp-text.js stripChannel; this copy is
1623
1623
  // here because the mod ships on its own. A test holds the two to the same answers.
1624
1624
  const CH_PAIR = /<channel\b[^>]*>([\s\S]*?)<\/channel\s*>/g
1625
+ // 2026-10-29: Claude Code's wrapper for a prompt our plugin submits between turns
1626
+ // ("The leftoff plugin sent a message:\n<words>\n\nThis is how Claude Code surfaces a prompt …").
1627
+ const PLUGIN_WRAP = /^\s*The leftoff plugin sent a message:\s*\n([\s\S]*?)(?:\n\s*\nThis is how Claude Code surfaces a prompt[\s\S]*)?$/i
1625
1628
  export function stripChannel(s) {
1626
1629
  let t = String(s == null ? '' : s)
1630
+ const pw = PLUGIN_WRAP.exec(t)
1631
+ if (pw) t = pw[1]
1627
1632
  if (!/<\/?channel\b/i.test(t)) return t.trim()
1628
1633
  for (let i = 0; i < 8 && CH_PAIR.test(t); i++) { CH_PAIR.lastIndex = 0; t = t.replace(CH_PAIR, '$1') }
1629
1634
  CH_PAIR.lastIndex = 0
@@ -28,7 +28,7 @@ const END_TIMEOUT_MS = 1000
28
28
  const MODEL_TIMEOUT_MS = 20000
29
29
 
30
30
  // Module state. $.state is not used: the pane redraws with $.ui.invalidate.
31
- const MOD_VERSION = '0.9.17-beta.5' // keep in step with .claude-plugin/plugin.json
31
+ const MOD_VERSION = '0.9.17-beta.7' // keep in step with .claude-plugin/plugin.json
32
32
 
33
33
  const st = {
34
34
  internal: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftoff/cli",
3
- "version": "0.9.17-beta.5",
3
+ "version": "0.9.17-beta.7",
4
4
  "description": "Leftoff: WTF was I working on? Tracks every Claude Code session, writes a 3-line checkpoint when one goes quiet, and gives you a daily digest and a board, on your Mac and (optionally) your phone.",
5
5
  "keywords": [
6
6
  "claude",
package/ui/app.css CHANGED
@@ -2862,12 +2862,21 @@ mark.tx, .co-snip mark, .tx-snip mark, .tx-msg-text mark { background: var(--tx-
2862
2862
  .tab-all[hidden] { display: none; }
2863
2863
  }
2864
2864
  @media (min-width: 700px) and (pointer: coarse) { .tab-keep { opacity: 1; } }
2865
- /* the rail as icons (700–1099px): the labels stay for screen readers */
2865
+ /* the rail as icons: collapsed by you or by a short room (html.nav-slim, ui/navrail.js); before the
2866
+ script runs, 700–1099px starts that way. The labels stay for screen readers. */
2866
2867
  @media (min-width: 700px) and (max-width: 1099.98px) {
2867
- :root { --rail-w: 64px; }
2868
- .tabs a .lbl, .topright .lbl, .topright .sb-lbl, .topright .health .label { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
2869
- .tabs a, .topright .searchbtn, .topright .iconbtn, .topright .health { min-height: 48px; padding: 6px 2px; }
2870
- .navcount { left: calc(50% + 2px); }
2868
+ html:not(.nav-ready) { --rail-w: 56px; }
2869
+ html:not(.nav-ready) :is(.tabs a .lbl, .topright .lbl, .topright .sb-lbl, .topright .health .label) { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
2870
+ }
2871
+ @media (min-width: 700px) {
2872
+ html.nav-slim { --rail-w: 56px; }
2873
+ html.nav-slim :is(.tabs a .lbl, .topright .lbl, .topright .sb-lbl, .topright .health .label) { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
2874
+ html.nav-slim :is(.tabs a, .topright .searchbtn, .topright .iconbtn, .topright .health) { min-height: 44px; padding: 6px 2px; }
2875
+ html.nav-slim .navcount { left: calc(50% + 2px); top: 1px; }
2876
+ html.nav-slim .brand .logo { width: 26px; height: 26px; }
2877
+ }
2878
+ @media (min-width: 700px) and (hover: hover) and (pointer: fine) { /* 44px is for fingers */
2879
+ html.nav-slim :is(.tabs a, .topright .searchbtn, .topright .iconbtn, .topright .health) { min-height: 34px; padding: 4px 2px; }
2871
2880
  }
2872
2881
  /* phones (under 700px): the Open row on the board, ‹ N of M › on a session page */
2873
2882
  @media (max-width: 699.98px) {
@@ -3980,13 +3989,11 @@ a.fact.fact-link[data-tip]::after { content: attr(data-tip); display: none; posi
3980
3989
 
3981
3990
  /* ------------------------------------------------------------ 0.9.14 full screen keeps the open tabs
3982
3991
  From 700px: the board's own tab bar stays pinned at the top, full screen fills the
3983
- space under it, and the left rail steps aside until you leave full screen.
3992
+ space under it and beside the left rail (0.9.18: the rail no longer steps aside).
3984
3993
  Under 700px (no tab bar there): one row of compact chips under the bar, 44px tall,
3985
3994
  never two rows: it scrolls sideways. */
3986
3995
  @media (min-width: 700px) {
3987
- html.live-full body { padding-left: 0; }
3988
- html.live-full .top { display: none !important; }
3989
- html.live-full .tabstrip { position: fixed; top: 0; left: 0; right: 0; z-index: 33; }
3996
+ html.live-full .tabstrip { position: fixed; top: 0; left: calc(var(--rail-w) + env(safe-area-inset-left)); right: 0; z-index: 33; } /* 0.9.18: the rail stays beside full screen */
3990
3997
  html.live-full .live-box.full { top: calc(var(--lf-top, 0px) + var(--lf-bar, 44px)); height: calc(var(--lf-h, 100vh) - var(--lf-bar, 44px)); height: calc(var(--lf-h, 100dvh) - var(--lf-bar, 44px)); }
3991
3998
  .lf-tabs { display: none !important; }
3992
3999
  }
@@ -4377,12 +4384,59 @@ html.lo-reordering, html.lo-reordering * { cursor: grabbing !important; user-sel
4377
4384
  html:not(.live-full) .fs-side { display: none; }
4378
4385
  .fs-side-in { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 10px 16px 16px; container: fsside / inline-size; }
4379
4386
  .fs-side-in > * { max-width: 100%; }
4380
- .fs-head { display: block; margin: 0 0 4px; padding: 0; }
4381
- .fs-head-row { display: flex; align-items: flex-start; gap: 2px; }
4382
- .fs-head .s-title { flex: 1 1 auto; min-width: 0; margin: 0; }
4383
- .fs-head h1 { font-size: 17px; line-height: 1.3; margin: 11px 0 4px; overflow-wrap: anywhere; }
4384
- .fs-head .sub { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 2px 0 6px; }
4385
- .fs-head .more-wrap { flex: none; }
4387
+ /* 2026-10-28 the panel's head: one row (› hide, the title, then ✎ and ⋯ in one cluster, all one size,
4388
+ centred on the title's FIRST line), the chips on one baseline under it, then the folder line.
4389
+ --fsh-b: the buttons' size (32px with a mouse, 44px on touch); --fsh-lh: one title line. */
4390
+ .fs-head { display: block; margin: 0 0 4px; padding: 0; --fsh-b: 32px; --fsh-lh: 22px; }
4391
+ @media (pointer: coarse) { .fs-head { --fsh-b: 44px; } }
4392
+ .fs-head-row { display: flex; align-items: flex-start; gap: 4px; margin: 2px 0 6px; min-width: 0; }
4393
+ .fs-head .s-title { flex: 1 1 auto; min-width: 0; margin: 0; display: block; }
4394
+ .fs-head h1 { position: relative; font-size: 17px; line-height: var(--fsh-lh); margin: calc((var(--fsh-b) - var(--fsh-lh)) / 2) 0 0; overflow-wrap: anywhere; }
4395
+ .fs-head .s-title-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; text-overflow: ellipsis; }
4396
+ .fs-head .s-title input { margin: calc((var(--fsh-b) - 34px) / 2) 0 0; font-size: 17px; width: 100%; box-sizing: border-box; }
4397
+ .fs-hacts { flex: none; display: flex; align-items: center; gap: 2px; height: var(--fsh-b); }
4398
+ .fs-hacts .more-wrap { flex: none; display: inline-flex; }
4399
+ .fs-head .fs-side-x, .fs-hacts .fs-hact, .fs-hacts .btn.icon-btn.more { flex: none; position: relative; display: inline-flex; align-items: center; justify-content: center; width: var(--fsh-b); height: var(--fsh-b); margin: 0; padding: 0; border: 0; border-radius: 8px; background: none; box-shadow: none; color: var(--text2); cursor: pointer; }
4400
+ .fs-head .fs-side-x .ic, .fs-hacts .fs-hact svg, .fs-hacts .btn.icon-btn.more .ic { width: 16px; height: 16px; margin: 0; }
4401
+ .fs-head .fs-side-x { margin-left: -6px; }
4402
+ @media (hover: hover) { .fs-head .fs-side-x:hover, .fs-hacts .fs-hact:hover, .fs-hacts .btn.icon-btn.more:hover { background: var(--card2); color: var(--text); border-color: transparent; } }
4403
+ .fs-hacts .btn.icon-btn.more[aria-expanded="true"] { background: var(--card2); color: var(--text); }
4404
+ .fs-hacts .fs-hact:focus-visible, .fs-hacts .btn.icon-btn.more:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
4405
+ .fs-hacts .retitle.is-busy { color: var(--accent); cursor: progress; }
4406
+ /* their tips hang below, right-aligned (the panel's edge is near) */
4407
+ .fs-side .fs-hacts [data-tip]::after { right: 0; left: auto; top: calc(100% + 4px); transform: none; }
4408
+ @media (hover: hover) and (pointer: fine) { .fs-hacts .fs-hact[data-tip]:hover::after, .fs-hacts .btn.more[data-tip]:hover::after { display: block; animation: tip-in .12s ease-out .35s both; } }
4409
+ .fs-hacts .fs-hact[data-tip]:focus-visible::after, .fs-hacts .btn.more[data-tip]:focus-visible::after { display: block; }
4410
+ .fs-hacts .btn.more[aria-expanded="true"]::after { display: none !important; }
4411
+ /* the session's PRs: none found says why in one quiet line; Link a PR… focuses the box under it */
4412
+ .sprs .pr-none { position: relative; margin: 0 0 8px; font-size: 13px; line-height: 1.4; color: var(--muted); }
4413
+ .sprs .pr-none-link { cursor: pointer; color: var(--accent); }
4414
+ @media (hover: hover) { .sprs .pr-none-link:hover { text-decoration: underline; } }
4415
+ .fs-side .sprs .pr-none[data-tip]::after { left: 0; right: auto; top: calc(100% + 4px); transform: none; white-space: normal; max-width: 100%; }
4416
+ @media (hover: hover) and (pointer: fine) { .sprs .pr-none[data-tip]:hover::after { display: block; animation: tip-in .12s ease-out .6s both; } }
4417
+ /* 2026-10-29 state by shape plus the word; a ref that couldn't be checked is a quiet dashed chip, why in its tip */
4418
+ .pr-state { display: inline-flex; align-items: center; gap: 3px; line-height: 1; }
4419
+ .pr-st-ic { flex: none; width: 13px; height: 13px; position: relative; top: .5px; }
4420
+ .sprs .pr-unlist { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; }
4421
+ .sprs .pr-unchecked { position: relative; display: inline-flex; align-items: center; gap: 5px; height: 26px; line-height: 1; padding: 0 9px; border: 1px dashed var(--line-strong); border-radius: 999px; font-size: 12.5px; color: var(--muted); background: transparent; max-width: 100%; min-width: 0; cursor: default; }
4422
+ .sprs .pr-unchecked .pr-un-ref { font-family: var(--mono); font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; position: relative; top: .5px; }
4423
+ .sprs .pr-unchecked .pr-un-word { flex: none; position: relative; top: .5px; }
4424
+ .sprs .pr-unchecked .pr-st-ic { color: var(--muted); }
4425
+ .sprs .pr-unchecked:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
4426
+ @media (pointer: coarse) { .sprs .pr-unchecked { height: 32px; } .sprs .pr-unlist { gap: 8px; } }
4427
+ .fs-side .sprs .pr-unchecked[data-tip]::after, .sprs .pr-unchecked[data-tip]::after { left: 0; right: auto; top: calc(100% + 4px); transform: none; white-space: normal; width: 260px; max-width: 70vw; font-family: var(--sans, inherit); font-weight: 400; line-height: 1.35; z-index: 30; }
4428
+ @media (hover: hover) and (pointer: fine) { .sprs .pr-unchecked[data-tip]:hover::after { display: block; animation: tip-in .12s ease-out .35s both; } }
4429
+ .sprs .pr-unchecked[data-tip]:focus-visible::after { display: block; }
4430
+ /* the whole title, when two lines cut it short */
4431
+ .fs-side .fs-head h1[data-tip]::after { left: 0; right: auto; top: calc(100% + 4px); transform: none; white-space: normal; width: max-content; max-width: 100%; font-size: 12px; line-height: 1.35; }
4432
+ @media (hover: hover) and (pointer: fine) { .fs-head h1[data-tip]:hover::after { display: block; animation: tip-in .12s ease-out .6s both; } }
4433
+ .fs-head .sub { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 6px; }
4434
+ .fs-head .sub > .pill, .fs-head .sub > .chip, .fs-head .sub > .s-tsrc, .fs-head .sub > .head-sg { display: inline-flex; align-items: center; box-sizing: border-box; height: 22px; line-height: 1; margin: 0; padding-top: 1px; padding-bottom: 0; vertical-align: middle; }
4435
+ .fs-head .sub > .s-tsrc { margin-left: 2px; font-size: 12px; color: var(--muted); }
4436
+ .fs-repo { position: relative; }
4437
+ .fs-side .fs-repo[data-tip]::after { left: 0; right: auto; top: calc(100% + 4px); transform: none; white-space: normal; max-width: 100%; overflow-wrap: anywhere; }
4438
+ @media (hover: hover) and (pointer: fine) { .fs-repo[data-tip]:hover::after { display: block; animation: tip-in .12s ease-out .5s both; } }
4439
+ @media (prefers-reduced-motion: reduce) { .fs-hacts [data-tip]::after, .fs-head h1[data-tip]::after, .fs-repo[data-tip]::after { animation: none !important; } }
4386
4440
  .fs-where { display: flex; align-items: center; gap: 6px; margin: 2px 0; font-size: 13px; color: var(--text2); min-width: 0; }
4387
4441
  .fs-where span:not(.faint) { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
4388
4442
  .fs-where .ic { flex: none; width: 14px; height: 14px; opacity: .7; }
@@ -4935,3 +4989,116 @@ details[open] > summary .wt-g-chev { transform: rotate(90deg); }
4935
4989
  /* narrow / a phone: arrows and dots only */
4936
4990
  @media (max-width: 699.98px) { .wt-dnow, .wt-dcount, .wt-dl { display: none; } .wt-dnav { gap: 4px; padding: 4px 6px calc(4px + env(safe-area-inset-bottom)); } .wt-dnav .btn { width: 44px; padding: 0; justify-content: center; } }
4937
4991
  @container (max-width: 560px) { .wt-dnow { display: none; } }
4992
+
4993
+ /* ------------------------------------------------------------ 0.9.18 the left nav on every screen (ui/navrail.js)
4994
+ From 700px the rail stays beside every page, full screen and the walkthrough too: expanded
4995
+ (84px, icon over label) or collapsed (56px, icons with a tip), its toggle at the foot or [.
4996
+ The current item: a filled pill and a bar at the rail's edge (shape, not color alone).
4997
+ Under 700px: a menu button in full screen's bar (and an edge swipe) opens it as a drawer. */
4998
+ .navtoggle, .lf-menu { display: none; }
4999
+ .railtip { position: fixed; z-index: 60; left: 0; top: 0; transform: translateY(-50%); width: max-content; max-width: 260px; padding: 4px 8px; border-radius: 7px; background: var(--card2); border: 1px solid var(--line-strong); box-shadow: var(--toast-shadow); color: var(--text2); font-size: 12px; line-height: 1.3; font-weight: 500; white-space: nowrap; pointer-events: none; animation: railtip-in .12s ease-out both; }
5000
+ @keyframes railtip-in { from { opacity: 0; } to { opacity: 1; } }
5001
+ .railtip[hidden] { display: none; }
5002
+ @media (min-width: 700px) {
5003
+ html.live-full .top { z-index: 34; }
5004
+ html.live-full .live-box.full { left: calc(var(--rail-w) + env(safe-area-inset-left)); }
5005
+ .term-dock { left: calc(var(--rail-w) + env(safe-area-inset-left)); } /* the terminal docks beside the rail, its toggle stays reachable */
5006
+ .top [data-tip]::after { display: none !important; } /* the rail scrolls and would cut a CSS tip off: one floating .railtip instead */
5007
+ .tabs a.on::before, .topright .iconbtn.on::before { content: ''; position: absolute; left: -8px; top: 50%; width: 3px; height: 22px; transform: translateY(-50%); border-radius: 0 3px 3px 0; background: var(--accent); }
5008
+ .topright .navtoggle { order: 10; display: flex; color: var(--faint); }
5009
+ .topright .navtoggle:hover { color: var(--text); }
5010
+ .topright .navtoggle[aria-disabled="true"] { opacity: .55; cursor: default; }
5011
+ .topright .navtoggle[aria-disabled="true"]:hover { background: none; color: var(--faint); }
5012
+ .top :is(a, button):focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
5013
+ html.nav-anim .top { transition: width .18s ease; }
5014
+ html.nav-anim body { transition: padding-left .18s ease; }
5015
+ html.nav-anim .live-box.full, html.nav-anim .tabstrip, html.nav-anim .term-dock { transition: left .18s ease; }
5016
+ .navdrawer, .navscrim { display: none !important; }
5017
+ }
5018
+ @media (prefers-reduced-motion: reduce) {
5019
+ .railtip { animation: none; }
5020
+ html.nav-anim .top, html.nav-anim body, html.nav-anim .live-box.full, html.nav-anim .tabstrip, html.nav-anim .term-dock { transition: none; }
5021
+ }
5022
+ @media (max-width: 699.98px) {
5023
+ .live-fbar .lf-menu { display: inline-flex; align-items: center; justify-content: center; flex: none; position: relative; width: 44px; height: 44px; min-height: 44px; padding: 0; border-color: transparent; background: none; box-shadow: none; color: var(--muted); }
5024
+ .live-fbar .lf-menu .ic { width: 20px; height: 20px; margin: 0; }
5025
+ .live-fbar .lf-menu:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
5026
+ .live-fbar .lf-menu + .live-fs-x { margin-left: -6px; }
5027
+ .navtoggle { display: none !important; }
5028
+ }
5029
+ .navscrim { position: fixed; inset: 0; z-index: 70; background: rgba(8, 10, 16, .42); opacity: 0; transition: opacity .2s ease; -webkit-tap-highlight-color: transparent; }
5030
+ .navscrim.open { opacity: 1; }
5031
+ .navscrim[hidden], .navdrawer[hidden] { display: none; }
5032
+ .navdrawer { position: fixed; z-index: 71; top: 0; bottom: 0; left: 0; width: min(300px, 84vw); display: flex; flex-direction: column; box-sizing: border-box; padding: max(10px, env(safe-area-inset-top)) 10px max(12px, env(safe-area-inset-bottom)) max(10px, env(safe-area-inset-left)); background: var(--bg); border-right: 1px solid var(--line); box-shadow: 0 0 40px rgba(0, 0, 0, .25); overflow-y: auto; overscroll-behavior: contain; transform: translateX(-100%); transition: transform .2s ease; outline: none; }
5033
+ .navdrawer.open { transform: none; }
5034
+ @media (prefers-reduced-motion: reduce) { .navdrawer, .navscrim { transition: none; } }
5035
+ html.nav-drawer-open, html.nav-drawer-open body { overflow: hidden; }
5036
+ .nd-head { display: flex; align-items: center; gap: 10px; min-height: 44px; margin: 0 0 6px; padding: 0 0 6px 8px; border-bottom: 1px solid var(--line); }
5037
+ .nd-head .logo { width: 22px; height: 22px; }
5038
+ .nd-title { flex: 1 1 auto; font-size: 15px; font-weight: 650; color: var(--text); }
5039
+ .nd-x { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 10px; background: none; color: var(--muted); cursor: pointer; }
5040
+ .nd-x:hover { color: var(--text); background: var(--bg2); }
5041
+ .nd-x .ic { width: 18px; height: 18px; }
5042
+ .nd-list { display: flex; flex-direction: column; gap: 2px; }
5043
+ .nd-item { position: relative; display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 0 12px; border-radius: 10px; color: var(--text2); font-size: 15px; font-weight: 550; text-decoration: none; -webkit-tap-highlight-color: transparent; }
5044
+ .nd-item .ic { flex: none; width: 20px; height: 20px; }
5045
+ .nd-item:hover { background: var(--bg2); color: var(--text); }
5046
+ .nd-item.on { background: var(--rail-on); color: var(--text); }
5047
+ .nd-item.on .ic { color: var(--accent); }
5048
+ .nd-item.on::before { content: ''; position: absolute; left: 0; top: 50%; width: 3px; height: 22px; transform: translateY(-50%); border-radius: 0 3px 3px 0; background: var(--accent); }
5049
+ .nd-lbl { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
5050
+ .nd-cur { flex: none; font-size: 12px; font-weight: 500; color: var(--muted); }
5051
+ .nd-n { flex: none; display: inline-flex; align-items: center; justify-content: center; height: 20px; min-width: 20px; padding: 0 6px; box-sizing: border-box; border-radius: 10px; background: var(--red); color: #fff; font-size: 11.5px; font-weight: 650; line-height: 1; }
5052
+ .nd-item:focus-visible, .nd-x:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
5053
+
5054
+ /* 2026-10-29 Needs you on a phone (beta feedback: "statically positioned so it's hard to read the message
5055
+ before it"). In full screen it sits between the thread and the composer: at most 45% of the height you can
5056
+ see (the visual viewport, so the keyboard counts; 40vh measured the whole layout height on iOS), folds to a
5057
+ one-line bar, and on a phone it leaves the question and Claude's last lines to the thread right above it. */
5058
+ .live-box.full .lf-needs { max-height: min(40vh, calc(var(--lf-h, 100dvh) * .45)); overscroll-behavior: contain; }
5059
+ .needs-strip .needs-fold, .needs-strip .needs-peek { display: none; }
5060
+ .lf-needs .needs-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
5061
+ .lf-needs .needs-head h2 { flex: none; }
5062
+ .lf-needs .needs-strip .needs-fold { display: inline-flex; align-items: center; justify-content: center; flex: none; margin-left: auto; width: 44px; height: 44px; margin-top: -10px; margin-bottom: -10px; margin-right: -8px; padding: 0; border: 0; border-radius: 10px; background: transparent; color: var(--text2); cursor: pointer; }
5063
+ .lf-needs .needs-fold .ic { width: 18px; height: 18px; }
5064
+ @media (hover: hover) and (pointer: fine) { .lf-needs .needs-strip .needs-fold { width: 32px; height: 32px; margin-top: -6px; margin-bottom: -6px; margin-right: -4px; } .lf-needs .needs-fold:hover { background: var(--subtle); color: var(--text); } }
5065
+ .lf-needs .needs-fold:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
5066
+ .lf-needs .needs-strip.folded .needs-fold .ic { transform: rotate(180deg); }
5067
+ .lf-needs .needs-strip.folded .needs-list { display: none; }
5068
+ .lf-needs .needs-strip.folded .needs-peek { display: block; flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text2); font-size: 13.5px; }
5069
+ .lf-needs .needs-strip.folded { padding-top: 8px; padding-bottom: 8px; }
5070
+ @media (max-width: 600px) {
5071
+ .live-box.full .lf-needs .needs-strip { margin: 6px 0 0; padding: 8px 10px 10px; }
5072
+ .live-box.full .lf-needs .needs-strip.folded { padding-top: 6px; padding-bottom: 6px; }
5073
+ .live-box.full .lf-needs .need-question, .live-box.full .lf-needs .need-ctx, .live-box.full .lf-needs .need-head { display: none; } /* the thread right above shows them */
5074
+ .live-box.full .lf-needs .need { padding: 0; border: 0; background: none; box-shadow: none; }
5075
+ .live-box.full .lf-needs .quick-replies { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent); padding-right: 24px; } /* one row that scrolls; the fade says there's more */
5076
+ .live-box.full .lf-needs .quick-replies .qr { flex: none; max-width: 80vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
5077
+ .live-box.full .lf-needs .need-actions { display: none; } /* Go opens the Mac's terminal; the bar's >_ is the phone's way there */
5078
+ /* answering in Needs you with the keyboard up: one box, not two */
5079
+ html.lf-kb.ny-typing .live-box.full > .composer { display: none; } /* ny-typing: app.js, kept a beat after the box loses focus so a tap lands where it was aimed */
5080
+ }
5081
+ @media (prefers-reduced-motion: no-preference) { .lf-needs .needs-fold .ic { transition: transform .15s ease; } }
5082
+
5083
+ /* 2026-10-29 the composer's suggestion on a touch screen (iOS has no Tab key; a long one was cut off):
5084
+ a chip above the box. "↳ Use suggestion" + its words (2 lines; ⌄ or a long-press shows all), ✕ dismisses. */
5085
+ .sug-chip { display: flex; align-items: stretch; gap: 2px; margin-top: 6px; border: 1px solid var(--line); border-radius: 12px; background: var(--card); min-height: 44px; box-sizing: border-box; }
5086
+ .sug-chip button { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; }
5087
+ .sug-chip button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
5088
+ .sug-use { flex: 1 1 auto; min-width: 0; min-height: 44px; display: flex; flex-direction: column; justify-content: center; gap: 1px; padding: 5px 4px 5px 12px; text-align: left; border-radius: 11px 0 0 11px; -webkit-touch-callout: none; user-select: none; -webkit-user-select: none; }
5089
+ .sug-l { font-size: 11.5px; line-height: 1.2; font-weight: 600; color: var(--accent); }
5090
+ .sug-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; font-size: 14px; line-height: 1.3; color: var(--text2); overflow-wrap: anywhere; }
5091
+ .sug-chip.open .sug-text { display: block; -webkit-line-clamp: unset; line-clamp: unset; overflow: visible; }
5092
+ .sug-more, .sug-x { flex: none; width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; padding: 0; color: var(--text2); }
5093
+ .sug-x { border-radius: 0 11px 11px 0; }
5094
+ .sug-more[hidden] { display: none; }
5095
+ .sug-more .ic, .sug-x .ic { width: 18px; height: 18px; }
5096
+ .sug-chip.open .sug-more .ic { transform: rotate(180deg); }
5097
+ @media (hover: hover) and (pointer: fine) { .sug-use:hover, .sug-more:hover, .sug-x:hover { background: var(--subtle); } }
5098
+ @media (prefers-reduced-motion: no-preference) { .sug-more .ic { transition: transform .15s ease; } }
5099
+ /* desktop: the words cut short, the Tab key always shown; a long one gets ⋯ with all of it in its tip */
5100
+ .ghost { display: flex; align-items: flex-start; gap: 0; }
5101
+ .ghost .ghost-t { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
5102
+ .ghost .ghost-k { flex: none; }
5103
+ .ghost .ghost-all { flex: none; margin-left: 4px; padding: 0 5px; border-radius: 5px; pointer-events: auto; cursor: help; line-height: 16px; font-size: 12px; border: 1px solid var(--line-strong); align-self: center; }
5104
+ .ghost .ghost-k { align-self: center; }