@xbghc/warden 0.13.1 → 0.14.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.
package/README.md CHANGED
@@ -11,6 +11,11 @@ with line comments you can copy back to the agent as a prompt.
11
11
  releasing one keeps the directory, installed dependencies included, for the next branch.
12
12
  - Side-by-side **Unstaged** and **Staged** file lists: staged means reviewed. Stage from the UI by dragging
13
13
  over the lines you have read (or a hunk, or a file), and unstage the same way from the Staged view.
14
+ - Debug code marked in a comment is folded out of the review and left out of whole-file staging, and
15
+ the Staged block warns while any of it is in the index (see [Debug code](#debug-code)).
16
+ - Code blocks fold by indentation, as an editor does for a language it has no grammar for: the
17
+ chevron beside a line that heads one shuts its body, and the head then says how many lines and
18
+ changes it hides. A dragged pick across a shut block takes none of them; stage the hunk to take them.
14
19
  - GitHub-style unified / side-by-side diff with syntax highlighting, collapsed file tree, lazy per-file loading, context expansion, virtual scrolling.
15
20
  - Line comments (single line or a dragged range), Markdown, edit / delete.
16
21
  - One click copies all comments as an agent-readable prompt to the clipboard.
@@ -200,6 +205,34 @@ Limits, each reported as a plain error rather than a half-applied patch:
200
205
  (the agent kept editing), the server answers 409 and the page reloads instead of staging the wrong lines.
201
206
  - If another git process holds the index lock the server retries briefly, then gives up with 409.
202
207
 
208
+ ## Debug code
209
+
210
+ Code written to poke at a problem and not meant to be committed can be marked in a comment. There is no
211
+ single convention for this, so warden takes the ones in use, in any comment syntax (`//`, `/*`, `#`,
212
+ `--`, `<!--`, `;`, `%`) and any case, as long as the marker opens the comment:
213
+
214
+ - a block between `debug:start` and `debug:end`, or `develblock:start` and `develblock:end` (the
215
+ webpack-strip-block / gulp-strip-block spelling); markers included, blocks nest, and a block left
216
+ open runs to the end of the file;
217
+ - a single line tagged `nocommit`, `no-commit` or `do not commit` (optionally `@nocommit` / `!nocommit`),
218
+ the tag the usual pre-commit hooks look for.
219
+
220
+ With *忽略调试代码* ticked in the sidebar (the default):
221
+
222
+ - runs of debug lines in a hunk fold into one striped row that opens with a click; jumping to a comment
223
+ inside one opens it;
224
+ - *暂存文件* and *暂存此 hunk* leave debug lines out. Lines picked one by one go in regardless — that
225
+ is how debug code is staged on purpose. A file whose changes are all debug code is refused with a
226
+ note to pick the lines instead;
227
+ - a file whose unstaged changes are all debug code does not count as still to review.
228
+
229
+ Whether it is ticked or not, the 已暂存 heading carries a *调试代码 N* mark while N added lines in the
230
+ index are debug code, with the files in its tooltip.
231
+
232
+ A hunk rarely shows the marker that opened the block its lines sit in, so warden reads both sides of a
233
+ file whole — only for the files `git grep` finds a marker in. Marker detection is textual: a marker
234
+ inside a string literal that follows a comment leader counts too.
235
+
203
236
  ## Comments and export
204
237
 
205
238
  Press the `+` that appears next to a line (drag to cover several lines) and write Markdown. What you
@@ -360,7 +393,8 @@ first time the file is read, and the 已读 marks those views used to keep are d
360
393
 
361
394
  Single user, local only. The server binds to `127.0.0.1` and executes git only through
362
395
  `execFile('git', [...])`. Reads go through an argument whitelist (`rev-parse`, `diff`, `show`, `log`,
363
- `worktree list`, `ls-files`, `status`, `merge-base`, `rev-list`) that refuses option-looking refs and any write-capable
396
+ `worktree list`, `ls-files`, `status`, `merge-base`, `rev-list`, `grep` without `-O` /
397
+ `--open-files-in-pager`) that refuses option-looking refs and any write-capable
364
398
  flag; requests that would need anything else get HTTP 400. The writes are few and each composes its
365
399
  own arguments. `POST /api/targets/:key/stage` runs `git apply --cached` (with `--reverse` for the
366
400
  Staged view) on a patch the server itself builds from the diff it just produced — the patch is never
package/dist/cli.js CHANGED
@@ -3059,7 +3059,8 @@ var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set([
3059
3059
  "merge-base",
3060
3060
  "rev-list",
3061
3061
  "for-each-ref",
3062
- "check-ref-format"
3062
+ "check-ref-format",
3063
+ "grep"
3063
3064
  ]);
3064
3065
  var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
3065
3066
  var DEFAULT_GIT_TIMEOUT_MS = 3e4;
@@ -3082,6 +3083,15 @@ function assertAllowedGitArgs(args) {
3082
3083
  if (sub === "worktree" && args[1] !== "list") {
3083
3084
  throw new GitError(`git worktree ${args[1] ?? ""} is not allowed`, null, "", 400, "git_subcommand_forbidden");
3084
3085
  }
3086
+ if (sub === "grep") {
3087
+ for (const arg of args.slice(1)) {
3088
+ if (arg === "--") break;
3089
+ const name = arg.split("=")[0];
3090
+ if (arg.startsWith("-O") || name.length > 3 && "--open-files-in-pager".startsWith(name)) {
3091
+ throw new GitError(`git option not allowed: ${arg}`, null, "", 400, "git_option_forbidden");
3092
+ }
3093
+ }
3094
+ }
3085
3095
  for (const arg of args.slice(1)) {
3086
3096
  if (arg.includes("\0")) throw new GitError("NUL byte in git argument", null, "", 400, "git_bad_argument");
3087
3097
  for (const prefix of FORBIDDEN_OPTION_PREFIXES) {
@@ -3587,6 +3597,40 @@ function parseUnifiedDiff(text) {
3587
3597
  return files;
3588
3598
  }
3589
3599
 
3600
+ // packages/server/src/debug.ts
3601
+ var LEADER = String.raw`(?:\/\/+|\/\*+|#+|--|<!--|;+|%+)\s*`;
3602
+ var BLOCK_START = new RegExp(`${LEADER}(?:debug|develblock):start\\b`, "i");
3603
+ var BLOCK_END = new RegExp(`${LEADER}(?:debug|develblock):end\\b`, "i");
3604
+ var LINE_TAG = new RegExp(`${LEADER}[@!]?(?:no-?commit|do not commit)\\b`, "i");
3605
+ var DEBUG_MARKER_STRINGS = ["debug:start", "develblock:start", "nocommit", "no-commit", "do not commit"];
3606
+ function debugLineNumbers(text) {
3607
+ const out = /* @__PURE__ */ new Set();
3608
+ const lines = text.split("\n");
3609
+ let depth = 0;
3610
+ lines.forEach((line, i) => {
3611
+ const no = i + 1;
3612
+ if (BLOCK_START.test(line)) depth++;
3613
+ if (depth > 0 || LINE_TAG.test(line)) out.add(no);
3614
+ if (depth > 0 && BLOCK_END.test(line)) depth--;
3615
+ });
3616
+ return out;
3617
+ }
3618
+ function markDebugLines(diff, newSide, oldSide) {
3619
+ let added = 0;
3620
+ let deleted = 0;
3621
+ for (const hunk of diff.hunks) {
3622
+ for (const line of hunk.lines) {
3623
+ const debug = line.type === "del" ? !!oldSide?.has(line.oldLineNo) : !!newSide?.has(line.newLineNo);
3624
+ if (!debug) continue;
3625
+ line.debug = true;
3626
+ if (line.type === "add") added++;
3627
+ else if (line.type === "del") deleted++;
3628
+ }
3629
+ }
3630
+ if (added) diff.debugAdditions = added;
3631
+ if (deleted) diff.debugDeletions = deleted;
3632
+ }
3633
+
3590
3634
  // packages/server/src/targets.ts
3591
3635
  var DIFF_BASE_ARGS = ["diff", "--no-color", "--no-ext-diff", "-U3", "-M", "--find-renames"];
3592
3636
  function resolveTargetContext(repo, worktrees, key) {
@@ -3722,7 +3766,8 @@ async function refForSide(ctx, side) {
3722
3766
  if (side === "new") return t.sha;
3723
3767
  return await revParse(ctx.cwd, `${t.sha}^`) ?? EMPTY_TREE_SHA;
3724
3768
  case "range":
3725
- return side === "new" ? t.head : t.base;
3769
+ if (side === "new") return t.head;
3770
+ return await mergeBase(ctx.cwd, t.base, t.head) ?? t.base;
3726
3771
  case "base":
3727
3772
  return side === "new" ? void 0 : baseSha(ctx, t.ref);
3728
3773
  }
@@ -3746,6 +3791,49 @@ async function getFullFile(ctx, filePath, side) {
3746
3791
  throw e;
3747
3792
  }
3748
3793
  }
3794
+ var GREP_CHUNK = 500;
3795
+ async function pathsWithMarkers(ctx, side, paths) {
3796
+ const found = /* @__PURE__ */ new Set();
3797
+ if (paths.length === 0) return found;
3798
+ const ref = await refForSide(ctx, side);
3799
+ const args = ["grep", "-l", "-z", "-I", "-i", "-F", ...DEBUG_MARKER_STRINGS.flatMap((m) => ["-e", m])];
3800
+ if (ref === void 0) args.push("--untracked");
3801
+ else if (ref === ":0") args.push("--cached");
3802
+ else args.push(ref);
3803
+ const prefix = ref === void 0 || ref === ":0" ? "" : `${ref}:`;
3804
+ for (let i = 0; i < paths.length; i += GREP_CHUNK) {
3805
+ const chunk = paths.slice(i, i + GREP_CHUNK).map(literal);
3806
+ const r = await runGit([...args, "--", ...chunk], { cwd: ctx.cwd, okCodes: [0, 1] });
3807
+ for (const p of r.stdout.split("\0")) if (p) found.add(p.startsWith(prefix) ? p.slice(prefix.length) : p);
3808
+ }
3809
+ return found;
3810
+ }
3811
+ async function annotateDebug(ctx, files) {
3812
+ const text = files.filter((f) => !f.binary && f.hunks.length > 0);
3813
+ const oldPathOf = (f) => f.oldPath ?? f.path;
3814
+ const none = () => /* @__PURE__ */ new Set();
3815
+ const [onNew, onOld] = await Promise.all([
3816
+ pathsWithMarkers(
3817
+ ctx,
3818
+ "new",
3819
+ text.filter((f) => f.status !== "deleted").map((f) => f.path)
3820
+ ).catch(none),
3821
+ pathsWithMarkers(ctx, "old", text.filter((f) => f.status !== "added").map(oldPathOf)).catch(none)
3822
+ ]);
3823
+ const read = async (p, side, marked) => {
3824
+ if (!marked.has(p)) return void 0;
3825
+ const content = await getFullFile(ctx, p, side).catch(() => null);
3826
+ return content === null ? void 0 : debugLineNumbers(content);
3827
+ };
3828
+ await mapLimit(
3829
+ text.filter((f) => onNew.has(f.path) || onOld.has(oldPathOf(f))),
3830
+ 8,
3831
+ async (f) => {
3832
+ const [n, o] = await Promise.all([read(f.path, "new", onNew), read(oldPathOf(f), "old", onOld)]);
3833
+ markDebugLines(f, n, o);
3834
+ }
3835
+ );
3836
+ }
3749
3837
 
3750
3838
  // packages/server/src/anchor.ts
3751
3839
  function sideLines(diff, side) {
@@ -3895,9 +3983,9 @@ function cutHunk(hunk, picked, mode) {
3895
3983
  });
3896
3984
  return out;
3897
3985
  }
3898
- function pickedLines(diff, selection) {
3986
+ function pickedLines(diff, selection, skipDebug) {
3899
3987
  const picked = /* @__PURE__ */ new Map();
3900
- const changed = (h) => h.lines.map((l, i) => l.type === "context" ? -1 : i).filter((i) => i >= 0);
3988
+ const changed = (h) => h.lines.map((l, i) => l.type === "context" || skipDebug && l.debug ? -1 : i).filter((i) => i >= 0);
3901
3989
  if (selection === void 0) {
3902
3990
  for (const [i, h] of diff.hunks.entries()) picked.set(i, new Set(changed(h)));
3903
3991
  return picked;
@@ -3921,10 +4009,11 @@ function pickedLines(diff, selection) {
3921
4009
  }
3922
4010
  return picked;
3923
4011
  }
3924
- function buildStagePatch(diff, mode, selection) {
4012
+ function buildStagePatch(diff, mode, selection, opts = {}) {
3925
4013
  if (diff.binary) throw badRequest("binary files cannot be staged from here; use git add", "not_stageable");
3926
4014
  const whole = selection === void 0;
3927
- const picked = pickedLines(diff, selection);
4015
+ const skipDebug = !!opts.skipDebug && mode === "stage";
4016
+ const picked = pickedLines(diff, selection, skipDebug);
3928
4017
  let lines = 0;
3929
4018
  for (const set of picked.values()) lines += set.size;
3930
4019
  const changedTotal = diff.hunks.reduce((n, h) => n + h.lines.filter((l) => l.type !== "context").length, 0);
@@ -3957,7 +4046,11 @@ function buildStagePatch(diff, mode, selection) {
3957
4046
  }
3958
4047
  if (lines === 0) {
3959
4048
  const headerOnly = whole && (modeChange || rename2 || (diff.status === "added" || diff.status === "deleted") && diff.hunks.length === 0);
3960
- if (!headerOnly) throw badRequest("no changed lines selected", "empty_selection");
4049
+ if (!headerOnly) {
4050
+ if (skipDebug && [...pickedLines(diff, selection, false).values()].some((set) => set.size > 0))
4051
+ throw badRequest("every changed line picked is debug code; pick those lines one by one to stage them anyway", "debug_only");
4052
+ throw badRequest("no changed lines selected", "empty_selection");
4053
+ }
3961
4054
  return { patch: header.join("\n") + "\n", lines: 0 };
3962
4055
  }
3963
4056
  let delta = 0;
@@ -4225,7 +4318,7 @@ function stateFilePath(repoRoot, baseDir = dataDir()) {
4225
4318
  return path4.join(baseDir, repoHash(repoRoot), "state.json");
4226
4319
  }
4227
4320
  function defaultPrefs() {
4228
- return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false };
4321
+ return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false, ignoreDebug: true };
4229
4322
  }
4230
4323
  function defaultState(repoRoot) {
4231
4324
  return { schemaVersion: 1, repoRoot, targets: {}, issues: [], todos: [], prefs: defaultPrefs() };
@@ -4266,7 +4359,8 @@ function normalise(raw2, repoRoot) {
4266
4359
  ...r.prefs ?? {},
4267
4360
  nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
4268
4361
  autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
4269
- railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false
4362
+ railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false,
4363
+ ignoreDebug: typeof r.prefs?.ignoreDebug === "boolean" ? r.prefs.ignoreDebug : true
4270
4364
  }
4271
4365
  };
4272
4366
  }
@@ -4863,6 +4957,7 @@ function createApp(opts) {
4863
4957
  if (typeof body.lastTarget === "string") s.prefs.lastTarget = body.lastTarget;
4864
4958
  if (typeof body.autoRefresh === "boolean") s.prefs.autoRefresh = body.autoRefresh;
4865
4959
  if (typeof body.railOpen === "boolean") s.prefs.railOpen = body.railOpen;
4960
+ if (typeof body.ignoreDebug === "boolean") s.prefs.ignoreDebug = body.ignoreDebug;
4866
4961
  if (body.nvimSocketByRoot && typeof body.nvimSocketByRoot === "object") {
4867
4962
  s.prefs.nvimSocketByRoot = { ...s.prefs.nvimSocketByRoot, ...body.nvimSocketByRoot };
4868
4963
  }
@@ -4874,6 +4969,7 @@ function createApp(opts) {
4874
4969
  const key = c.req.param("key");
4875
4970
  const ctx = await targetCtx(key);
4876
4971
  const diffs = await listTargetDiffs(ctx);
4972
+ await annotateDebug(ctx, diffs);
4877
4973
  listings.set(key, { at: Date.now(), files: diffs });
4878
4974
  const notices = changedNotices.get(key) ?? /* @__PURE__ */ new Set();
4879
4975
  changedNotices.set(key, notices);
@@ -4914,6 +5010,7 @@ function createApp(opts) {
4914
5010
  const untracked = c.req.query("untracked") === "1" ? true : void 0;
4915
5011
  const diff = await fileDiffWithHints(ctx, filePath, { oldPath, untracked });
4916
5012
  if (!diff) throw notFound(`no diff for ${filePath} in ${key}`, "no_diff");
5013
+ await annotateDebug(ctx, [diff]);
4917
5014
  return c.json(diff);
4918
5015
  });
4919
5016
  api.get("/targets/:key/file/full", async (c) => {
@@ -4950,11 +5047,13 @@ function createApp(opts) {
4950
5047
  if (!body.path || typeof body.path !== "string") throw badRequest("missing path");
4951
5048
  if (!body.contentHash || typeof body.contentHash !== "string") throw badRequest("contentHash is required");
4952
5049
  if (body.hunks !== void 0 && !Array.isArray(body.hunks)) throw badRequest("hunks must be an array", "bad_selection");
5050
+ if (body.skipDebug !== void 0 && typeof body.skipDebug !== "boolean") throw badRequest("skipDebug must be a boolean", "bad_selection");
4953
5051
  const diff = await fileDiffWithHints(ctx, body.path);
4954
5052
  if (!diff) throw badRequest(`file ${body.path} is not part of ${key}`, "no_diff");
4955
5053
  if (diff.contentHash !== body.contentHash)
4956
5054
  throw new HttpError(409, "the diff changed since it was loaded; refresh and pick the lines again", "diff_changed");
4957
- const { patch, lines } = buildStagePatch(diff, mode, body.hunks);
5055
+ if (body.skipDebug) await annotateDebug(ctx, [diff]);
5056
+ const { patch, lines } = buildStagePatch(diff, mode, body.hunks, { skipDebug: body.skipDebug });
4958
5057
  await applyToIndex(ctx.cwd, patch, { reverse: mode === "unstage" });
4959
5058
  void watchers.get(ctx.cwd)?.poll();
4960
5059
  const res = { ok: true, lines };
@@ -5665,7 +5764,7 @@ async function startServer(opts) {
5665
5764
  }
5666
5765
 
5667
5766
  // bin/cli.ts
5668
- var VERSION = true ? "0.13.1" : "dev";
5767
+ var VERSION = true ? "0.14.0" : "dev";
5669
5768
  function parseArgs(argv) {
5670
5769
  const args = { repoPath: process.cwd(), open: true, updateCheck: true, help: false, version: false };
5671
5770
  for (let i = 0; i < argv.length; i++) {
@@ -0,0 +1 @@
1
+ @font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff) format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff) format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2) format("woff2"),url(/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff) format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2) format("woff2"),url(/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff) format("woff")}:root{--ground: #eef0f3;--surface: #ffffff;--surface-2: #f5f6f8;--surface-3: #e4e7ec;--line: #d9dde3;--line-strong: #b7bdc7;--ink: #16181d;--ink-2: #454b58;--muted: #626977;--accent: #5b47d9;--accent-strong: #4a38bf;--accent-soft: #ebe8fb;--accent-line: #c4bbf2;--add-bg: #e3f5e7;--add-bg-strong: #bfe9c9;--add-fg: #1e7a3a;--del-bg: #fce8e8;--del-bg-strong: #f5c2c2;--del-fg: #c42b2b;--warn-bg: #f6e7b8;--warn-soft: #fdf8e6;--warn-fg: #8a6100;--warn-line: #d9b64a;--r-ctl: 3px;--r-card: 6px;--ctl-h: 26px;--t-meta: 11px;--t-ui: 13px;--t-lead: 15px;--t-figure: 26px;--lift: 0 4px 16px rgba(20, 24, 33, .13);--sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Noto Sans Mono CJK SC", monospace}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{font:var(--t-ui) / 1.5 var(--sans);color:var(--ink);background:var(--ground);-webkit-font-smoothing:antialiased}button,input,select,textarea{font:inherit;color:inherit}button{height:var(--ctl-h);display:inline-flex;align-items:center;gap:6px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-ctl);padding:0 10px;cursor:pointer;white-space:nowrap}button:hover:not(:disabled){border-color:var(--line-strong);background:var(--surface-2)}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--ink);border-color:var(--ink);color:#fff}button.primary:hover:not(:disabled){background:#2b2f38;border-color:#2b2f38}button.accent{background:var(--accent);border-color:var(--accent);color:#fff}button.accent:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}button.link{height:auto;background:none;border:none;color:var(--ink-2);padding:0 4px}button.link:hover:not(:disabled){background:none;color:var(--ink);text-decoration:underline;text-underline-offset:2px}button.link.danger{color:var(--del-fg)}button.link.active{color:var(--accent);font-weight:500}button.icon{padding:0 7px}button.quiet{background:none;border-color:transparent;color:var(--ink-2)}button.quiet:hover:not(:disabled){background:var(--surface-3);border-color:transparent;color:var(--ink)}body.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}button.toggle:before{content:"";width:6px;height:6px;border-radius:50%;border:1.5px solid currentColor}button.toggle[aria-pressed=true]{border-color:var(--ink)}button.toggle[aria-pressed=true]:before{background:currentColor}input,select,textarea{height:var(--ctl-h);border:1px solid var(--line);border-radius:var(--r-ctl);padding:0 8px;background:var(--surface)}select{padding-right:4px}textarea{height:auto;width:100%;resize:vertical;font-family:var(--mono);font-size:12px;line-height:1.6;padding:6px 8px}input[type=checkbox]{height:auto;margin:0;accent-color:var(--ink)}input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}button:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--accent);outline-offset:1px}kbd{font-family:var(--mono);font-size:11px;background:var(--surface);border:1px solid var(--line);border-bottom-width:2px;border-radius:3px;padding:0 5px;color:var(--ink-2)}.mono{font-family:var(--mono);font-size:12px}.muted{color:var(--muted)}.small{font-size:11px}.spacer{flex:1}.add{color:var(--add-fg)}.del{color:var(--del-fg)}.check{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;cursor:pointer}.badge{display:inline-flex;align-items:center;height:17px;padding:0 6px;border-radius:var(--r-ctl);font-family:var(--mono);font-size:11px;background:var(--surface-3);color:var(--ink-2);white-space:nowrap}.badge-active{background:var(--accent-soft);color:var(--accent)}.badge-exported{background:var(--surface-3);color:var(--muted)}.badge-orphaned{background:var(--warn-bg);color:var(--warn-fg)}.badge-open{background:var(--ink);color:#fff}.badge-closed{background:var(--surface-3);color:var(--muted)}.seg{display:inline-flex;gap:1px;padding:2px;background:var(--surface-3);border-radius:4px}.seg button{height:22px;padding:0 9px;background:transparent;border:1px solid transparent;border-radius:var(--r-ctl);color:var(--ink-2)}.seg button:hover:not(:disabled){background:transparent;border-color:transparent;color:var(--ink)}.seg button.active{background:var(--surface);border-color:var(--line);color:var(--ink);font-weight:500}.seg.small button{height:20px;padding:0 7px;font-size:12px}.seg button.active .tab-count{color:var(--ink)}.tabs{display:inline-flex;align-self:stretch;align-items:stretch;gap:2px}.tab{height:auto;background:none;border:none;border-radius:0;padding:0 6px;color:var(--ink-2);position:relative}.tab:hover:not(:disabled){background:none;color:var(--ink)}.tab.active{color:var(--ink);font-weight:500}.tab.active:after{content:"";position:absolute;left:6px;right:6px;bottom:-1px;height:2px;background:var(--ink)}.tab-count{font-family:var(--mono);font-size:11px;color:var(--muted)}.tab.active .tab-count{color:var(--ink)}.tabs.small .tab{padding:0 5px;font-size:12px}.error-box{color:var(--del-fg);background:var(--del-bg);padding:8px 12px;border-radius:var(--r-card);margin:8px}.empty{padding:20px 12px;text-align:center;color:var(--muted);font-size:12px;line-height:1.6}.app{display:flex;flex-direction:column;height:100%}.body{flex:1;display:flex;min-height:0}.main{flex:1;min-width:0;display:flex;flex-direction:column;background:var(--surface)}.loading-screen,.fatal{padding:48px;text-align:center;color:var(--muted)}.fatal h1{font-family:var(--mono);font-weight:500;font-size:20px;letter-spacing:-.02em;color:var(--ink)}.placeholder{padding:48px 24px;text-align:center;color:var(--muted)}.placeholder p{margin:6px 0}.topbar{display:flex;align-items:center;gap:14px;padding:0 10px 0 12px;min-height:40px;background:var(--ground);flex-wrap:wrap}.brand{display:flex;align-items:center;gap:7px}.mark{width:16px;height:16px;flex:none}.wordmark{font-family:var(--mono);font-weight:500;font-size:15px;letter-spacing:-.02em;color:var(--ink)}.scope{display:flex;align-items:center;gap:8px}.update-chip{height:auto;padding:1px 6px;border:none;font-family:var(--mono);font-size:var(--t-meta);color:var(--ink-2);background:var(--surface-3)}.update-chip:hover:not(:disabled){color:var(--ink);background:var(--line)}.repo-name{font-weight:500}.branch{font-family:var(--mono);font-size:11px;color:var(--ink-2);background:var(--surface-3);border-radius:var(--r-ctl);padding:1px 6px}.inline-form{display:inline-flex;align-items:center;gap:4px}.inline-form input{width:130px;font-family:var(--mono);font-size:12px}.topbar-right{margin-left:auto;display:flex;align-items:center;gap:6px}.nvim{display:inline-flex;align-items:center;gap:4px;max-width:300px}.nvim select{max-width:240px}.nvim-one,.nvim-idle{font-family:var(--mono);font-size:12px;color:var(--muted)}.rail-switch{margin-left:8px;background:none;border-color:transparent;color:var(--ink-2)}.rail-switch:hover:not(:disabled){background:var(--surface-3);border-color:transparent;color:var(--ink)}.rail-switch.active{background:var(--surface);border-color:var(--line);color:var(--ink)}.rail-switch-n{font-family:var(--mono);font-size:var(--t-meta);color:var(--muted)}.rail-switch.has-unexported .rail-switch-n{background:var(--accent-soft);color:var(--accent);border-radius:var(--r-ctl);padding:0 5px}.sidebar{flex:none;display:flex;flex-direction:column;background:var(--ground);overflow:hidden;position:relative}.side-nav{padding:8px 12px;flex-shrink:0}.side-view-select{width:100%;min-height:30px;font-weight:500}.side-slot,.side-form{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;align-items:stretch;gap:10px;padding:12px 12px 16px}.side-form{overflow:visible;padding:0}.side-slot>.side-form{flex:none}.field{display:flex;flex-direction:column;gap:3px;font-size:var(--t-meta);color:var(--muted)}.field input{width:100%;font-size:var(--t-ui)}.field-row{display:flex;align-items:center;gap:6px}.field-row input{min-width:0;flex:1}.side-group{display:flex;flex-direction:column;gap:8px;padding-top:12px;border-top:1px solid var(--line)}.side-group-title{font-weight:600;color:var(--ink-2)}.side-form button{justify-content:center;background:var(--surface-2);border-color:var(--line-strong);font-weight:500}.side-form button:hover:not(:disabled){background:var(--surface-3);border-color:var(--line-strong)}.side-form button.toggle{justify-content:flex-start;font-weight:400}.side-form button.toggle[aria-pressed=true]{background:var(--ink);border-color:var(--ink);color:#fff}.side-form button.link{justify-content:flex-start;background:none;border:none;font-weight:400}.side-form-status{display:flex;align-items:center;gap:8px;font-variant-numeric:tabular-nums}.side-grip{position:absolute;top:0;right:0;bottom:0;width:5px;cursor:col-resize;background:var(--line);background-clip:content-box;border-left:4px solid transparent}.side-grip:hover{background:var(--line-strong)}.tree-blocks{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column}.tree-block{display:flex;flex-direction:column;flex:none;min-width:0}.tree-block+.tree-block{margin-top:6px;border-top:1px solid var(--line)}.tree-block.empty .block-head{padding:6px 12px}.progress{flex:none;padding:10px 12px 12px}.progress-figure{display:flex;align-items:baseline;gap:6px}.progress-done{font-family:var(--mono);font-size:var(--t-figure);font-weight:500;line-height:1.1;letter-spacing:-.03em;color:var(--ink);font-variant-numeric:tabular-nums}.progress-done.complete{color:var(--add-fg)}.progress-of{font-family:var(--mono);font-size:var(--t-lead);color:var(--muted);font-variant-numeric:tabular-nums}.progress-label{font-size:var(--t-meta);color:var(--muted)}.progress-track{margin-top:7px;height:3px;background:var(--surface-3);border-radius:2px;overflow:hidden}.progress-track>span{display:block;height:100%;background:var(--ink);transition:width .25s}.sidebar-target{flex:none;display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--line);white-space:nowrap}.sidebar-target-name{font-family:var(--mono);font-size:12px;color:var(--ink-2);min-width:0;overflow:hidden;text-overflow:ellipsis}.sidebar-target .link{font-size:12px}.block-head{position:sticky;top:0;z-index:1;display:flex;align-items:center;gap:8px;padding:8px 12px 4px;background:var(--ground);font-size:12px;white-space:nowrap}.block-title{font-weight:600}.block-n{font-family:var(--mono);font-size:var(--t-meta);color:var(--muted)}.block-head .counts{font-family:var(--mono);font-size:var(--t-meta);display:inline-flex;gap:4px}.tree-tools{display:inline-flex;gap:2px;opacity:0}.tree-block:hover .tree-tools,.tree-tools:focus-within{opacity:1}.tree-tools .link{font-size:var(--t-meta);color:var(--muted)}.tree{padding:0 0 4px}.tree-row{display:flex;align-items:center;gap:6px;padding:0 10px;height:24px;cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none}.tree-row:hover{background:var(--surface-3)}.tree-row.active{background:var(--surface);box-shadow:inset 3px 0 var(--ink);font-weight:500}.tree-row.active .name{color:var(--ink)}.tree-row.viewed .name{color:var(--muted)}.tree-row .name{overflow:hidden;text-overflow:ellipsis}.tree-row.dir .name{font-weight:500;color:var(--ink-2)}.chev{display:inline-block;width:12px;font-size:10px;color:var(--muted);transition:transform .1s}.chev.open{transform:rotate(90deg)}.tree-row .counts{margin-left:auto;font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.tree-row .count{font-family:var(--mono);font-size:11px}.viewed-box{margin:0}.status{font-family:var(--mono);font-size:11px;font-weight:500;width:14px;text-align:center}.status-added{color:var(--add-fg)}.status-modified{color:var(--warn-fg)}.status-deleted{color:var(--del-fg)}.status-renamed{color:var(--ink-2)}.changed{font-size:11px;background:var(--warn-bg);color:var(--warn-fg);border-radius:var(--r-ctl);padding:0 5px}.cc{font-family:var(--mono);font-size:11px;background:var(--accent-soft);color:var(--accent);border-radius:var(--r-ctl);padding:0 5px}.diff-panel{flex:1;display:flex;flex-direction:column;min-height:0;position:relative}.file-head{display:flex;align-items:center;gap:10px;padding:0 14px;min-height:40px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}.file-head .path{font-family:var(--mono);font-size:var(--t-lead);letter-spacing:-.01em}.file-head .path .dir{color:var(--muted)}.file-head .path .base{font-weight:500}.file-head .counts{font-family:var(--mono);font-size:11px;display:inline-flex;gap:4px}.file-head .check{font-size:12px}.diff-scroll{flex:1;overflow:auto;font-family:var(--mono);font-size:12px;line-height:20px;tab-size:4;background:var(--surface)}.diff-scroll.selecting,.diff-scroll.picking{-webkit-user-select:none;user-select:none;cursor:row-resize}.diff-scroll.reattaching .add-btn{background:var(--warn-fg)}.vrow{will-change:transform}.row{display:flex;min-height:20px}.row.line,.cell{position:relative}.pin{position:absolute;right:0;top:0;bottom:0;width:3px;background:var(--accent)}.pin.muted{background:var(--line-strong)}.pill{position:absolute;right:10px;top:2px;height:16px;min-width:16px;padding:0 5px;justify-content:center;border:none;border-radius:8px;background:var(--accent);color:#fff;font-family:var(--sans);font-size:11px;line-height:16px}.pill:hover:not(:disabled){background:var(--accent-strong);border-color:transparent}.pill.muted{background:var(--line-strong)}.row.hunk-head{align-items:center;background:var(--surface-2);color:var(--muted);font-size:11.5px;padding:3px 12px 3px 128px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);white-space:pre-wrap}.hunk-text{flex:1;min-width:0}.hunk-stage{font-family:var(--sans);font-size:11px;color:var(--muted);white-space:nowrap}.stagec{flex:0 0 18px;position:relative;-webkit-user-select:none;user-select:none}.stage-box{position:absolute;left:4px;top:4px;width:12px;height:12px;padding:0;border:1.5px solid var(--ink-2);border-radius:2px;background:var(--surface);opacity:0;cursor:pointer;z-index:1}.stage-box:hover:not(:disabled){background:var(--surface-3);border-color:var(--ink)}.row:hover .stage-box,.cell:hover .stage-box,.stage-box.on{opacity:1}.stage-box.on,.stage-box.on:hover:not(:disabled){background:var(--ink);border-color:var(--ink)}.stage-box.on:after{content:"";position:absolute;left:3px;top:0;width:3px;height:6px;border:solid #fff;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)}.row.line.picked,.cell.picked{box-shadow:inset 3px 0 var(--ink)}.row.line.add.picked,.cell.add.picked{background:var(--add-bg-strong)}.row.line.del.picked,.cell.del.picked{background:var(--del-bg-strong)}.stage-bar{position:absolute;left:50%;bottom:16px;transform:translate(-50%);display:flex;align-items:center;gap:8px;padding:6px 8px 6px 12px;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--r-card);box-shadow:0 6px 20px #16181d24;font-size:12px;white-space:nowrap;z-index:3}.stage-bar-count{margin-right:4px}.stage-bar kbd{margin-left:2px}.stage-bar .primary kbd{background:#ffffff29;border-color:#ffffff4d;color:#fff}.tree-row .stage-file{display:none;font-size:11px;padding:0 2px}.tree-row:hover .stage-file{display:inline-flex}.row.gap-row{background:var(--surface-2);justify-content:center;gap:16px;padding:2px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line);font-family:var(--sans);font-size:12px}.gap-row .link{color:var(--muted)}.row.debug-row{background:repeating-linear-gradient(-45deg,var(--surface-2) 0 6px,var(--surface) 6px 12px);border-top:1px dashed var(--line-strong);border-bottom:1px dashed var(--line-strong);padding:2px 0 2px 60px;font-family:var(--sans);font-size:12px}.row.debug-row.open{border-bottom-style:none}.debug-row .link{color:var(--muted);text-decoration:none}.debug-row .link:hover{color:var(--ink)}.row.attach{background:var(--surface-2);border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:6px 12px 6px 60px;font-family:var(--sans);font-size:13px;display:block}.gut{flex:0 0 48px;text-align:right;padding-right:6px;font-size:11px;color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none}.gut:hover{color:var(--ink)}.addc{flex:0 0 18px;position:relative}.add-btn{position:absolute;left:0;top:2px;width:16px;height:16px;padding:0;justify-content:center;line-height:16px;border-radius:var(--r-ctl);background:var(--accent);color:#fff;border:none;font-weight:600;font-size:13px;opacity:0;cursor:pointer;z-index:1}.add-btn:hover:not(:disabled){background:var(--accent-strong);border-color:transparent}.row:hover .add-btn,.cell:hover .add-btn{opacity:1}.add-btn.static{position:static;display:inline-flex;opacity:1;width:16px;height:16px;font-size:11px;vertical-align:-3px}.foldc{flex:0 0 14px;position:relative;-webkit-user-select:none;user-select:none}.fold-btn{position:absolute;inset:0;height:auto;display:flex;align-items:center;justify-content:center;padding:0;border:0;background:none;color:var(--muted);cursor:pointer;opacity:0}.fold-btn svg{transform:rotate(90deg);transition:transform .1s}.fold-btn.shut svg{transform:none}.row.line:hover .fold-btn,.cell:hover .fold-btn,.fold-btn.shut,.fold-btn:focus-visible{opacity:1}.fold-btn:hover:not(:disabled){background:none;color:var(--ink)}.fold-chip{margin-left:8px;padding:0 6px;border:1px solid var(--line);border-radius:var(--r-ctl);background:var(--surface-2);color:var(--muted);font-family:var(--sans);font-size:var(--t-meta);line-height:14px;cursor:pointer;display:inline-block;height:16px;vertical-align:top;margin-top:2px}.fold-chip:hover{border-color:var(--line-strong);color:var(--ink)}.fold-chip .add{color:var(--add-fg)}.fold-chip .del{color:var(--del-fg)}.marker{flex:0 0 14px;text-align:center;-webkit-user-select:none;user-select:none;color:var(--muted)}.code{flex:1;min-width:0;white-space:pre-wrap;word-break:break-all;padding-right:40px}.nonl{color:var(--del-fg);margin-left:6px}.row.line.add,.cell.add{background:var(--add-bg)}.row.line.del,.cell.del{background:var(--del-bg)}.row.line.add .marker{color:var(--add-fg)}.row.line.del .marker{color:var(--del-fg)}.row.expanded,.row.pair.expanded .cell{background:var(--surface);color:var(--muted)}.row.line.selected,.cell.selected{background:var(--accent-soft)!important}.row.line.focused,.cell.focused{background:var(--accent-soft)!important;box-shadow:inset 3px 0 var(--accent)}.row.line.flash,.cell.flash{animation:flash 1.6s ease-out}@keyframes flash{0%{background:var(--accent-line)}to{background:transparent}}.row.pair{display:grid;grid-template-columns:1fr 1fr}.cell{display:flex;min-width:0;border-left:1px solid var(--line)}.cell:first-child{border-left:none}.cell.blank{background:var(--surface-2)}.diff-list.split .row.hunk-head{padding-left:78px}.comment-card{position:relative;background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:var(--r-card);padding:7px 10px 8px}.comment-card.status-exported{border-left-color:var(--line-strong)}.comment-card.status-orphaned{border-color:var(--warn-line);border-left-style:dashed;background:var(--warn-soft)}.comment-card.selected{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.comment-head{display:flex;align-items:center;gap:8px;min-height:20px;font-size:12px;color:var(--ink-2);flex-wrap:wrap}.comment-head .mono{font-size:11px}.comment-head .time{font-size:11px;color:var(--muted)}.card-actions{position:absolute;right:6px;top:5px;display:inline-flex;align-items:center;gap:2px;padding-left:8px;border-radius:var(--r-ctl);background:inherit;opacity:0;transition:opacity .1s}.card-actions .link{font-size:12px}.comment-card:hover .card-actions,.comment-card:focus-within .card-actions,.comment-card.reattaching .card-actions{opacity:1}.comment-editor{background:var(--surface);border:1px solid var(--accent);border-left-width:3px;border-radius:var(--r-card);padding:8px 10px;max-width:900px}.comment-editor-head{font-family:var(--mono);font-size:11px;color:var(--muted);margin-bottom:6px}.comment-editor textarea{border-color:var(--line)}.comment-editor-actions{display:flex;gap:6px;margin-top:8px}.comment-editor .primary{background:var(--accent);border-color:var(--accent)}.comment-editor .primary:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}.snippet{font-family:var(--mono);font-size:11.5px;line-height:18px;background:var(--surface-2);border-radius:var(--r-ctl);padding:4px 8px;margin:6px 0;overflow-x:auto;white-space:pre}.snippet .ln{display:inline-block;width:40px;color:var(--muted);text-align:right;margin-right:10px;-webkit-user-select:none;user-select:none}.md{font-size:13px}.md p{margin:4px 0}.md pre{background:var(--surface-2);padding:6px 8px;border-radius:var(--r-ctl);overflow-x:auto}.md code{font-family:var(--mono);font-size:12px;background:var(--surface-2);padding:0 3px;border-radius:3px}.md pre code{background:none;padding:0}.md ul,.md ol{padding-left:20px;margin:4px 0}.md blockquote{margin:4px 0;padding-left:10px;border-left:3px solid var(--line);color:var(--muted)}.commits-panel{flex:1;display:flex;flex-direction:column;min-height:0}.commits-panel[hidden]{display:none}.commits-list{flex:1;overflow:auto;background:var(--surface)}.commit-day{position:sticky;top:0;z-index:1;display:flex;align-items:center;height:24px;padding:0 12px;background:var(--ground);border-bottom:1px solid var(--line);color:var(--ink-2);font-size:12px;font-weight:500}.commit-row{display:flex;align-items:center;gap:10px;height:30px;padding:0 12px;border-bottom:1px solid var(--surface-3);cursor:pointer;white-space:nowrap}.commit-row:hover{background:var(--surface-2)}.commit-row.active{background:var(--surface-2);box-shadow:inset 2px 0 var(--ink)}.commit-row .sha{flex:0 0 auto;min-width:7ch;color:var(--ink-2)}.commit-row .subject{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.commit-row .subject .pr{margin-left:6px;color:var(--muted);font-family:var(--mono);font-size:12px}.commit-row .refs{display:inline-flex;gap:4px;flex:0 1 auto;min-width:0;overflow:hidden}.ref{display:inline-block;max-width:180px;height:17px;padding:0 7px;border:1px solid var(--line-strong);border-radius:9px;color:var(--ink-2);font-family:var(--mono);font-size:11px;line-height:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ref-tag{background:var(--surface-3);border-color:var(--surface-3)}.ref-head{background:var(--ink);border-color:var(--ink);color:#fff}.ref-more{border-style:dashed;color:var(--muted)}.ref-fork{border-style:dashed;border-color:var(--ink-2);color:var(--ink)}.commit-row.fork{border-top:1px dashed var(--line-strong)}.fork-note{font-size:var(--t-meta);line-height:1.6}.fork-note .link{padding:0;font-size:var(--t-meta)}.commit-row .author{flex:0 0 110px;overflow:hidden;text-overflow:ellipsis;color:var(--ink-2);font-size:12px}.commit-row .time{flex:0 0 40px;text-align:right;color:var(--muted);font-family:var(--mono);font-size:11px}.commits-foot{display:flex;justify-content:center;align-items:center;min-height:44px;font-size:12px}.worktrees-panel{flex:1;display:flex;flex-direction:column;min-height:0;background:var(--surface)}.wt-list{flex:1;overflow:auto;max-width:740px;padding:16px 20px 24px;display:flex;flex-direction:column;gap:8px}.wt-row{display:flex;align-items:flex-start;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:var(--r-card);background:var(--surface)}.wt-row.active{border-color:var(--line-strong);box-shadow:inset 3px 0 var(--ink)}.wt-row.gone .wt-name{color:var(--muted);text-decoration:line-through}.wt-row.free{border-style:dashed;background:var(--surface-2)}.wt-row.free .wt-name{color:var(--ink-2)}.wt-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.wt-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.wt-name{font-weight:500;overflow-wrap:anywhere}.wt-path{font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wt-actions{display:flex;align-items:center;gap:2px;flex:0 0 auto}.wt-comparison{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12px}.wt-commit-count{display:inline-flex;align-items:center;gap:3px;font-variant-numeric:tabular-nums}.wt-extras{display:flex;align-items:center;flex-wrap:wrap;gap:4px 10px}.wt-extras>.row-actions{margin:0;gap:2px}.wt-expanded{flex-basis:100%;min-width:0;margin-top:4px;padding:12px;background:var(--surface-2);border-radius:var(--r-card);overflow-wrap:anywhere}.wt-commit{display:flex;align-items:baseline;gap:10px;width:100%;padding:8px 0;border:0;border-bottom:1px solid var(--line);background:transparent;text-align:left;white-space:normal}.wt-commit>span{flex:1;min-width:0}.wt-commit small{color:var(--muted)}.wt-todo{padding:8px 0;border-bottom:1px solid var(--line)}.wt-todo summary{display:flex;align-items:center;gap:8px;cursor:pointer}.wt-todo.done summary>span:last-child{text-decoration:line-through;color:var(--muted)}.wt-tmux>*+*{margin-top:8px}@media(max-width:650px){.wt-commit{flex-wrap:wrap}}.wt-confirm{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:6px;padding:7px 9px;border-radius:var(--r-ctl);background:var(--warn-soft);border:1px solid var(--warn-line);color:var(--warn-fg);font-size:12px}.wt-confirm .link{color:var(--warn-fg)}.wt-confirm .link.danger{color:var(--del-fg)}.wt-empty{padding:20px 2px}.rail-list.tasks{padding:4px 0 12px;gap:0}.task-add{height:34px;margin:0 4px 2px;padding:0 10px 0 24px;justify-content:flex-start;gap:10px;border:none;border-radius:var(--r-card);background:none;color:var(--ink-2);font-weight:500}.task-add:hover:not(:disabled){background:var(--surface-3);border-color:transparent;color:var(--ink)}.task-add-plus{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background:var(--ink);color:#fff;font-size:15px;line-height:1}.task-list{display:flex;flex-direction:column}.task-row{position:relative;display:flex;align-items:flex-start;gap:6px;margin:0 4px;padding:5px 10px 5px 4px;border-radius:var(--r-card)}.task-row:hover,.task-row.open{background:var(--surface)}.task-row.open{box-shadow:0 0 0 1px var(--line)}.task-grip{flex:0 0 14px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--muted);cursor:grab;visibility:hidden}.task-row:hover .task-grip{visibility:visible}.task-list.dragging .task-grip{visibility:hidden}.task-check{position:relative;flex:0 0 18px;width:18px;height:18px;margin-top:1px;padding:0;border:1.5px solid var(--ink-2);border-radius:50%;background:var(--surface)}.task-check:after{content:"";position:absolute;left:5px;top:1.5px;width:4px;height:8px;border:solid var(--ink);border-width:0 1.5px 1.5px 0;transform:rotate(45deg);opacity:0}.task-check:hover:not(:disabled){background:var(--surface);border-color:var(--ink)}.task-check:hover:not(:disabled):after{opacity:.45}.task-row.done .task-check,.task-row.completing .task-check{background:var(--ink);border-color:var(--ink)}.task-row.done .task-check:after,.task-row.completing .task-check:after{border-color:#fff;opacity:1}.task-row.draft .task-check{border-style:dashed;border-color:var(--muted)}.task-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.task-line{display:flex;align-items:flex-start;gap:6px}.task-ta{width:100%;min-width:0;height:auto;padding:0;border:none;border-radius:0;background:transparent;resize:none;overflow:hidden;font-family:var(--sans);line-height:20px}.task-ta:focus{box-shadow:none}.task-title{flex:1;font-size:13px}.task-body{font-size:12px;line-height:18px;color:var(--ink-2)}.task-details{font-size:12px;line-height:18px;color:var(--muted);cursor:text}.task-details .md{font-size:12px}.task-details .md p{margin:0 0 2px}.task-row.done .task-title,.task-row.completing .task-title{text-decoration:line-through;color:var(--muted)}.task-row.completing{opacity:.55;transition:opacity .3s}.task-row .badge{margin-top:2px}.task-count{font-family:var(--mono);font-size:11px;line-height:20px;color:var(--muted);white-space:nowrap}.task-actions{display:inline-flex;align-items:center;gap:2px;margin-left:auto;line-height:20px;white-space:nowrap;visibility:hidden}.task-actions .link{font-size:12px}.task-row:hover .task-actions,.task-row:focus-within .task-actions,.task-sub:hover .task-actions{visibility:visible}.task-row.dragging{opacity:.4}.task-row.drop-before{box-shadow:0 -2px 0 0 var(--ink)}.task-row.drop-after{box-shadow:0 2px 0 0 var(--ink)}.task-empty{padding:14px 20px;color:var(--muted);font-size:12px}.task-add-hint{padding:0 10px 6px 46px;font-size:12px;color:var(--muted)}.task-done{margin-top:8px;padding-top:4px;border-top:1px solid var(--line)}.task-done-head{display:flex;align-items:center;min-height:28px;padding:0 14px 0 10px}.task-done-toggle{height:auto;padding:0 4px;gap:6px;border:none;background:none;color:var(--ink-2);font-weight:500}.task-done-toggle:hover:not(:disabled){background:none;border-color:transparent;color:var(--ink)}.task-list-pick{max-width:220px}.task-subs{display:flex;flex-direction:column;gap:4px;margin-top:6px}.task-sub{display:flex;align-items:flex-start;gap:8px;padding:4px 6px;border-radius:var(--r-ctl);background:var(--surface-2);font-size:12px}.task-sub-dot{flex:0 0 8px;width:8px;height:8px;margin-top:6px;border-radius:50%;background:var(--accent)}.task-sub.status-exported .task-sub-dot{background:var(--line-strong)}.task-sub.status-orphaned .task-sub-dot{background:var(--warn-line)}.task-sub-main{flex:1;min-width:0}.task-sub-head{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.task-sub-head .mono{font-size:11px}.task-sub-body{color:var(--muted);white-space:pre-wrap;max-height:54px;overflow:hidden}.task-sub-add{align-self:flex-start;font-size:12px}.task-sub-none{font-size:12px;color:var(--muted)}.toast{position:fixed;bottom:20px;left:50%;transform:translate(-50%);background:var(--ink);color:#fff;padding:8px 16px;border-radius:var(--r-card);box-shadow:0 6px 20px #16181d40;z-index:100;max-width:80vw}.toast-error{background:var(--del-fg)}.toast-action{height:22px;margin-left:14px;padding:0 8px;background:#ffffff24;border-color:#ffffff59;color:#fff;font-weight:500}.toast-action:hover:not(:disabled){background:#ffffff42;border-color:#ffffff80}.rail{width:360px;flex:none;border-left:1px solid var(--line);display:flex;flex-direction:column;background:var(--ground);min-height:0}.rail.rail-wide{width:440px}.rail-head{display:flex;align-items:center;gap:10px;padding:0 10px;min-height:36px;border-bottom:1px solid var(--line)}.rail-tools{display:flex;align-items:center;gap:8px;padding:0 10px;min-height:34px;border-bottom:1px solid var(--line);font-size:12px}.rail-notice{padding:6px 10px;background:var(--warn-bg);color:var(--warn-fg);border-bottom:1px solid var(--warn-line);display:flex;gap:8px;align-items:center;font-size:12px}.rail-notice .link{color:var(--warn-fg)}.rail-list{flex:1;overflow:auto;padding:10px;display:flex;flex-direction:column;gap:8px}.rail-slot{display:flex;flex-direction:column;gap:8px}.rail-foot{display:flex;flex-direction:column;gap:8px;padding:10px;border-top:1px solid var(--line);background:var(--ground)}.rail-foot-row{display:flex;align-items:center;gap:10px}.send{height:30px;padding:0 14px;background:var(--accent);border-color:var(--accent);color:#fff;font-weight:500}.send:hover:not(:disabled){background:var(--accent-strong);border-color:var(--accent-strong)}.send-n{font-family:var(--mono);font-size:11px;font-weight:400;line-height:16px;padding:0 5px;border-radius:var(--r-ctl);background:#ffffff38}.rail .comment-card{cursor:pointer}.rail .comment-card.focused{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.comment-head .file{color:var(--muted);max-width:120px;overflow:hidden;text-overflow:ellipsis}.rail .comment-editor{max-width:none}@media(prefers-reduced-motion:reduce){.chev,.card-actions,.task-row.completing,.progress-track>span{transition:none}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}.action-icon{display:inline-flex;align-items:center;justify-content:center;gap:5px;vertical-align:middle;line-height:1}.action-icon>svg{flex-shrink:0}button:has(>.action-icon){display:inline-flex;align-items:center;justify-content:center;min-width:30px;min-height:30px;padding:6px}button.link:has(>.action-icon){color:var(--muted);border-radius:5px}button.link:has(>.action-icon):hover:not(:disabled){color:var(--accent);background:var(--accent-soft);text-decoration:none}button.link.danger:has(>.action-icon){color:var(--del-fg)}button.link.danger:has(>.action-icon):hover:not(:disabled){background:var(--del-bg)}.nvim>span{display:inline-flex;align-items:center;padding:4px}.nvim{position:relative}.nvim summary{display:flex;align-items:center;justify-content:center;width:30px;height:30px;border:1px solid var(--line);border-radius:6px;cursor:pointer;list-style:none}.nvim summary::-webkit-details-marker{display:none}.nvim summary:hover{background:var(--surface-3)}.nvim summary:focus-visible{outline:1px solid var(--accent)}.nvim[open] summary{background:var(--accent-soft);color:var(--accent)}.nvim-popover{position:absolute;z-index:30;left:0;top:calc(100% + 6px);display:flex;align-items:center;gap:8px;width:320px;max-width:calc(100vw - 32px);padding:12px;background:var(--surface);border:1px solid var(--line);border-radius:8px;box-shadow:0 4px 16px #1f23281a}.nvim-popover>:first-child{flex:1;min-width:0;overflow-wrap:anywhere}.tree-icon{flex:0 0 16px;color:var(--muted)}.tree-icon-folder{color:#8c9aab;fill:#8c9aab12}.tree-row{display:flex;align-items:center;gap:6px;padding:0 8px;min-height:32px;border-radius:5px;cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none;line-height:20px}.tree-row.active{background:#eaf3ff;box-shadow:inset 1px 0 var(--accent)}.tree-row.active .tree-file-open{color:var(--accent)}.tree-row:focus-within{outline:1px solid var(--accent);outline-offset:-1px}.tree-row.dir .name{font-weight:500}.tree-row.dir{width:100%;border:0;background:transparent;text-align:left;color:var(--muted)}.tree-row.dir:hover{background:var(--surface-2)}.tree-row.dir.viewed .name{font-weight:400;color:var(--muted)}.tree-row.dir.viewed .tree-icon-folder{opacity:.55}.tree-row.dir.viewed .count{display:inline-flex;align-items:center;gap:3px}.tree-row.dir.viewed .count .tree-icon{color:var(--add-fg);width:12px;height:12px;flex-basis:12px}.tree-row .status{display:inline-flex;align-items:center;justify-content:center;flex:0 0 14px;height:18px;font-family:var(--sans);font-size:11px;font-weight:500;line-height:1}.comment-editor-head{font-family:var(--mono);font-size:12px;color:var(--muted);margin:-8px -10px 8px;padding:8px 19px;background:var(--surface-2);border-bottom:1px solid var(--line);border-radius:5px 5px 0 0}.block-heading{display:flex;align-items:center;gap:7px;min-height:26px}.block-summary{display:flex;align-items:center;gap:10px;margin-top:5px;font-size:10px;font-variant-numeric:tabular-nums}.tree-tools{display:inline-flex;gap:2px}.tree-tools button{display:grid;place-items:center;width:26px;height:26px;padding:0;border:none;background:transparent;color:var(--muted)}.tree-empty{display:flex;align-items:center;gap:8px;padding:16px 10px;color:var(--muted);font-size:12px}.block-head{flex-shrink:0;padding:9px 12px 10px;border-bottom:1px solid var(--line);background:#f8fafc;font-size:12px;white-space:nowrap}.block-indicator{width:6px;height:6px;border-radius:50%;background:#bf8700;flex-shrink:0}.block-indicator.is-staged{background:var(--add-fg)}.block-warn{padding:0 6px;border:1px solid var(--warn-line);border-radius:5px;background:var(--warn-soft);color:var(--warn-fg);font-size:10px;font-weight:600;line-height:17px;white-space:nowrap}.side-toggle{flex:none;display:flex;align-items:center;gap:6px;padding:0 12px 8px;color:var(--ink-2);font-size:var(--t-meta);cursor:pointer}.block-file-count{padding:0 6px;border:1px solid var(--line);border-radius:5px;color:var(--muted);font-size:10px;line-height:17px;font-variant-numeric:tabular-nums}.block-diff-counts{display:flex;gap:7px;font-family:var(--mono)}.action-count{font-size:10px;font-weight:500;font-variant-numeric:tabular-nums;line-height:1}.icon-spinning{animation:icon-spin 1s linear infinite}.tree-file-icon{display:inline-flex;width:16px;height:16px;flex:0 0 16px}.tree-file-icon svg{width:100%;height:100%}.tree-file-open{display:flex;align-items:center;gap:7px;flex:1;min-width:0;align-self:stretch;padding:0;border:0;border-radius:0;background:transparent;text-align:left}.tree-file-open:hover:not(:disabled){background:transparent}.tree-file-open:focus-visible{outline:none}.tree-row .name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.tree-row .counts{margin-left:auto;font-family:var(--mono);font-size:11px;display:inline-flex;gap:6px;flex-shrink:0;font-variant-numeric:tabular-nums;min-width:48px;justify-content:flex-end}.tree-row .count{font-size:11px;margin-left:auto;padding:0 4px}.tree-changed{width:6px;height:6px;flex:0 0 6px;border-radius:50%;background:#bf8700}.tree-binary{color:var(--muted);font-size:9px;letter-spacing:.04em}.tree-block .block-head{display:block}.tree-tools{opacity:1;margin-left:auto}