agent-feedbacker 0.2.0 → 0.3.1

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,8 +1,8 @@
1
1
  {
2
2
  "name": "feedbacker",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Review an agent's changes across several git repos in a local MR-style page and send line comments back to it.",
5
5
  "author": { "name": "Pavel Polivka" },
6
- "repository": "https://github.com/PavlikPolivka/feedbacker",
6
+ "repository": "https://github.com/polivkadev/feedbacker",
7
7
  "license": "MIT"
8
8
  }
package/README.md CHANGED
@@ -14,7 +14,7 @@ A local, merge-request-style review page for coding agents. The agent passes the
14
14
  ### Claude Code plugin
15
15
 
16
16
  ```bash
17
- claude plugin marketplace add PavlikPolivka/feedbacker
17
+ claude plugin marketplace add polivkadev/feedbacker
18
18
  claude plugin install feedbacker@feedbacker
19
19
  ```
20
20
 
@@ -48,8 +48,9 @@ The npm package is `agent-feedbacker`; the command is `feedbacker`. Node 22 or n
48
48
 
49
49
  ```bash
50
50
  feedbacker ~/Projects/api ~/Projects/web@main \
51
- -m "Bind the API to localhost; document the port" \
52
- -q "Should the port also be a CLI flag?"
51
+ -m $'Bind the API to localhost.\n\n- New `loadConfig()` reads HOST and PORT\n- README documents the port' \
52
+ -f 'src/server.ts:5-12=Where the host is bound' \
53
+ -f 'src/config.ts=Defaults live here'
53
54
  ```
54
55
 
55
56
  - `repo` diffs the working tree (staged, unstaged and untracked files) against `HEAD`.
@@ -65,9 +66,11 @@ feedbacker ~/Projects/api ~/Projects/web@main \
65
66
 
66
67
  In the browser:
67
68
 
69
+ - The agent's summary is short markdown, with a lead sentence and a list. Under it, **Where to look** lists the places the agent thinks matter most; each links to the file, and the agent's note also appears in the diff above those lines.
70
+ - Files are sorted by kind: the agent's focus first, then **Code**, then **Tests**, **Config**, **Docs** and **Generated** (lock files, build output, minified and binary files). The kind comes from the path and works across languages (`*.spec.ts`, `*_test.go`, `test_*.py`, `FooTest.java`, `__tests__/`, `CLAUDE.md`, …). Only Code starts expanded; the chips above the diffs open or close a whole kind.
68
71
  - In changed lines, the changed words are marked.
69
72
  - Tick **Viewed** on a file when you're done with it: it collapses, dims in the file tree, and the top bar counts how many you've viewed.
70
- - Click a line number to comment on that line (a highlighter "+" shows where); shift-click a second one to comment on a range. Ctrl+Enter saves, Esc cancels.
73
+ - Click a line number to comment on that line (a highlighter "+" shows where); shift-click a second one to cover a range. Then **Comment** (something to change) or **Ask** (a question the agent must answer). Ctrl+Enter comments, Ctrl+Shift+Enter asks, Esc cancels.
71
74
  - With several repos, the picker in the top bar switches between all of them and a single one (the summary, questions and your comments in other repos stay). The top bar also shows the size of the change and keeps **Approve** and **Request changes** in reach; files can be collapsed, and the file tree shows where your comments are.
72
75
  - Comments, answers and the general comment are kept in the browser's local storage until you submit, so a refresh loses nothing.
73
76
  - **Approve** or **Request changes** sends everything to the agent and closes the server.
@@ -79,8 +82,9 @@ In the browser:
79
82
  | Argument | Type | |
80
83
  |---|---|---|
81
84
  | `repos` | `[{ path, base?, worktrees? }]` | Required. Absolute paths; `base` defaults to `HEAD` (or, with the plugin's hooks, where the repo stood when the session started); `worktrees: true` adds all worktrees of that repo. (`exact` and `session` are set by the hook.) |
82
- | `summary` | string | Shown at the top of the page. |
83
- | `questions` | string[] | Shown with an answer box each. |
85
+ | `summary` | string | Markdown: paragraphs, `- ` lists, `` `code` ``, `**bold**`, `### ` subheadings. The first paragraph is the lead. |
86
+ | `focus` | `[{ path, note, startLine?, endLine?, repo? }]` | Where the user should look first, and why. `path` is relative to its repo or absolute. |
87
+ | `questions` | string[] | Only for a decision the agent can't make alone; usually left out. |
84
88
 
85
89
  It blocks until the user submits (Claude Code keeps long MCP calls open; it sends progress notifications every 30 s for clients that need them) and returns markdown like:
86
90
 
@@ -97,11 +101,19 @@ Reviewed:
97
101
 
98
102
  Almost there.
99
103
 
100
- ## Answers
104
+ ## Questions from the reviewer
101
105
 
102
- **Q: Should the port also be a CLI flag?**
106
+ The reviewer asks about this code. Answer each one in your reply. Change code only where a question shows something is wrong, and say so.
103
107
 
104
- No, env is enough.
108
+ ### /home/me/Projects/api
109
+
110
+ **src/config.ts:4** (new side)
111
+
112
+ ```diff
113
+ + host: env.HOST ?? "127.0.0.1",
114
+ ```
115
+
116
+ Why not 0.0.0.0 inside containers?
105
117
 
106
118
  ## Comments
107
119
 
@@ -161,7 +173,7 @@ Publishing to npm is done by GitHub Actions (`.github/workflows/publish.yml`) wi
161
173
  2. Create a GitHub release with the tag `vX.Y.Z` matching that version: `gh release create v0.2.0 --generate-notes`.
162
174
  3. The workflow checks the tag, runs the tests and publishes.
163
175
 
164
- One-time setup: on npmjs.com, add a trusted publisher for the package (GitHub Actions, repository `PavlikPolivka/feedbacker`, workflow `publish.yml`). npm only offers that once the package exists, so the very first version is published by hand with `npm publish --access public`.
176
+ One-time setup: on npmjs.com, add a trusted publisher for the package (GitHub Actions, repository `polivkadev/feedbacker`, workflow `publish.yml`). npm only offers that once the package exists, so the very first version is published by hand with `npm publish --access public`.
165
177
 
166
178
  Plans: [ROADMAP.md](ROADMAP.md).
167
179
 
@@ -19,7 +19,8 @@ and prints the feedback to stdout when the user submits.
19
19
  branch as base, the diff starts where HEAD forked from it.
20
20
  -w, --worktrees Also review every other worktree of each repo.
21
21
  -m, --summary TXT What the agent changed, shown at the top.
22
- -q, --question TXT A question for the user (repeatable).
22
+ -f, --focus SPEC Where to look first: "path[:start[-end]]=why" (repeatable).
23
+ -q, --question TXT A decision you need from the user (repeatable; rarely).
23
24
  --json Print JSON instead of markdown.
24
25
  --port N Port to listen on (default: random).
25
26
  --no-open Don't open the browser; only print the URL to stderr.
@@ -36,8 +37,16 @@ function fail(msg) {
36
37
  process.exit(1);
37
38
  }
38
39
 
40
+ // "src/core.mjs:120-160=Merge logic" -> { path, startLine, endLine, note }
41
+ function parseFocus(spec) {
42
+ const eq = spec.indexOf('=');
43
+ const where = eq < 0 ? spec : spec.slice(0, eq);
44
+ const m = where.match(/^(.*?)(?::(\d+)(?:-(\d+))?)?$/);
45
+ return { path: m[1], startLine: Number(m[2]) || 0, endLine: Number(m[3] || m[2]) || 0, note: eq < 0 ? '' : spec.slice(eq + 1) };
46
+ }
47
+
39
48
  function parseArgs(argv) {
40
- const opts = { repos: [], questions: [], summary: '', worktrees: false, json: false, port: 0, open: true };
49
+ const opts = { repos: [], questions: [], focus: [], summary: '', worktrees: false, json: false, port: 0, open: true };
41
50
  for (let i = 0; i < argv.length; i++) {
42
51
  const a = argv[i];
43
52
  const next = () => {
@@ -47,6 +56,7 @@ function parseArgs(argv) {
47
56
  if (a === '-h' || a === '--help') { console.log(USAGE); process.exit(0); }
48
57
  else if (a === '-m' || a === '--summary') opts.summary = next();
49
58
  else if (a === '-q' || a === '--question') opts.questions.push(next());
59
+ else if (a === '-f' || a === '--focus') opts.focus.push(parseFocus(next()));
50
60
  else if (a === '-w' || a === '--worktrees') opts.worktrees = true;
51
61
  else if (a === '--json') opts.json = true;
52
62
  else if (a === '--port') opts.port = Number(next());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-feedbacker",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A local merge-request-style review page that coding agents open to collect your line comments and answers, across several git repos.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "git+https://github.com/PavlikPolivka/feedbacker.git"
28
+ "url": "git+https://github.com/polivkadev/feedbacker.git"
29
29
  },
30
30
  "keywords": [
31
31
  "claude-code",
@@ -49,8 +49,8 @@
49
49
  "esbuild"
50
50
  ]
51
51
  },
52
- "homepage": "https://github.com/PavlikPolivka/feedbacker#readme",
52
+ "homepage": "https://github.com/polivkadev/feedbacker#readme",
53
53
  "bugs": {
54
- "url": "https://github.com/PavlikPolivka/feedbacker/issues"
54
+ "url": "https://github.com/polivkadev/feedbacker/issues"
55
55
  }
56
56
  }
@@ -5,7 +5,7 @@ description: Ask the user to review your changes in a local merge-request-style
5
5
 
6
6
  # Review with Feedbacker
7
7
 
8
- Feedbacker opens a review page in the user's browser with the diffs of the repos you pass. The user comments on lines, answers your questions and clicks **Approve** or **Request changes**. You get their feedback back as markdown.
8
+ Feedbacker opens a review page in the user's browser with the diffs of the repos you pass. The user comments on lines, asks you questions about the code, and clicks **Approve** or **Request changes**. You get their feedback back as markdown.
9
9
 
10
10
  ## How
11
11
 
@@ -13,8 +13,9 @@ Feedbacker opens a review page in the user's browser with the diffs of the repos
13
13
  2. Pick a base per repo, usually by leaving it out: the plugin's hooks recorded where each repo stood when this session first touched it, and fill that in, so the review shows exactly what you changed in this session, commits included. Only pass `base` for something else, e.g. a whole branch against `main`. The result starts with a "Reviewed" list showing what the user saw.
14
14
  3. Call the `request_review` tool of the `feedbacker` MCP server with:
15
15
  - `repos`: `[{ "path": "/abs/path", "base": "main", "worktrees": false }, ...]`
16
- - `summary`: two to five sentences on what you changed and why.
17
- - `questions`: only real decisions you want the user to make (optional).
16
+ - `summary`: markdown. One or two sentences on what changed and why, then a short `- ` list of the notable parts. Use `### ` subheadings only if the change has distinct parts. No walls of text.
17
+ - `focus`: two to six places the user should look first, each `{ "path": "src/x.ts", "startLine": 40, "endLine": 80, "note": "why it matters" }`. Pick the main logic and anything risky or non-obvious, not tests, docs or config (the page collapses those anyway).
18
+ - `questions`: leave this out. Only add one if you are blocked on a decision you truly cannot make; the user asks *you* questions on the page instead.
18
19
  4. Before the call, tell the user a review page is opening in their browser. If it doesn't appear, they can run `node "${CLAUDE_PLUGIN_ROOT}/bin/feedbacker.mjs" list` to get its URL. The tool returns once they submit, however long that takes.
19
20
 
20
21
  If the MCP tool isn't available, run the CLI in the background instead, so the shell timeout doesn't kill it, and read its output when it exits:
@@ -26,6 +27,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/feedbacker.mjs" /abs/repo-a /abs/repo-b@main \
26
27
 
27
28
  ## After the review
28
29
 
30
+ - **Questions from the reviewer:** answer every one in your reply, quoting the file and line. Change code only if a question shows something is wrong, and say that you did.
29
31
  - **Changes requested:** address every comment. Each one names the repo, file, line range and the code it refers to. Answer comments you disagree with instead of silently skipping them. Then offer another review round.
30
32
  - **Approved:** carry on (commit, push, or whatever was next), taking any comments and answers into account.
31
33
  - Use the answers to your questions as decisions; don't ask them again.
package/src/core.mjs CHANGED
@@ -122,7 +122,74 @@ export function loadRepo(spec) {
122
122
 
123
123
  // `worktrees` (globally or per repo) expands each repo into all its worktrees,
124
124
  // e.g. to review parallel agents' work together. Duplicates are dropped.
125
- export function buildReview({ repos = ['.'], summary = '', questions = [], worktrees = false }) {
125
+ // What kind of file a path is, for the page's quick filters. Works on paths
126
+ // only, for any language: tests by folder or file-name convention, docs and
127
+ // agent instructions (CLAUDE.md, AGENTS.md), config, and generated files
128
+ // (lock files, build output, minified, snapshots, binaries).
129
+ const TEST_DIRS = /(^|\/)(tests?|__tests__|__mocks__|specs?|testdata|test-data|fixtures|e2e|testing)\//i;
130
+ const TEST_FILES = [
131
+ /\.(test|spec|e2e|cy|stories|story)\.[^/]+$/i, // app.test.ts, app.spec.js, x.e2e.ts, x.cy.ts, x.stories.tsx
132
+ /(^|\/)test_[^/]+\.py$/i, /_test\.(py|go|rb|exs?|dart|cpp|cc|c|rs)$/i, // test_x.py, x_test.go
133
+ /(Tests?|Spec|IT)\.(java|kt|kts|scala|groovy|cs|fs|vb|swift|php)$/, // FooTest.java, FooTests.cs
134
+ /_spec\.rb$/i, /\.t$/, // foo_spec.rb, Perl foo.t
135
+ ];
136
+ const GENERATED = [
137
+ /(^|\/)(package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Cargo\.lock|go\.sum|poetry\.lock|uv\.lock|Pipfile\.lock|Gemfile\.lock|composer\.lock|mix\.lock|flake\.lock|Podfile\.lock|packages\.lock\.json)$/,
138
+ /\.min\.(js|css)$/i, /\.map$/i, /\.snap$/i, /\.lock$/i,
139
+ /(^|\/)(dist|build|out|vendor|node_modules|generated|__generated__|\.next|target)\//,
140
+ /\.(pb\.go|g\.dart|generated\.\w+|designer\.cs)$/i, /_pb2\.py$/,
141
+ ];
142
+ const DOCS = [
143
+ /\.(md|mdx|markdown|rst|adoc|txt|org)$/i,
144
+ /(^|\/)(docs?|documentation)\//i,
145
+ /(^|\/)(LICEN[CS]E|COPYING|NOTICE|AUTHORS|CHANGELOG|CHANGES|CODEOWNERS)(\.\w+)?$/i,
146
+ ];
147
+ const CONFIG = [
148
+ /\.(json|jsonc|json5|ya?ml|toml|ini|cfg|conf|properties|env|editorconfig|xml|plist)$/i,
149
+ /(^|\/)\.[^/]+$/, // dotfiles: .gitignore, .npmrc, .eslintrc
150
+ /(^|\/)\.(github|gitlab|circleci|vscode|idea|devcontainer|husky)\//,
151
+ /(^|\/)(Dockerfile|Containerfile|docker-compose[^/]*|Makefile|Justfile|Procfile|Brewfile)$/i,
152
+ ];
153
+
154
+ export function classifyFile(file) {
155
+ const p = file.path || '';
156
+ if (file.binary) return 'generated';
157
+ if (GENERATED.some((re) => re.test(p))) return 'generated';
158
+ if (TEST_DIRS.test(p) || TEST_FILES.some((re) => re.test(p))) return 'test';
159
+ if (DOCS.some((re) => re.test(p))) return 'docs';
160
+ if (CONFIG.some((re) => re.test(p))) return 'config';
161
+ return 'code';
162
+ }
163
+
164
+ // The agent's pointers to what matters: [{ path, repo?, startLine?, endLine?,
165
+ // note }]. `path` is relative to its repo or absolute. Each match is attached
166
+ // to its file; review.focus keeps them in the agent's order for the page.
167
+ function attachFocus(repos, focus) {
168
+ const out = [];
169
+ for (const raw of Array.isArray(focus) ? focus : []) {
170
+ const item = typeof raw === 'string' ? { path: raw } : raw || {};
171
+ const note = typeof item.note === 'string' ? item.note : '';
172
+ const want = String(item.path || '').replace(/^\.\//, '');
173
+ if (!want) continue;
174
+ const startLine = Number(item.startLine) || 0;
175
+ const endLine = Number(item.endLine) || startLine;
176
+ let hit = null;
177
+ repos.forEach((repo, ri) => {
178
+ if (hit || (item.repo && path.resolve(item.repo) !== repo.path && path.basename(repo.path) !== item.repo)) return;
179
+ const rel = path.isAbsolute(want) ? path.relative(repo.path, want) : want;
180
+ const fi = repo.files.findIndex((f) => f.path === rel || f.oldPath === rel);
181
+ if (fi >= 0) hit = { ri, fi };
182
+ });
183
+ // Matched entries show the repo-relative path, however the agent wrote it.
184
+ const shown = hit ? repos[hit.ri].files[hit.fi].path : want;
185
+ const entry = { path: shown, note, startLine, endLine, ...(hit || { ri: -1, fi: -1 }) };
186
+ if (hit) (repos[hit.ri].files[hit.fi].focus ||= []).push({ note, startLine, endLine });
187
+ out.push(entry);
188
+ }
189
+ return out;
190
+ }
191
+
192
+ export function buildReview({ repos = ['.'], summary = '', questions = [], focus = [], worktrees = false }) {
126
193
  const specs = [];
127
194
  for (const raw of repos) {
128
195
  const spec = parseSpec(raw);
@@ -141,7 +208,8 @@ export function buildReview({ repos = ['.'], summary = '', questions = [], workt
141
208
  ? path.join(path.basename(path.dirname(r.path)), name(r))
142
209
  : name(r);
143
210
  }
144
- return { summary, questions, repos: loaded };
211
+ for (const r of loaded) for (const f of r.files) f.kind = classifyFile(f);
212
+ return { summary, questions, repos: loaded, focus: attachFocus(loaded, focus) };
145
213
  }
146
214
 
147
215
  export function hasChanges(review) {
@@ -161,14 +229,11 @@ export function toMarkdown(review, fb) {
161
229
  }
162
230
  out.push('');
163
231
  if (fb.general.trim()) out.push('## General', '', fb.general.trim(), '');
164
- if (fb.answers.length) {
165
- out.push('## Answers', '');
166
- for (const a of fb.answers) out.push(`**Q: ${a.question}**`, '', a.answer.trim() || '_(no answer)_', '');
167
- }
168
- if (fb.comments.length) {
169
- out.push('## Comments', '');
232
+ const questions = fb.comments.filter((c) => c.kind === 'question');
233
+ const comments = fb.comments.filter((c) => c.kind !== 'question');
234
+ const byRepo = (list) => {
170
235
  for (const repo of review.repos) {
171
- const cs = fb.comments.filter((c) => c.repo === repo.path);
236
+ const cs = list.filter((c) => c.repo === repo.path);
172
237
  if (!cs.length) continue;
173
238
  out.push(`### ${repo.path}`, '');
174
239
  for (const c of cs) {
@@ -176,6 +241,19 @@ export function toMarkdown(review, fb) {
176
241
  out.push(`**${c.file}:${lines}** (${c.side} side)`, '', '```diff', c.code, '```', '', c.body.trim(), '');
177
242
  }
178
243
  }
244
+ };
245
+ if (questions.length) {
246
+ out.push('## Questions from the reviewer', '',
247
+ 'The reviewer asks about this code. Answer each one in your reply. Change code only where a question shows something is wrong, and say so.', '');
248
+ byRepo(questions);
249
+ }
250
+ if (comments.length) {
251
+ out.push('## Comments', '');
252
+ byRepo(comments);
253
+ }
254
+ if (fb.answers.length) {
255
+ out.push('## Answers to your questions', '');
256
+ for (const a of fb.answers) out.push(`**Q: ${a.question}**`, '', a.answer.trim() || '_(no answer)_', '');
179
257
  }
180
258
  if (!fb.comments.length && !fb.general.trim() && !fb.answers.some((a) => a.answer.trim())) {
181
259
  out.push('_No comments._', '');
@@ -196,6 +274,7 @@ export function normalizeFeedback(review, fb) {
196
274
  const comments = (Array.isArray(fb.comments) ? fb.comments : []).map((c) => ({
197
275
  repo: str(c.repo), file: str(c.file), side: c.side === 'old' ? 'old' : 'new',
198
276
  startLine: Number(c.startLine) || 0, endLine: Number(c.endLine) || 0, code: str(c.code), body: str(c.body),
277
+ kind: c.kind === 'question' ? 'question' : 'comment',
199
278
  }));
200
279
  return { verdict: fb.verdict === 'approve' ? 'approve' : 'request_changes', general: str(fb.general), answers, comments };
201
280
  }
package/src/dist/app.css CHANGED
@@ -1 +1 @@
1
- @font-face{font-family:Atkinson Hyperlegible Next Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url("./fonts/atkinson-hyperlegible-next-latin-ext-wght-normal-2N3YVG5Z.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Atkinson Hyperlegible Next Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url("./fonts/atkinson-hyperlegible-next-latin-wght-normal-VX3WN42K.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-cyrillic-ext-wght-normal-BUWC6ZS3.woff2") format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-cyrillic-wght-normal-ENSFVLK4.woff2") format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-greek-wght-normal-Y3BRZMJV.woff2") format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-vietnamese-wght-normal-SP6EBKAH.woff2") format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-latin-ext-wght-normal-TSLVDSQR.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-latin-wght-normal-VBS632QH.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--paper: #fbfbfc;--panel: #f2f3f6;--sunk: #e9ebf0;--ink: #1b1f2a;--muted: #5d6475;--faint: #8a90a0;--line: #dde0e7;--add: #e4f4ee;--add-gutter: #c8e9dc;--add-ink: #13795b;--del: #fbeaed;--del-gutter: #f4d1d8;--del-ink: #b4233c;--add-word: #a9dfc8;--del-word: #f0b6c1;--hunk: #eef0f6;--marker: #ffe766;--marker-soft: #fff6bf;--marker-ink: #5c4700;--logo-ink: #1b1f2a;--on-marker: #3d2f00;--focus: #3355d6;--approve: #13795b;--approve-ink: #ffffff;--reject: #b4233c;--reject-ink: #ffffff;--syn-keyword: #8a2a9c;--syn-string: #1c6b4a;--syn-number: #a5500a;--syn-comment: #7c8293;--syn-title: #2347b8;--syn-type: #9a3412;--syn-attr: #6d4c00;--sans: "Atkinson Hyperlegible Next Variable", system-ui, sans-serif;--mono: "JetBrains Mono Variable", ui-monospace, monospace;--bar: 56px;color-scheme:light}@media(prefers-color-scheme:dark){:root{--paper: #14171e;--panel: #1a1e27;--sunk: #222733;--ink: #e4e7ee;--muted: #9aa1b2;--faint: #6f7689;--line: #2a303c;--add: #10261f;--add-gutter: #173a2f;--add-ink: #5cc9a0;--del: #2a1820;--del-gutter: #45212d;--del-ink: #f0778b;--add-word: #1f5a45;--del-word: #6a2a3b;--hunk: #1c2130;--marker: #f2d44c;--marker-soft: rgba(242, 212, 76, .16);--marker-ink: #f2d44c;--focus: #7d97ff;--approve: #2f9e78;--reject: #c53a52;--syn-keyword: #d49bf0;--syn-string: #8fd3a8;--syn-number: #f0b072;--syn-comment: #737a8d;--syn-title: #8fb0ff;--syn-type: #f2a37f;--syn-attr: #e6c977;color-scheme:dark}}*{box-sizing:border-box}html{scroll-padding-top:calc(var(--bar) + 12px)}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.55 var(--sans);-webkit-font-smoothing:antialiased}button,textarea{font:inherit;color:inherit}:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.bar{position:sticky;top:0;z-index:10;height:var(--bar);display:flex;align-items:center;gap:16px;padding:0 20px;background:color-mix(in srgb,var(--paper) 88%,transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}.brand{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}.brand .logo{flex:none;display:block}.brand .wordmark{font-weight:800;font-size:17px;letter-spacing:-.015em}.status .brand{margin-bottom:28px}.bar .stats{display:flex;gap:12px;color:var(--muted);font-size:14px;white-space:nowrap;overflow:hidden}.bar .spacer{flex:1}.plus{color:var(--add-ink);font-family:var(--mono);font-size:13px}.minus{color:var(--del-ink);font-family:var(--mono);font-size:13px}.marks{background:var(--marker);color:var(--on-marker);border-radius:999px;padding:0 9px;font-weight:700;font-size:13px;white-space:nowrap}.btn{border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:6px 14px;cursor:pointer;font-weight:600;font-size:14px;white-space:nowrap}.btn:hover{background:var(--sunk)}.btn.approve{background:var(--approve);border-color:var(--approve);color:var(--approve-ink)}.btn.reject{background:transparent;border-color:var(--reject);color:var(--reject)}.btn.approve:hover{filter:brightness(1.08)}.btn.reject:hover{background:var(--del)}.btn.small{padding:3px 10px;font-size:13px;border-radius:6px}.btn.primary{background:var(--ink);border-color:var(--ink);color:var(--paper)}.btn.quiet{border-color:transparent;background:transparent;color:var(--muted)}.btn.quiet:hover{color:var(--ink);background:var(--sunk)}.picker{position:relative;display:inline-flex;min-width:0}.picker select{appearance:none;min-width:0;max-width:320px;text-overflow:ellipsis;font:inherit;font-size:14px;font-weight:600;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:5px 30px 5px 12px;cursor:pointer}.picker select:hover{background:var(--sunk)}.picker:after{content:"\25be";position:absolute;right:11px;top:50%;transform:translateY(-52%);pointer-events:none;color:var(--muted);font-size:12px}.layout{display:grid;grid-template-columns:280px minmax(0,1fr)}.tree{position:sticky;top:var(--bar);align-self:start;height:calc(100vh - var(--bar));overflow:auto;padding:20px 12px 40px 16px;border-right:1px solid var(--line);font-size:14px}.tree .repo{display:flex;align-items:baseline;gap:8px;margin:0 0 6px 6px;font-weight:700}.tree>div+div{margin-top:22px}.branch{font-family:var(--mono);font-size:12px;font-weight:500;color:var(--muted);background:var(--sunk);border-radius:5px;padding:1px 6px;white-space:nowrap}.tree a{display:grid;grid-template-columns:14px minmax(0,1fr) auto;gap:8px;align-items:baseline;padding:3px 6px;border-radius:6px;color:var(--ink);text-decoration:none}.tree a:hover{background:var(--panel)}.tree .path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.tree .count{font-size:12px}.tree .count .marks{padding:0 6px;font-size:12px}.st{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--faint)}.st.added{color:var(--add-ink)}.st.deleted{color:var(--del-ink)}.st.renamed{color:var(--syn-title)}.review{padding:28px 32px 120px;min-width:0}.handoff{max-width:76ch;margin-bottom:36px}.handoff .from{color:var(--muted);font-size:14px;margin:0 0 6px}.summary{font-size:21px;line-height:1.5;margin:0;white-space:pre-wrap;text-wrap:pretty}.summary.empty{color:var(--muted);font-size:17px}.questions{margin-top:24px;display:grid;gap:14px}.question{border-left:3px solid var(--line);padding:2px 0 2px 16px}.question.answered{border-left-color:var(--approve)}.question p{margin:0 0 8px;font-weight:700;white-space:pre-wrap}textarea{display:block;width:100%;min-height:64px;resize:vertical;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);line-height:1.5}textarea:focus{border-color:var(--focus);outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--focus) 22%,transparent)}.repo-head{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px;margin:32px 0 12px}.repo-head h2{margin:0;font-size:19px}.repo-head .where{color:var(--muted);font-size:13px;font-family:var(--mono);overflow-wrap:anywhere}.empty-note{color:var(--muted);margin:0 0 8px}.error{color:var(--del-ink)}.file{border:1px solid var(--line);border-radius:10px;margin-bottom:18px;background:var(--paper)}.fhead{position:sticky;top:var(--bar);z-index:5;display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--panel);border-bottom:1px solid var(--line);border-radius:10px 10px 0 0;font-family:var(--mono);font-size:13px}.file.collapsed .fhead{border-bottom:none;border-radius:10px}.fhead .name{flex:1;min-width:0;overflow-wrap:anywhere}.fhead .old{color:var(--muted)}.viewed-box{display:inline-flex;align-items:center;gap:6px;font:13px var(--sans);color:var(--muted);cursor:pointer;white-space:nowrap}.viewed-box input{accent-color:var(--approve);margin:0}.file.viewed .fhead .name{color:var(--muted)}.tree a.viewed .path{color:var(--faint)}.tree .tick{color:var(--approve);font-weight:700}.fold{border:none;background:none;cursor:pointer;color:var(--muted);padding:0 2px;font-size:13px;width:18px}.fold:hover{color:var(--ink)}.fnote{padding:12px 16px;color:var(--muted);font-size:14px}.scroll{overflow-x:auto;border-radius:0 0 10px 10px}table{border-collapse:collapse;width:100%;font:12.5px/21px var(--mono);font-variant-ligatures:none}td.ln{width:1%;min-width:48px;padding:0 10px;text-align:right;color:var(--faint);user-select:none;cursor:pointer;vertical-align:top;position:relative}tr:not(.hunk):hover td.ln:nth-child(2):before{content:"+";position:absolute;left:-2px;top:1px;width:18px;height:18px;line-height:18px;text-align:center;background:var(--marker);color:var(--on-marker);border-radius:5px;font-weight:700}td.ln:hover{color:var(--ink)}td.code{white-space:pre;padding:0 12px 0 4px}td.code .sign{display:inline-block;width:1.4em;color:var(--faint);user-select:none}tr.add td{background:var(--add)}tr.add td.ln{background:var(--add-gutter)}tr.add .sign{color:var(--add-ink)}tr.del td{background:var(--del)}tr.del td.ln{background:var(--del-gutter)}tr.del .sign{color:var(--del-ink)}mark.wd{color:inherit;padding:0;border-radius:0}tr.add mark.wd{background:var(--add-word)}tr.del mark.wd{background:var(--del-word)}tr.sel mark.wd{background:var(--marker);color:var(--on-marker, #3d2f00)}tr.hunk td{background:var(--hunk);color:var(--muted);padding-top:3px;padding-bottom:3px}tr.sel td{background:var(--marker-soft)!important}tr.sel td.ln{color:var(--ink)}tr.sel td.ln:first-child{box-shadow:inset 3px 0 0 var(--marker)}tr.comment>td{padding:8px 14px 10px 62px;background:var(--panel);font:15px/1.55 var(--sans);white-space:normal;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.note-card{border-left:3px solid var(--marker);padding:4px 0 4px 12px;max-width:80ch}.note-card .meta{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px;margin-bottom:2px}.note-card .meta .where{font-family:var(--mono);font-size:12px}.note-card .meta .actions{margin-left:auto;display:flex;gap:2px}.note-card p{margin:0;white-space:pre-wrap}.form{max-width:80ch}.form .row{display:flex;align-items:center;gap:8px;margin-top:8px}.form .hint{color:var(--faint);font-size:13px;margin-right:auto}.finish{max-width:76ch;margin-top:44px;padding-top:24px;border-top:1px solid var(--line)}.finish h2{margin:0 0 4px;font-size:19px}.finish p{margin:0 0 12px;color:var(--muted)}.finish .row{display:flex;gap:10px;margin-top:12px;flex-wrap:wrap}.hljs-keyword,.hljs-selector-tag,.hljs-built_in,.hljs-literal{color:var(--syn-keyword)}.hljs-string,.hljs-regexp,.hljs-template-tag,.hljs-addition{color:var(--syn-string)}.hljs-number,.hljs-symbol,.hljs-variable.constant_{color:var(--syn-number)}.hljs-comment,.hljs-quote,.hljs-meta{color:var(--syn-comment);font-style:italic}.hljs-title,.hljs-title.function_,.hljs-section,.hljs-name{color:var(--syn-title)}.hljs-type,.hljs-title.class_,.hljs-selector-class{color:var(--syn-type)}.hljs-attr,.hljs-attribute,.hljs-property,.hljs-params{color:var(--syn-attr)}.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}.status{max-width:46ch;margin:22vh auto 0;padding:0 16px}.status h1{font-size:30px;margin:0 0 8px}.status p{color:var(--muted);margin:0;font-size:17px}.status .stamp{display:table;margin-bottom:18px;padding:2px 12px;border-radius:999px;font-weight:700;font-size:14px;animation:stamp .42s cubic-bezier(.2,.9,.3,1.3)}.status .stamp.approve{background:var(--add);color:var(--add-ink)}.status .stamp.request_changes{background:var(--del);color:var(--del-ink)}@keyframes stamp{0%{transform:scale(.6);opacity:0}to{transform:scale(1);opacity:1}}@media(prefers-reduced-motion:reduce){.status .stamp{animation:none}}@media(max-width:900px){.layout{display:block}.tree{display:none}.review{padding:20px 16px 100px}.bar{padding:0 16px;gap:10px}.bar .stats,.bar .marks,.bar .wordmark{display:none}.picker select{max-width:34vw}.bar .btn{padding:6px 10px}.summary{font-size:18px}tr.comment>td{padding-left:14px}}
1
+ @font-face{font-family:Atkinson Hyperlegible Next Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url("./fonts/atkinson-hyperlegible-next-latin-ext-wght-normal-2N3YVG5Z.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Atkinson Hyperlegible Next Variable;font-style:normal;font-display:swap;font-weight:200 800;src:url("./fonts/atkinson-hyperlegible-next-latin-wght-normal-VX3WN42K.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-cyrillic-ext-wght-normal-BUWC6ZS3.woff2") format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-cyrillic-wght-normal-ENSFVLK4.woff2") format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-greek-wght-normal-Y3BRZMJV.woff2") format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-vietnamese-wght-normal-SP6EBKAH.woff2") format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-latin-ext-wght-normal-TSLVDSQR.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono Variable;font-style:normal;font-display:swap;font-weight:100 800;src:url("./fonts/jetbrains-mono-latin-wght-normal-VBS632QH.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--paper: #fbfbfc;--panel: #f2f3f6;--sunk: #e9ebf0;--ink: #1b1f2a;--muted: #5d6475;--faint: #8a90a0;--line: #dde0e7;--add: #e4f4ee;--add-gutter: #c8e9dc;--add-ink: #13795b;--del: #fbeaed;--del-gutter: #f4d1d8;--del-ink: #b4233c;--add-word: #a9dfc8;--del-word: #f0b6c1;--hunk: #eef0f6;--marker: #ffe766;--marker-soft: #fff6bf;--marker-ink: #5c4700;--logo-ink: #1b1f2a;--on-marker: #3d2f00;--focus: #3355d6;--agent: #3355d6;--agent-soft: #eaeffd;--approve: #13795b;--approve-ink: #ffffff;--reject: #b4233c;--reject-ink: #ffffff;--syn-keyword: #8a2a9c;--syn-string: #1c6b4a;--syn-number: #a5500a;--syn-comment: #7c8293;--syn-title: #2347b8;--syn-type: #9a3412;--syn-attr: #6d4c00;--sans: "Atkinson Hyperlegible Next Variable", system-ui, sans-serif;--mono: "JetBrains Mono Variable", ui-monospace, monospace;--bar: 56px;color-scheme:light}@media(prefers-color-scheme:dark){:root{--paper: #14171e;--panel: #1a1e27;--sunk: #222733;--ink: #e4e7ee;--muted: #9aa1b2;--faint: #6f7689;--line: #2a303c;--add: #10261f;--add-gutter: #173a2f;--add-ink: #5cc9a0;--del: #2a1820;--del-gutter: #45212d;--del-ink: #f0778b;--add-word: #1f5a45;--del-word: #6a2a3b;--hunk: #1c2130;--marker: #f2d44c;--marker-soft: rgba(242, 212, 76, .16);--marker-ink: #f2d44c;--focus: #7d97ff;--agent: #8fa6ff;--agent-soft: #1b2236;--approve: #2f9e78;--reject: #c53a52;--syn-keyword: #d49bf0;--syn-string: #8fd3a8;--syn-number: #f0b072;--syn-comment: #737a8d;--syn-title: #8fb0ff;--syn-type: #f2a37f;--syn-attr: #e6c977;color-scheme:dark}}*{box-sizing:border-box}html{scroll-padding-top:calc(var(--bar) + 12px)}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.55 var(--sans);-webkit-font-smoothing:antialiased}button,textarea{font:inherit;color:inherit}:focus-visible{outline:2px solid var(--focus);outline-offset:2px}.bar{position:sticky;top:0;z-index:10;height:var(--bar);display:flex;align-items:center;gap:16px;padding:0 20px;background:color-mix(in srgb,var(--paper) 88%,transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}.brand{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}.brand .logo{flex:none;display:block}.brand .wordmark{font-weight:800;font-size:17px;letter-spacing:-.015em}.status .brand{margin-bottom:28px}.bar .stats{display:flex;gap:12px;color:var(--muted);font-size:14px;white-space:nowrap;overflow:hidden}.bar .spacer{flex:1}.plus{color:var(--add-ink);font-family:var(--mono);font-size:13px}.minus{color:var(--del-ink);font-family:var(--mono);font-size:13px}.marks{background:var(--marker);color:var(--on-marker);border-radius:999px;padding:0 9px;font-weight:700;font-size:13px;white-space:nowrap}.btn{border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:6px 14px;cursor:pointer;font-weight:600;font-size:14px;white-space:nowrap}.btn:hover{background:var(--sunk)}.btn.approve{background:var(--approve);border-color:var(--approve);color:var(--approve-ink)}.btn.reject{background:transparent;border-color:var(--reject);color:var(--reject)}.btn.approve:hover{filter:brightness(1.08)}.btn.reject:hover{background:var(--del)}.btn.small{padding:3px 10px;font-size:13px;border-radius:6px}.btn.primary{background:var(--ink);border-color:var(--ink);color:var(--paper)}.btn.quiet{border-color:transparent;background:transparent;color:var(--muted)}.btn.quiet:hover{color:var(--ink);background:var(--sunk)}.picker{position:relative;display:inline-flex;min-width:0}.picker select{appearance:none;min-width:0;max-width:320px;text-overflow:ellipsis;font:inherit;font-size:14px;font-weight:600;color:var(--ink);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:5px 30px 5px 12px;cursor:pointer}.picker select:hover{background:var(--sunk)}.picker:after{content:"\25be";position:absolute;right:11px;top:50%;transform:translateY(-52%);pointer-events:none;color:var(--muted);font-size:12px}.layout{display:grid;grid-template-columns:280px minmax(0,1fr)}.tree{position:sticky;top:var(--bar);align-self:start;height:calc(100vh - var(--bar));overflow:auto;padding:20px 12px 40px 16px;border-right:1px solid var(--line);font-size:14px}.tree .repo{display:flex;align-items:baseline;gap:8px;margin:0 0 6px 6px;font-weight:700}.tree>div+div{margin-top:22px}.branch{font-family:var(--mono);font-size:12px;font-weight:500;color:var(--muted);background:var(--sunk);border-radius:5px;padding:1px 6px;white-space:nowrap}.tree a{display:grid;grid-template-columns:14px minmax(0,1fr) auto;gap:8px;align-items:baseline;padding:3px 6px;border-radius:6px;color:var(--ink);text-decoration:none}.tree a:hover{background:var(--panel)}.tree a.minor .path{color:var(--muted)}.tree a.focused .path{font-weight:700;color:var(--ink)}.tree .path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}.tree .count{font-size:12px}.tree .count .marks{padding:0 6px;font-size:12px}.st{font-family:var(--mono);font-size:12px;font-weight:700;color:var(--faint)}.st.added{color:var(--add-ink)}.st.deleted{color:var(--del-ink)}.st.renamed{color:var(--syn-title)}.review{padding:28px 32px 120px;min-width:0}.handoff{max-width:76ch;margin-bottom:28px}.summary{font-size:15.5px;line-height:1.6;text-wrap:pretty}.summary>:first-child{margin-top:0}.summary p{margin:0 0 10px}.summary .summary-lead,.summary-lead{font-size:19px;line-height:1.5;margin:0 0 12px}.summary-lead.empty{color:var(--muted);font-size:17px}.summary h3{font-size:15px;margin:18px 0 6px}.summary ul,.summary ol{margin:0 0 12px;padding-left:22px}.summary li{margin:3px 0}.summary code,.focus-list a,.focus-list .missing{font-family:var(--mono);font-size:.88em}.summary code{background:var(--sunk);padding:1px 5px;border-radius:4px}.handoff h3{font-size:15px;margin:0 0 8px}.focus-list{margin-top:22px;padding:12px 16px;border-left:3px solid var(--agent);background:var(--agent-soft);border-radius:0 8px 8px 0}.focus-list ul{list-style:none;margin:0;padding:0;display:grid;gap:10px}.focus-list li{display:grid;gap:1px}.focus-list a{color:var(--agent);text-decoration:none;font-weight:600}.focus-list a:hover{text-decoration:underline}.focus-list a .repo{font-family:var(--sans);font-weight:700;color:var(--muted);margin-right:6px}.focus-list .missing{color:var(--muted);text-decoration:line-through}.focus-list .why{color:var(--ink)}.questions{margin-top:22px;display:grid;gap:12px}.question{border-left:3px solid var(--line);padding:2px 0 2px 16px}.question.answered{border-left-color:var(--approve)}.question p{margin:0 0 8px;font-weight:700;white-space:pre-wrap}.toolbar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px 20px;margin-bottom:4px}.toolbar .empty-note{margin:0}.filters{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.filters .lead{color:var(--muted);font-size:13px;margin-right:2px}.chip{font:inherit;font-size:13px;font-weight:600;cursor:pointer;border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:999px;padding:2px 10px}.chip .n{font-weight:500;margin-left:2px}.chip:hover{background:var(--panel);color:var(--ink)}.chip.on{background:var(--ink);border-color:var(--ink);color:var(--paper)}textarea{display:block;width:100%;min-height:64px;resize:vertical;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);line-height:1.5}textarea:focus{border-color:var(--focus);outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--focus) 22%,transparent)}.repo-head{display:flex;align-items:baseline;flex-wrap:wrap;gap:10px;margin:32px 0 12px}.repo-head h2{margin:0;font-size:19px}.repo-head .where{color:var(--muted);font-size:13px;font-family:var(--mono);overflow-wrap:anywhere}.empty-note{color:var(--muted);margin:0 0 8px}.error{color:var(--del-ink)}.file{border:1px solid var(--line);border-radius:10px;margin-bottom:18px;background:var(--paper)}.fhead{position:sticky;top:var(--bar);z-index:5;display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--panel);border-bottom:1px solid var(--line);border-radius:10px 10px 0 0;font-family:var(--mono);font-size:13px}.file.collapsed .fhead{border-bottom:none;border-radius:10px}.fhead .name{flex:1;min-width:0;overflow-wrap:anywhere}.fhead .old{color:var(--muted)}.kind-tag{font:12px var(--sans);color:var(--muted);background:var(--sunk);border-radius:5px;padding:0 6px}.file.focused{border-color:color-mix(in srgb,var(--agent) 45%,var(--line))}tr.agent-note>td{padding:6px 14px 6px 62px;background:var(--agent-soft);font:14px/1.5 var(--sans);white-space:normal}.agent-note.file-level{padding:8px 14px;background:var(--agent-soft);border-bottom:1px solid var(--line);font-size:14px}.agent-card{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 10px;border-left:3px solid var(--agent);padding-left:10px;max-width:90ch}.agent-card .who{color:var(--agent);font-weight:700;font-size:13px}.agent-card .lines{color:var(--muted);font-size:12px;font-family:var(--mono)}.viewed-box{display:inline-flex;align-items:center;gap:6px;font:13px var(--sans);color:var(--muted);cursor:pointer;white-space:nowrap}.viewed-box input{accent-color:var(--approve);margin:0}.file.viewed .fhead .name{color:var(--muted)}.tree a.viewed .path{color:var(--faint)}.tree .tick{color:var(--approve);font-weight:700}.fold{border:none;background:none;cursor:pointer;color:var(--muted);padding:0 2px;font-size:13px;width:18px}.fold:hover{color:var(--ink)}.fnote{padding:12px 16px;color:var(--muted);font-size:14px}.scroll{overflow-x:auto;border-radius:0 0 10px 10px}table{border-collapse:collapse;width:100%;font:12.5px/21px var(--mono);font-variant-ligatures:none}td.ln{width:1%;min-width:48px;padding:0 10px;text-align:right;color:var(--faint);user-select:none;cursor:pointer;vertical-align:top;position:relative}tr:not(.hunk):hover td.ln:nth-child(2):before{content:"+";position:absolute;left:-2px;top:1px;width:18px;height:18px;line-height:18px;text-align:center;background:var(--marker);color:var(--on-marker);border-radius:5px;font-weight:700}td.ln:hover{color:var(--ink)}td.code{white-space:pre;padding:0 12px 0 4px}td.code .sign{display:inline-block;width:1.4em;color:var(--faint);user-select:none}tr.add td{background:var(--add)}tr.add td.ln{background:var(--add-gutter)}tr.add .sign{color:var(--add-ink)}tr.del td{background:var(--del)}tr.del td.ln{background:var(--del-gutter)}tr.del .sign{color:var(--del-ink)}mark.wd{color:inherit;padding:0;border-radius:0}tr.add mark.wd{background:var(--add-word)}tr.del mark.wd{background:var(--del-word)}tr.sel mark.wd{background:var(--marker);color:var(--on-marker, #3d2f00)}tr.hunk td{background:var(--hunk);color:var(--muted);padding-top:3px;padding-bottom:3px}tr.sel td{background:var(--marker-soft)!important}tr.sel td.ln{color:var(--ink)}tr.sel td.ln:first-child{box-shadow:inset 3px 0 0 var(--marker)}tr.comment>td{padding:8px 14px 10px 62px;background:var(--panel);font:15px/1.55 var(--sans);white-space:normal;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.note-card{border-left:3px solid var(--marker);padding:4px 0 4px 12px;max-width:80ch}.note-card .meta{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px;margin-bottom:2px}.note-card .meta .where{font-family:var(--mono);font-size:12px}.note-card .meta .actions{margin-left:auto;display:flex;gap:2px}.note-card p{margin:0;white-space:pre-wrap}.note-card.question-card{border-left-color:var(--agent)}.kind-pill{color:var(--agent);font-weight:700;font-size:12px}.marks.ask{background:var(--agent-soft);color:var(--agent)}.btn.ask{border-color:var(--agent);color:var(--agent);background:transparent}.btn.ask:hover{background:var(--agent-soft)}.form{max-width:80ch}.form .row{display:flex;align-items:center;gap:8px;margin-top:8px}.form .hint{color:var(--faint);font-size:13px;margin-right:auto}.finish{max-width:76ch;margin-top:44px;padding-top:24px;border-top:1px solid var(--line)}.finish h2{margin:0 0 4px;font-size:19px}.finish p{margin:0 0 12px;color:var(--muted)}.finish .row{display:flex;gap:10px;margin-top:12px;flex-wrap:wrap}.hljs-keyword,.hljs-selector-tag,.hljs-built_in,.hljs-literal{color:var(--syn-keyword)}.hljs-string,.hljs-regexp,.hljs-template-tag,.hljs-addition{color:var(--syn-string)}.hljs-number,.hljs-symbol,.hljs-variable.constant_{color:var(--syn-number)}.hljs-comment,.hljs-quote,.hljs-meta{color:var(--syn-comment);font-style:italic}.hljs-title,.hljs-title.function_,.hljs-section,.hljs-name{color:var(--syn-title)}.hljs-type,.hljs-title.class_,.hljs-selector-class{color:var(--syn-type)}.hljs-attr,.hljs-attribute,.hljs-property,.hljs-params{color:var(--syn-attr)}.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}.status{max-width:46ch;margin:22vh auto 0;padding:0 16px}.status h1{font-size:30px;margin:0 0 8px}.status p{color:var(--muted);margin:0;font-size:17px}.status .stamp{display:table;margin-bottom:18px;padding:2px 12px;border-radius:999px;font-weight:700;font-size:14px;animation:stamp .42s cubic-bezier(.2,.9,.3,1.3)}.status .stamp.approve{background:var(--add);color:var(--add-ink)}.status .stamp.request_changes{background:var(--del);color:var(--del-ink)}@keyframes stamp{0%{transform:scale(.6);opacity:0}to{transform:scale(1);opacity:1}}@media(prefers-reduced-motion:reduce){.status .stamp{animation:none}}@media(max-width:900px){.layout{display:block}.tree{display:none}.review{padding:20px 16px 100px}.bar{padding:0 16px;gap:10px}.bar .stats,.bar .marks,.bar .wordmark{display:none}.picker select{max-width:34vw}.bar .btn{padding:6px 10px}.summary{font-size:18px}tr.comment>td{padding-left:14px}}