@leftoff/cli 0.9.8-beta.1 → 0.9.8-beta.3

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.
package/docs/SECURITY.md CHANGED
@@ -102,7 +102,7 @@ Live link is opt-in, per session: only sessions started with `leftoff live` have
102
102
 
103
103
  - **The channel** is a stdio MCP server that Claude Code starts. It opens no port. It talks to the daemon on 127.0.0.1 with the board's token, and the daemon accepts its calls (hello, next message, ack, reply, permission) from loopback only. A channel is tied to the session whose process started it, so the daemon only writes into sessions on its own Mac. Other Macs go through the peer link.
104
104
  - **Prompts from the board or a phone** reach Claude exactly like typed input, so they follow the same rule as answering a permission: a phone needs *Trust my paired devices*, and they're rate-limited and written to the session's audit trail. Anyone who can send prompts can make Claude act, so treat trusting your paired devices as giving them your keyboard.
105
- - **Permission relay** never allows anything on its own. A prompt waits for Allow once or Deny. With no answer the terminal dialog stays and decides, and an answer in the terminal wins. Live link answers one call at a time and never adds session-wide rules.
105
+ - **Permission relay** never allows anything on its own. A prompt waits for Allow once or Deny. With no answer the terminal dialog stays and decides, and an answer in the terminal wins. Live link answers one call at a time and never adds session-wide rules. **Always allow here** (0.9.8) is a click that writes the exact rule its ⓘ tooltip and label show into that project's `.claude/settings.local.json` (the file Claude Code's own option writes), only for shapes an exact rule can pin down. A broken settings file is never overwritten.
106
106
  - **Outside events** need this Mac's event key (`~/.config/checkpoints/secrets/live-events`, mode 600, made by `leftoff live key`, replaced with `--new`). Locally that's a Bearer key. Through the relay only an HMAC signature is accepted (5-minute window, replays refused), so the relay can pass an event along but can't read the key, forge an event or change one. Both the relay and the Mac rate-limit, and bodies are capped at 16 KB. Claude's instructions call an event information, not a command, but it is still text Claude reads: anyone with the key can put words in front of your session. Keep the key in your CI's secrets.
107
107
  - **Remote Restore** opens Ghostty tabs, so a phone or another Mac can only ask. The Mac shows a notification and a card, and only that Mac's own browser can confirm. Requests expire after 5 minutes, at most 3 wait at once, and they're rate-limited.
108
108
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leftoff",
3
- "version": "0.9.8-beta.1",
3
+ "version": "0.9.8-beta.3",
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": "Joel"
package/mod/hooks/core.js CHANGED
@@ -876,7 +876,7 @@ export function applyEvent(s, ev) {
876
876
  s.needsInput = false
877
877
  s.question = null
878
878
  s.lastNotification = null
879
- if (d.prompt && !s.firstPrompt) s.firstPrompt = clip(d.prompt, 300)
879
+ if (d.prompt && !s.firstPrompt) s.firstPrompt = clip(String(d.prompt).replace(/^\s*<channel\b[^>]*>([\s\S]*?)<\/channel>\s*$/, '$1').trim(), 300) // 0.9.8: a Live link message, without its wrapper
880
880
  if (d.countTurn === true) s.turns = (s.turns || 0) + 1 // 0.8.1: Codex counts a turn at its prompt when no task_started came first
881
881
  if (s.capture === 'hooks') { s.inTurn = true; s.activeSince = s.activeSince || at }
882
882
  }
@@ -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.8-beta.1' // keep in step with .claude-plugin/plugin.json
31
+ const MOD_VERSION = '0.9.8-beta.3' // 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.8-beta.1",
3
+ "version": "0.9.8-beta.3",
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
@@ -507,6 +507,22 @@ h2 .count { color: var(--faint); font-weight: 500; }
507
507
  .btn.allow:hover { filter: brightness(1.08); }
508
508
  .btn.deny { color: var(--red); border-color: color-mix(in srgb, var(--red) 40%, var(--line)); }
509
509
  .btn.deny:hover { background: var(--tint-red); }
510
+ /* Always allow here (0.9.8): one line like the rest, green outline, an ⓘ inside it with the rule in a tooltip */
511
+ .always-wrap { position: relative; display: inline-flex; max-width: 100%; }
512
+ .btn.always { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, var(--btn-line)); font-weight: 600; white-space: nowrap; padding-right: 34px; }
513
+ .btn.always:hover { background: var(--tint-green, color-mix(in srgb, var(--green) 10%, transparent)); color: var(--green); }
514
+ .tip-btn { position: absolute; top: 0; right: 0; bottom: 0; width: 32px; display: grid; place-items: center; padding: 0; margin: 0; border: 0; background: none; color: color-mix(in srgb, var(--green) 75%, var(--muted)); cursor: help; border-radius: 0 8px 8px 0; }
515
+ .tip-btn .ic { width: 15px; height: 15px; }
516
+ .tip-btn:hover, .tip-btn[aria-expanded="true"] { color: var(--green); background: var(--tint-green, color-mix(in srgb, var(--green) 10%, transparent)); }
517
+ .tip-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
518
+ .btn.always:disabled + .tip-btn { color: var(--muted); }
519
+ @media (pointer: coarse) { .btn.always { padding-right: 46px; } .tip-btn { width: 44px; } } /* the ⓘ is its own 44px target on touch */
520
+ .tip { position: fixed; z-index: 70; max-width: 320px; padding: 9px 11px; border-radius: 10px; background: var(--card2); border: 1px solid var(--line-strong); box-shadow: var(--toast-shadow); color: var(--text2); font-size: 12.5px; line-height: 1.45; font-weight: 400; text-align: left; white-space: normal; overflow-wrap: anywhere; }
521
+ .tip[hidden] { display: none; }
522
+ .tip code { font: 11.5px/1.4 var(--mono); color: var(--text); background: var(--bg2); border: 1px solid var(--line); border-radius: 5px; padding: 0 4px; }
523
+ .tip b { color: var(--text); font-weight: 600; }
524
+ .need-actions .btn { white-space: nowrap; }
525
+ .need.k-permission .need-actions .btn, .need.k-permission .deny-form .btn { min-height: 44px; }
510
526
  .deny-form { display: flex; gap: 6px; flex-wrap: wrap; }
511
527
  .deny-form[hidden] { display: none; }
512
528
  .deny-form input, .need .qother, .reply-text { flex: 1 1 220px; min-width: 0; background: var(--field); border: 1px solid var(--line); border-radius: 8px; color: var(--text); padding: 7px 9px; font: inherit; font-size: 13px; }
@@ -541,6 +557,10 @@ h2 .count { color: var(--faint); font-weight: 500; }
541
557
  @media (max-width: 640px) {
542
558
  .needs-strip { padding: 10px; margin-left: -4px; margin-right: -4px; }
543
559
  .need-actions .btn, .deny-form .btn { flex: 1 1 auto; justify-content: center; }
560
+ .need.k-permission .need-actions { flex-direction: column; align-items: stretch; } /* Allow / Always / Deny stack on a phone */
561
+ .need.k-permission .need-actions .btn { width: 100%; }
562
+ .need.k-permission .need-actions .always-wrap { width: 100%; }
563
+ .need.k-permission .need-actions .btn.always { padding-left: 46px; padding-right: 46px; } /* the label stays centred beside the ⓘ */
544
564
  .need-wait { margin-left: 0; }
545
565
  .answered .where, .recent .where { margin-left: 0; width: 100%; }
546
566
  }
@@ -2017,33 +2037,226 @@ mark.tx, .co-snip mark, .tx-snip mark, .tx-msg-text mark { background: var(--tx-
2017
2037
  }
2018
2038
 
2019
2039
  /* 0.9.8 Live link: the Live badge, and the session page's message box with its delivery states */
2020
- .pill.live { color: var(--green); background: var(--tint-green); }
2040
+ .pill.live-pill { color: var(--green); background: var(--tint-green); font-weight: 600; } /* not .live: that class is the open/closed dot */
2021
2041
  .live-box { margin: 16px 0 18px; border: 1px solid color-mix(in srgb, var(--green) 35%, var(--line)); background: linear-gradient(180deg, var(--tint-green), transparent 70%), var(--card); border-radius: var(--radius); padding: 12px 14px 14px; }
2022
2042
  .live-box.off { border-color: var(--line); background: var(--card); }
2023
2043
  .live-box.off p { margin: 4px 0 0; font-size: 13px; line-height: 1.5; }
2024
2044
  .live-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
2025
2045
  .live-head h2 { margin: 0; font-size: 15px; display: inline-flex; align-items: center; gap: 8px; }
2026
2046
  .live-head .faint { font-size: 12.5px; }
2027
- .live-log { list-style: none; margin: 8px 0 10px; padding: 0; display: flex; flex-direction: column; gap: 8px; max-height: 420px; overflow-y: auto; }
2028
- .lv { border: 1px solid var(--line); border-radius: 10px; padding: 7px 10px; background: var(--card); max-width: 88%; }
2029
- .lv-you { align-self: flex-end; background: var(--tint-accent); border-color: color-mix(in srgb, var(--accent) 30%, var(--line)); }
2030
- .lv-event { align-self: flex-end; background: var(--tint-yellow); border-color: color-mix(in srgb, var(--yellow) 35%, var(--line)); }
2031
- .lv-claude { align-self: flex-start; }
2047
+ /* 0.9.8 polish: a plain block list (a flex column squeezed its bubbles and clipped long ones), scrolling inside */
2048
+ .live-thread { position: relative; }
2049
+ .live-log { list-style: none; margin: 8px 0 10px; padding: 0 2px 0 0; display: block; max-height: min(62vh, 560px); overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; scroll-padding-bottom: 8px; }
2050
+ .live-log:empty { display: none; }
2051
+ .lv { display: block; border: 1px solid var(--line); border-radius: 10px; padding: 8px 11px; background: var(--card); margin: 0 0 8px; min-width: 0; }
2052
+ .lv:last-child { margin-bottom: 0; }
2053
+ .lv-you, .lv-event { width: fit-content; max-width: 88%; margin-left: auto; }
2054
+ .lv-you { background: var(--tint-accent); border-color: color-mix(in srgb, var(--accent) 30%, var(--line)); }
2055
+ .lv-event { background: var(--tint-yellow); border-color: color-mix(in srgb, var(--yellow) 35%, var(--line)); }
2056
+ .lv-claude { width: auto; max-width: 100%; }
2032
2057
  .lv-who { font-size: 11.5px; color: var(--muted); display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; }
2033
- .lv-text { white-space: pre-wrap; word-break: break-word; font-size: 13.5px; color: var(--text); margin-top: 2px; }
2034
- .lv-state { margin-left: auto; font-weight: 600; }
2035
- .lv-state.sent { color: var(--muted); }
2036
- .lv-state.received { color: var(--green); }
2037
- .lv-state.failed { color: var(--red); }
2058
+ .lv-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13.5px; line-height: 1.5; color: var(--text); margin-top: 2px; }
2059
+ .lv-text.lv-md { white-space: normal; }
2060
+ .lv-md > :first-child { margin-top: 0; }
2061
+ .lv-md > :last-child { margin-bottom: 0; }
2062
+ .lv-md p { margin: 0 0 8px; }
2063
+ .lv-md ul, .lv-md ol { margin: 4px 0 8px; padding-left: 22px; }
2064
+ .lv-md li { margin: 2px 0; }
2065
+ .lv-md h2, .lv-md h3, .lv-md h4, .lv-md h5, .lv-md h6 { font-size: 14px; font-weight: 700; margin: 10px 0 4px; color: var(--text); text-transform: none; letter-spacing: normal; border: 0; padding: 0; }
2066
+ .lv-md b, .lv-md strong { font-weight: 650; color: var(--text); }
2067
+ .lv-md code { font-family: var(--mono); font-size: .88em; background: var(--card2); border: 1px solid var(--line); border-radius: 5px; padding: 0 4px; }
2068
+ .lv-md pre { background: var(--card2); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; margin: 6px 0 8px; overflow-x: auto; white-space: pre; font-size: 12.5px; }
2069
+ .lv-md pre code { background: none; border: 0; padding: 0; }
2070
+ .lv-md a { color: var(--accent); text-decoration: underline; }
2071
+ .lv-md blockquote { margin: 6px 0; padding-left: 10px; border-left: 3px solid var(--line); color: var(--muted); }
2072
+ .lv-note { margin-top: 6px; font-size: 12px; color: var(--muted); }
2073
+ /* the status line under a message: Sent → Received → Working → Replied, with a spinner and the time */
2074
+ .lv-status { margin-top: 6px; font-size: 12px; color: var(--muted); }
2075
+ .lvs-steps { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
2076
+ .lvs-step { color: var(--faint); }
2077
+ .lvs-step.done { color: var(--green); }
2078
+ .lvs-step.now { color: var(--text); font-weight: 650; }
2079
+ .lvs-step.bad { color: var(--red); font-weight: 650; }
2080
+ .lvs-arrow { color: var(--faint); }
2081
+ .lvs-line { display: flex; align-items: center; gap: 6px; margin-top: 2px; color: var(--text2); }
2082
+ .lv-status.warn .lvs-line { color: var(--yellow); }
2083
+ .lv-status.bad .lvs-line { color: var(--red); }
2084
+ .lv-status.good .lvs-line { color: var(--green); }
2085
+ .lvs-steps .lvs-time { padding-left: 8px; }
2086
+ .lvs-time { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--muted); }
2087
+ .lv-spin { width: 12px; height: 12px; flex: none; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--accent) 25%, transparent); border-top-color: var(--accent); animation: spin .8s linear infinite; }
2088
+ @media (prefers-reduced-motion: reduce) { .lv-spin { animation: none; border-top-color: color-mix(in srgb, var(--accent) 25%, transparent); background: var(--accent); transform: scale(.6); } }
2089
+ .lv-earlier { list-style: none; display: flex; justify-content: center; margin: 2px 0 10px; }
2090
+ .live-earlier { min-height: 44px; font-size: 13px; }
2091
+ .lv-terminal { width: fit-content; max-width: 88%; margin-left: auto; background: color-mix(in srgb, var(--tint-accent) 50%, var(--card)); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); }
2092
+ .lv-tick { margin-top: 4px; font-size: 11.5px; color: var(--muted); text-align: right; }
2093
+ .lv-tick.got span { color: var(--accent); letter-spacing: -2px; padding-right: 2px; }
2094
+ .lv-tick.bad { color: var(--red); text-align: left; }
2095
+ .lv-working .lvs-line { margin-top: 0; }
2096
+ .lv-pending .lv-working { margin-top: 4px; }
2097
+ .lvs-sep { color: var(--faint); }
2098
+ .lv-working .lvs-time { margin-left: 0; }
2099
+ .lv-tool { font-size: 12px; line-height: 1.45; color: var(--text2); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
2100
+ .lv-tool.done { color: var(--faint); }
2101
+ .lv-tool.now { color: var(--text); }
2102
+ .lv-tools { margin-top: 6px; font-size: 12px; color: var(--muted); }
2103
+ .lv-tools > summary { cursor: pointer; min-height: 32px; display: inline-flex; align-items: center; gap: 4px; list-style: none; }
2104
+ .lv-tools > summary::-webkit-details-marker { display: none; }
2105
+ .lv-tools > summary::before { content: '▸'; font-size: 10px; color: var(--faint); transition: transform .15s; }
2106
+ .lv-tools[open] > summary::before { transform: rotate(90deg); }
2107
+ @media (prefers-reduced-motion: reduce) { .lv-tools > summary::before { transition: none; } }
2108
+ .lv-tools > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
2109
+ .lv-tools[open] > summary { margin-bottom: 2px; }
2110
+ .lv-none { color: var(--muted); font-style: italic; }
2111
+ .lv-pending { width: fit-content; min-width: min(260px, 100%); }
2112
+ .live-jump { position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%); min-height: 44px; padding: 0 16px; border-radius: 22px; border: 1px solid var(--btn-line); background: var(--card); color: var(--text); font: inherit; font-size: 13px; font-weight: 600; box-shadow: 0 4px 14px rgba(0,0,0,.18); cursor: pointer; z-index: 2; }
2113
+ .live-jump:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
2114
+ .live-jump-n { font-weight: 500; color: var(--muted); }
2038
2115
  .live-empty { margin: 6px 0 10px; font-size: 13px; }
2039
2116
  .live-err { color: var(--red); font-size: 13px; }
2040
2117
  .live-row .live-text { min-height: 44px; }
2118
+ /* Chat on a live card: straight to the Live box */
2119
+ .btn.chat-btn { position: relative; color: var(--green); border-color: color-mix(in srgb, var(--green) 40%, var(--btn-line)); font-weight: 600; }
2120
+ .chat-btn .unread-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); position: absolute; top: -3px; right: -3px; box-shadow: 0 0 0 2px var(--card); }
2041
2121
  .restore-ask .rs-m { color: var(--muted); font-size: 12.5px; }
2042
2122
  .rs-note.rs-ask.ok { color: var(--green); }
2043
2123
  .rs-note.rs-ask.warn { color: var(--red); }
2044
2124
  @media (pointer: coarse), (max-width: 600px) {
2045
- .live-row .btn, .restore-ask .btn { min-height: 44px; min-width: 44px; }
2125
+ .live-row .btn, .restore-ask .btn, .btn.chat-btn { min-height: 44px; min-width: 44px; }
2046
2126
  .need-actions .btn, .deny-form .btn { min-height: 44px; } /* Allow / Deny from the phone */
2127
+ .lv-tools > summary { min-height: 44px; }
2047
2128
  .live-box { padding: 10px; margin-left: -4px; margin-right: -4px; }
2048
- .lv { max-width: 100%; }
2129
+ .lv-you, .lv-event, .lv-terminal { max-width: 92%; }
2130
+ .live-log { max-height: 60vh; max-height: 60dvh; }
2049
2131
  }
2132
+
2133
+ /* ------------------------------------------------------------ the sidebar rail (0.9.8)
2134
+ From 820px up: a narrow rail on the left. The Leftoff mark at the top, then
2135
+ the sections as an icon over a small label, the current one in a soft pill,
2136
+ a red count on Needs you; search, What's new, Settings and this Mac's status
2137
+ (the Mac switcher) at the bottom. Under 820px: the mark, search and status in
2138
+ a slim top row, and a bottom tab bar (Board, Needs you, Lists, PRs, More). */
2139
+ :root { --rail-w: 84px; --rail-on: color-mix(in srgb, var(--accent) 13%, transparent); --bar-h: 58px; }
2140
+ .tab-more { display: none; }
2141
+ .moredot { position: absolute; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 2px var(--header-bg); }
2142
+ .moredot.warn { background: var(--yellow); }
2143
+ .moredot[hidden] { display: none; }
2144
+ .iconbtn .lbl { display: none; }
2145
+ .tabs a, .tab-more { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
2146
+ @media (min-width: 820px) {
2147
+ body { padding-left: calc(var(--rail-w) + env(safe-area-inset-left)); }
2148
+ .top { position: fixed !important; top: 0; bottom: 0; left: 0; width: calc(var(--rail-w) + env(safe-area-inset-left)); z-index: 20; /* !important: the rail never joins the page flow */
2149
+ flex-direction: column; flex-wrap: nowrap; align-items: stretch; justify-content: flex-start; gap: 6px; min-height: 0;
2150
+ padding: max(14px, env(safe-area-inset-top)) 8px max(12px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
2151
+ border-bottom: 0; border-right: 1px solid var(--line); overflow-x: hidden; overflow-y: auto; scrollbar-width: none; }
2152
+ .brand { order: 0; justify-content: center; padding: 2px 0 10px; border-radius: 12px; }
2153
+ .brand .logo { width: 30px; height: 30px; }
2154
+ .brand span { display: none; }
2155
+ .tabs { order: 0; flex: none; display: flex; flex-direction: column; align-items: stretch; gap: 4px; margin: 0; }
2156
+ .tabs a, .topright .searchbtn, .topright .iconbtn, .topright .health {
2157
+ position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
2158
+ width: auto; min-width: 0; height: auto; min-height: 54px; margin: 0; padding: 7px 2px 6px; border: 0; border-radius: 12px; background: none;
2159
+ color: var(--muted); font-size: 11px; font-weight: 500; line-height: 1.15; text-align: center; white-space: nowrap; box-shadow: none; }
2160
+ .tabs a .ic, .topright .ic { width: 20px; height: 20px; opacity: 1; }
2161
+ .tabs a .lbl, .topright .lbl, .topright .sb-lbl, .topright .health .label { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; flex: none; text-align: center; }
2162
+ .tabs .lbl-short, .topright .sb-kbd { display: none; }
2163
+ .tabs a:hover, .topright .searchbtn:hover, .topright .iconbtn:hover, .topright .health:hover, .topright .health[aria-expanded="true"] { color: var(--text); background: var(--bg2); }
2164
+ .tabs a.on, .topright .iconbtn.on { color: var(--text); background: var(--rail-on); box-shadow: none; }
2165
+ .tabs a.on .ic, .topright .iconbtn.on .ic { color: var(--accent); }
2166
+ .tabs a#needsTab.has-needs .ic { color: var(--red); }
2167
+ .navcount { position: absolute; top: 3px; left: calc(50% + 3px); min-width: 18px; height: 18px; line-height: 18px; font-size: 10.5px; padding: 0 5px; box-shadow: 0 0 0 2px var(--bg); }
2168
+ .topright { order: 0; margin: auto 0 0; padding-top: 8px; border-top: 1px solid var(--line); display: flex; flex-direction: column; align-items: stretch; gap: 4px; }
2169
+ .topright .health { order: 9; max-width: none; gap: 6px; }
2170
+ .topright .health .dot { width: 9px; height: 9px; margin: 5px 0 1px; }
2171
+ .newdot, .topdot { top: 8px; right: calc(50% - 15px); box-shadow: 0 0 0 2px var(--bg); }
2172
+ .set-nav { top: 24px; }
2173
+ }
2174
+ @media (min-width: 820px) and (max-height: 640px) {
2175
+ .tabs a, .topright .searchbtn, .topright .iconbtn, .topright .health { min-height: 46px; padding: 4px 2px; gap: 2px; }
2176
+ .brand { padding-bottom: 4px; }
2177
+ }
2178
+ @media (max-width: 819.98px) {
2179
+ .top { flex-wrap: nowrap; gap: 8px; min-height: 52px; padding-top: max(6px, env(safe-area-inset-top)); padding-bottom: 6px; background: var(--bg); -webkit-backdrop-filter: none; backdrop-filter: none; }
2180
+ .brand { order: 1; }
2181
+ .topright { order: 2; margin-left: auto; gap: 2px; }
2182
+ .topright #wnTab, .topright #setTab { display: none; }
2183
+ .searchbtn { width: 44px; height: 44px; }
2184
+ .health { height: 44px; }
2185
+ .tabs { position: fixed; z-index: 30; left: 0; right: 0; bottom: 0; order: 3; margin: 0; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 2px;
2186
+ padding: 4px max(6px, env(safe-area-inset-right)) max(4px, env(safe-area-inset-bottom)) max(6px, env(safe-area-inset-left));
2187
+ background: var(--header-bg); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); border-top: 1px solid var(--line); }
2188
+ .tabs .tab-extra { display: none; }
2189
+ .tabs a, .tab-more { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; height: auto; min-height: 50px; padding: 5px 2px 4px;
2190
+ border: 0; border-radius: 12px; background: none; color: var(--muted); font: inherit; font-size: 11px; font-weight: 500; line-height: 1.1; text-align: center; white-space: nowrap; box-shadow: none; cursor: pointer; }
2191
+ .tabs a .ic, .tab-more .ic { width: 22px; height: 22px; opacity: 1; }
2192
+ .tabs #needsTab .lbl { display: block; }
2193
+ .tabs #needsTab .lbl-short { display: none; }
2194
+ .tabs a.on, .tab-more.on { color: var(--text); background: none; box-shadow: none; }
2195
+ .tabs a:hover, .tab-more:hover { background: none; color: var(--text); }
2196
+ .tabs a.on .ic, .tab-more.on .ic { color: var(--accent); }
2197
+ .tabs a.on::before, .tab-more.on::before { content: ''; position: absolute; top: 4px; left: 50%; width: 46px; height: 28px; transform: translateX(-50%); border-radius: 14px; background: var(--rail-on); z-index: -1; }
2198
+ .tabs a, .tab-more { isolation: isolate; }
2199
+ .tab-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
2200
+ .navcount { position: absolute; top: 2px; left: calc(50% + 4px); right: auto; min-width: 17px; height: 17px; line-height: 17px; font-size: 10.5px; padding: 0 4px; box-shadow: 0 0 0 2px var(--bg); }
2201
+ .moredot { top: 6px; left: calc(50% + 8px); }
2202
+ main { padding-bottom: calc(var(--bar-h) + 40px + env(safe-area-inset-bottom)); }
2203
+ .toast { bottom: calc(var(--bar-h) + 14px + env(safe-area-inset-bottom)); }
2204
+ }
2205
+ /* More (phones): the rest of the sections, above the bar */
2206
+ .moremenu { position: fixed; z-index: 45; right: max(8px, env(safe-area-inset-right)); bottom: calc(var(--bar-h) + 8px + env(safe-area-inset-bottom)); width: 230px; max-width: calc(100vw - 16px);
2207
+ display: flex; flex-direction: column; padding: 6px; background: var(--card); color: var(--text); border: 1px solid var(--line); border-radius: 14px; box-shadow: 0 16px 40px rgba(15, 20, 35, .18), 0 2px 6px rgba(15, 20, 35, .08); animation: lo-in .12s ease-out; }
2208
+ [data-theme="dark"] .moremenu { background: var(--card2); box-shadow: 0 16px 40px rgba(0, 0, 0, .5); }
2209
+ .moremenu[hidden] { display: none; }
2210
+ .moremenu a { position: relative; display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 0 12px; border-radius: 10px; color: var(--text); text-decoration: none; font-size: 15px; }
2211
+ .moremenu a .ic { width: 20px; height: 20px; color: var(--muted); flex: none; }
2212
+ .moremenu a:hover, .moremenu a:focus-visible { background: var(--bg2); outline: none; }
2213
+ .moremenu a:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
2214
+ .moremenu a.on { background: var(--rail-on); }
2215
+ .moremenu a.on .ic { color: var(--accent); }
2216
+ .moremenu .newdot, .moremenu .topdot { position: static; margin-left: auto; box-shadow: none; }
2217
+ @media (min-width: 820px) { .moremenu { display: none !important; } }
2218
+ /* Quick look on Needs you (0.9.8) */
2219
+ .quicklook { position: fixed; z-index: 45; width: 360px; max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); overflow-y: auto; overscroll-behavior: contain;
2220
+ background: var(--card); color: var(--text); border: 1px solid var(--line); border-radius: 14px; box-shadow: 0 16px 40px rgba(15, 20, 35, .18), 0 2px 6px rgba(15, 20, 35, .08); padding: 12px 12px 8px; font-size: 13.5px; animation: lo-in .12s ease-out; }
2221
+ [data-theme="dark"] .quicklook { background: var(--card2); box-shadow: 0 16px 40px rgba(0, 0, 0, .5); }
2222
+ .quicklook[hidden] { display: none; }
2223
+ .ql-head { display: flex; align-items: center; gap: 8px; padding: 2px 4px 10px; border-bottom: 1px solid var(--line); }
2224
+ .ql-head h2 { margin: 0; font-size: 15px; font-weight: 650; text-transform: none; letter-spacing: 0; color: var(--text); }
2225
+ .ql-list { list-style: none; margin: 0; padding: 0; }
2226
+ .ql-item { display: flex; align-items: center; gap: 8px; padding: 9px 4px; border-bottom: 1px solid var(--line); min-width: 0; }
2227
+ .ql-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; color: var(--text); text-decoration: none; border-radius: 8px; }
2228
+ .ql-main:hover .ql-t { color: var(--accent); }
2229
+ .ql-main:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
2230
+ .ql-t { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2231
+ .ql-what { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2232
+ .ql-meta { color: var(--faint); font: 11.5px/1.3 var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2233
+ .ql-item.blocking .ql-t::before { content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--red); margin: 0 6px 1px 0; vertical-align: middle; }
2234
+ .ql-actions { display: flex; gap: 6px; flex: none; }
2235
+ .ql-actions .btn { min-height: 44px; padding: 0 11px; font-size: 13px; justify-content: center; }
2236
+ .ql-item.has-always { flex-wrap: wrap; } /* three buttons: their own row under the item */
2237
+ .ql-item.has-always .ql-main { flex: 1 1 100%; }
2238
+ .ql-item.has-always .ql-actions { flex: 1 1 100%; }
2239
+ .ql-item.has-always .ql-actions .btn, .ql-item.has-always .ql-actions .always-wrap { flex: 1 1 0; min-width: 0; }
2240
+ .ql-actions .always-wrap .btn.always { width: 100%; padding-right: 34px; }
2241
+ @media (pointer: coarse) { .ql-actions .always-wrap .btn.always { padding-right: 46px; } }
2242
+ .ql-empty, .ql-more { color: var(--muted); margin: 0; padding: 12px 4px; font-size: 13px; }
2243
+ .ql-more { padding: 8px 4px 0; }
2244
+ .ql-foot { display: flex; align-items: center; justify-content: flex-end; gap: 6px; min-height: 44px; padding: 0 6px; margin-top: 2px; color: var(--accent); font-weight: 600; text-decoration: none; border-radius: 8px; }
2245
+ .ql-foot:hover { text-decoration: underline; }
2246
+ .ql-foot:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
2247
+ @media (pointer: coarse), (max-width: 819.98px) { .ql-actions .btn { min-height: 44px; min-width: 64px; } .ql-main { min-height: 44px; justify-content: center; } }
2248
+ @media (max-width: 819.98px) { .quicklook { left: max(8px, env(safe-area-inset-left)); right: max(8px, env(safe-area-inset-right)); width: auto; max-width: none; bottom: calc(var(--bar-h) + 8px + env(safe-area-inset-bottom)); max-height: calc(100vh - var(--bar-h) - 80px); } }
2249
+ @media (prefers-reduced-motion: reduce) { .quicklook, .moremenu { animation: none; } }
2250
+
2251
+ /* Pull to refresh (phones, 0.9.8): the spinner comes down from under the top row */
2252
+ .ptr { position: fixed; z-index: 19; left: 50%; top: calc(env(safe-area-inset-top) + 8px); width: 38px; height: 38px; margin-top: -8px; transform: translate(-50%, 0); display: grid; place-items: center;
2253
+ border-radius: 50%; background: var(--card); border: 1px solid var(--line); box-shadow: 0 6px 18px rgba(15, 20, 35, .16); color: var(--muted); pointer-events: none; }
2254
+ [data-theme="dark"] .ptr { background: var(--card2); box-shadow: 0 6px 18px rgba(0, 0, 0, .45); }
2255
+ .ptr[hidden] { display: none; }
2256
+ .ptr-ring { display: grid; place-items: center; transform: rotate(var(--ptr-turn, 0deg)); }
2257
+ .ptr-ring svg { width: 19px; height: 19px; }
2258
+ .ptr.armed { color: var(--accent); }
2259
+ .ptr.busy .ptr-ring { animation: spin .8s linear infinite; }
2260
+ @media (pointer: coarse) and (max-width: 819.98px) { html, body { overscroll-behavior-y: contain; } }
2261
+ @media (prefers-reduced-motion: reduce) { .ptr-ring { transform: none; } .ptr.busy .ptr-ring { animation: lo-pulse 1.2s ease-in-out infinite; } }
2262
+ @keyframes lo-pulse { 50% { opacity: .35; } }