add CSS build pipeline, avatar improvements, and various features

- Extract CSS to src/styles/main.css, compile with lightningcss via scripts/build-css.ts; add scripts/dev.ts to run server + CSS watcher together; update Containerfile for multi-stage CSS build
- Auto-create default avatar on GET /avatars/:id when none exists instead of 404
- Show avatars in issue and patch list views
- Add "completed" status for issues with a Completed tab and POST /:repo/issues/:number/complete endpoint
- Make git author name/email configurable via GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL env vars, used when merging patches
- Rewrite syntax highlighting output as a line-number table (blob-table) instead of plain <pre>
- Filter already-generated source archives from pending list in ReleaseDetail
- Conditionally show SSH clone URL based on SSH_DISABLED
- Add admin CTA to empty repo list

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
AuthorKonata <konata@posteo.jp>
Date
Commite5e41ae99d0e44d645c1e92fa71a91c31d1e6553
Parent673f60b
23 files changed, 2788 insertions(+), 1871 deletions(-)
M.gitignore
@@ -37,3 +37,6 @@ data/
3737
3838 # Test data
3939 data-test/
40+
41+# Generated CSS
42+public/assets/main.css
MContainerfile
@@ -1,3 +1,11 @@
1+FROM oven/bun:1 AS builder
2+WORKDIR /app
3+COPY package.json bun.lock* ./
4+COPY scripts ./scripts
5+COPY src/styles ./src/styles
6+RUN mkdir -p public/assets
7+RUN bun install --frozen-lockfile
8+
19 FROM oven/bun:1 AS base
210 WORKDIR /app
311
@@ -7,10 +15,11 @@ RUN apt-get --update install -y --no-install-recommends \
715
816 # Copy public first so postinstall can write vendor files into it
917 COPY public ./public
18+COPY --from=builder /app/public/assets/* ./public/assets
1019
11-# Install dependencies (postinstall vendors simplewebauthn + jxl-polyfill into public/)
20+# Install runtime dependencies (skip postinstall — builder stage handled it)
1221 COPY package.json bun.lock* ./
13-RUN bun install --frozen-lockfile --production
22+RUN bun install --frozen-lockfile --production --ignore-scripts
1423
1524 # Copy source
1625 COPY src ./src
MREADME.md
@@ -64,6 +64,9 @@ All settings are environment variables:
6464 | `TRUSTED_PROXY` | `0` | Trust `X-Forwarded-For` |
6565 | `RATE_LIMIT_DISABLED` | `0` | Set to `1` to disable rate limiting |
6666 | `HIGHLIGHT_WORKERS` | `4` | Number of syntax highlighting workers* |
67+| `GIT_AUTHOR_NAME` | `OWNER_DISPLAY_NAME` | Author/committer name used when merging patches |
68+| `GIT_AUTHOR_EMAIL` | `{name}@{BASE_URL host}`| Author/committer email used when merging patches|
69+
6770 \* More workers mean more CPU cores can be used to parallelize highlighting of files.
6871 Because of the language grammars, which can't be shared across workers, the memory usage per worker is quite high, at about 200MB.
6972 So be careful when increasing this.
@@ -90,4 +93,4 @@ bun run test # Playwright E2E tests (don't use bun test, it doesn't res
9093 - Use [git-bug](https://github.com/git-bug/git-bug) for issue tracking instead of custom implementation
9194 - Issue labels
9295 - Repository list reordering (e.g. last committed) and starring
93-- redirect image urls in readme
96+- redirect image urls in readme
Mbun.lock
@@ -28,6 +28,7 @@
2828 "@types/argon2": "^0.15.4",
2929 "@types/bun": "latest",
3030 "@types/ssh2": "^1.15.5",
31+ "lightningcss": "^1.32.0",
3132 "playwright": "^1.58.2",
3233 },
3334 "peerDependencies": {
@@ -306,6 +307,30 @@
306307
307308 "kysely-bun-sqlite": ["kysely-bun-sqlite@0.4.0", "", { "dependencies": { "bun-types": "^1.1.31" }, "peerDependencies": { "kysely": "^0.28.2" } }, "sha512-2EkQE5sT4ewiw7IWfJsAkpxJ/QPVKXKO5sRYI/xjjJIJlECuOdtG+ssYM0twZJySrdrmuildNPFYVreyu1EdZg=="],
308309
310+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
311+
312+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
313+
314+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
315+
316+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
317+
318+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
319+
320+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
321+
322+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
323+
324+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
325+
326+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
327+
328+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
329+
330+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
331+
332+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
333+
309334 "linguist-languages": ["linguist-languages@9.3.1", "", {}, "sha512-Mum2sqg3MyhgKfpulFhKZMAK/1VnV6m9vCV8YQCSqWs+pbKouKn9EqRshZjVWUaJjl6NTTDcYJk/1+C02siXEQ=="],
310335
311336 "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
Mpackage.json
@@ -2,14 +2,15 @@
22 "name": "hearthforge",
33 "module": "src/index.tsx",
44 "scripts": {
5- "dev": "bun run --watch src/index.tsx",
5+ "dev": "bun scripts/dev.ts",
66 "start": "bun run src/index.tsx",
7+ "css:build": "bun scripts/build-css.ts",
78 "db:init": "bun run src/db/init.ts",
89 "db:seed": "bun run src/db/seed.ts",
910 "test": "bun test tests/e2e.test.ts --timeout 60000",
1011 "vendor:simplewebauthn": "bun build node_modules/@simplewebauthn/browser/esm/index.js --outfile public/assets/simplewebauthn-browser.js --format esm",
1112 "vendor:jxl-polyfill": "cp node_modules/jxl-rs-polyfill/dist/auto.js public/assets/jxl-polyfill.js",
12- "postinstall": "bun run vendor:simplewebauthn && bun run vendor:jxl-polyfill",
13+ "postinstall": "bun run vendor:simplewebauthn && bun run vendor:jxl-polyfill && bun run css:build",
1314 "lint": "biome check src/",
1415 "format": "biome check --write src/"
1516 },
@@ -17,9 +18,12 @@
1718 "private": true,
1819 "devDependencies": {
1920 "@biomejs/biome": "^2.4.7",
21+ "@simplewebauthn/browser": "^13.3.0",
2022 "@types/argon2": "^0.15.4",
2123 "@types/bun": "latest",
2224 "@types/ssh2": "^1.15.5",
25+ "jxl-rs-polyfill": "^0.1.1",
26+ "lightningcss": "^1.32.0",
2327 "playwright": "^1.58.2"
2428 },
2529 "peerDependencies": {
@@ -29,13 +33,11 @@
2933 "@elysiajs/static": "^1.4.7",
3034 "@jsquash/jxl": "^1.3.0",
3135 "@kitajs/html": "^4.2.13",
32- "@simplewebauthn/browser": "^13.3.0",
3336 "@simplewebauthn/server": "^13.3.0",
3437 "argon2": "^0.44.0",
3538 "elysia": "^1.4.27",
3639 "file-type": "^21.3.2",
3740 "isomorphic-dompurify": "^3.3.0",
38- "jxl-rs-polyfill": "^0.1.1",
3941 "kysely": "^0.28.12",
4042 "kysely-bun-sqlite": "^0.4.0",
4143 "linguist-languages": "^9.3.1",
Dpublic/assets/main.css
-1807
@@ -1,1807 +0,0 @@
1-/* View Transitions */
2-@view-transition {
3- navigation: auto;
4-}
5-
6-.site-nav { view-transition-name: site-nav; }
7-.repo-header { view-transition-name: repo-header; }
8-.breadcrumb { view-transition-name: breadcrumb; }
9-
10-/* ============================================================
11- @layer reset
12- ============================================================ */
13-@layer reset {
14- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
15- html { -webkit-text-size-adjust: 100%; }
16- body { min-height: 100dvh; min-width: 500px; }
17- img, video { max-width: 100%; height: auto; }
18- button, input, textarea, select { font: inherit; }
19- a { color: inherit; }
20- ul, ol { list-style: none; }
21- h1, h2, h3, h4, h5, h6 { font-weight: 600; line-height: 1.2; }
22- pre, code { font-family: var(--font-mono); }
23-}
24-
25-/* ============================================================
26- @layer tokens
27- ============================================================ */
28-@layer tokens {
29- :root {
30- /* Colors — light */
31- --color-bg: #ffffff;
32- --color-bg-subtle: #f6f8fa;
33- --color-bg-inset: #eef0f2;
34- --color-border: #d0d7de;
35- --color-border-muted: #e4e7eb;
36- --color-text: #1f2328;
37- --color-text-muted: #656d76;
38- --color-link: #0969da;
39- --color-link-hover: #0550ae;
40-
41- --color-accent: #fd7e14;
42- --color-accent-bg: #fff3cd;
43-
44- --color-success: #1a7f37;
45- --color-success-bg: #dafbe1;
46- --color-danger: #cf222e;
47- --color-danger-bg: #ffebe9;
48- --color-warning: #9a6700;
49- --color-warning-bg: #fff8c5;
50- --color-merged: #8250df;
51- --color-merged-bg: #fbefff;
52-
53- /* Typography */
54- --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
55- --font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace;
56-
57- --text-xs: 0.75rem;
58- --text-sm: 0.875rem;
59- --text-base: 1rem;
60- --text-lg: 1.125rem;
61- --text-xl: 1.25rem;
62- --text-2xl: 1.5rem;
63- --text-3xl: 1.875rem;
64-
65- /* Spacing */
66- --space-1: 0.25rem;
67- --space-2: 0.5rem;
68- --space-3: 0.75rem;
69- --space-4: 1rem;
70- --space-5: 1.25rem;
71- --space-6: 1.5rem;
72- --space-8: 2rem;
73- --space-10: 2.5rem;
74- --space-12: 3rem;
75- --space-16: 4rem;
76-
77- /* Border radius */
78- --radius-sm: 4px;
79- --radius-md: 6px;
80- --radius-lg: 8px;
81- --radius-full: 9999px;
82-
83- /* Shadows */
84- --shadow-sm: 0 1px 0 rgba(27,31,36,0.04);
85- --shadow-md: 0 3px 6px rgba(140,149,159,0.15);
86- --shadow-lg: 0 8px 24px rgba(140,149,159,0.2);
87- }
88-
89- @media (prefers-color-scheme: dark) {
90- :root {
91- --color-bg: #0d1117;
92- --color-bg-subtle: #161b22;
93- --color-bg-inset: #1c2128;
94- --color-border: #30363d;
95- --color-border-muted: #21262d;
96- --color-text: #e6edf3;
97- --color-text-muted: #7d8590;
98- --color-link: #58a6ff;
99- --color-link-hover: #79c0ff;
100-
101- --color-accent: #f0883e;
102- --color-accent-bg: #3d2f00;
103-
104- --color-success: #3fb950;
105- --color-success-bg: #0d2b19;
106- --color-danger: #f85149;
107- --color-danger-bg: #2d1419;
108- --color-warning: #d29922;
109- --color-warning-bg: #2c2000;
110- --color-merged: #bc8cff;
111- --color-merged-bg: #1e0b3e;
112- }
113- }
114-
115- /* Explicit theme overrides — applied via data-theme attribute set by cookie */
116- :root[data-theme="dark"] {
117- --color-bg: #0d1117;
118- --color-bg-subtle: #161b22;
119- --color-bg-inset: #1c2128;
120- --color-border: #30363d;
121- --color-border-muted: #21262d;
122- --color-text: #e6edf3;
123- --color-text-muted: #7d8590;
124- --color-link: #58a6ff;
125- --color-link-hover: #79c0ff;
126- --color-accent: #f0883e;
127- --color-accent-bg: #3d2f00;
128- --color-success: #3fb950;
129- --color-success-bg: #0d2b19;
130- --color-danger: #f85149;
131- --color-danger-bg: #2d1419;
132- --color-warning: #d29922;
133- --color-warning-bg: #2c2000;
134- --color-merged: #bc8cff;
135- --color-merged-bg: #1e0b3e;
136- }
137- :root[data-theme="light"] {
138- --color-bg: #ffffff;
139- --color-bg-subtle: #f6f8fa;
140- --color-bg-inset: #eef0f2;
141- --color-border: #d0d7de;
142- --color-border-muted: #e4e7eb;
143- --color-text: #1f2328;
144- --color-text-muted: #656d76;
145- --color-link: #0969da;
146- --color-link-hover: #0550ae;
147- --color-accent: #fd7e14;
148- --color-accent-bg: #fff3cd;
149- --color-success: #1a7f37;
150- --color-success-bg: #dafbe1;
151- --color-danger: #cf222e;
152- --color-danger-bg: #ffebe9;
153- --color-warning: #9a6700;
154- --color-warning-bg: #fff8c5;
155- --color-merged: #8250df;
156- --color-merged-bg: #fbefff;
157- }
158-}
159-
160-/* ============================================================
161- @layer layout
162- ============================================================ */
163-@layer layout {
164- body {
165- font-family: var(--font-sans);
166- font-size: var(--text-base);
167- color: var(--color-text);
168- background: var(--color-bg);
169- display: grid;
170- grid-template-rows: auto 1fr auto;
171- min-height: 100dvh;
172- }
173-
174- .site-header {
175- background: var(--color-bg-subtle);
176- border-bottom: 1px solid var(--color-border);
177- position: sticky;
178- top: 0;
179- z-index: 100;
180- }
181-
182- .site-nav {
183- max-width: 1200px;
184- margin: 0 auto;
185- padding: var(--space-3) var(--space-4);
186- display: flex;
187- align-items: center;
188- gap: var(--space-4);
189- }
190-
191- .site-logo {
192- display: flex;
193- align-items: center;
194- gap: var(--space-2);
195- text-decoration: none;
196- font-weight: 700;
197- font-size: var(--text-lg);
198- color: var(--color-text);
199- }
200-
201- .logo-icon { width: 1.6em; height: 1.6em; display: block; flex-shrink: 0; }
202-
203- .nav-links {
204- margin-left: auto;
205- display: flex;
206- align-items: center;
207- gap: var(--space-3);
208- }
209-
210- .nav-user {
211- font-size: var(--text-sm);
212- color: var(--color-text-muted);
213- }
214-
215- .site-main {
216- padding: var(--space-8) var(--space-4);
217- min-width: 0;
218- }
219-
220- .site-footer {
221- text-align: center;
222- padding: var(--space-6) var(--space-4);
223- font-size: var(--text-sm);
224- color: var(--color-text-muted);
225- border-top: 1px solid var(--color-border);
226- }
227-
228- .container {
229- max-width: 1000px;
230- margin: 0 auto;
231- overflow-x: clip;
232- }
233-
234- .container-narrow {
235- max-width: 680px;
236- }
237-
238- .inline-form {
239- display: inline;
240- }
241-
242- /* --- Two-step delete confirmation --- */
243- .confirm-details { position: relative; display: inline-block; }
244- .confirm-details > summary { list-style: none; }
245- .confirm-details > summary::-webkit-details-marker { display: none; }
246- .confirm-popup {
247- position: absolute;
248- right: 0;
249- top: calc(100% + var(--space-1));
250- background: var(--color-bg);
251- border: 1px solid var(--color-border);
252- border-radius: var(--radius-md);
253- padding: var(--space-3) var(--space-4);
254- display: flex;
255- align-items: center;
256- gap: var(--space-3);
257- white-space: nowrap;
258- z-index: 100;
259- font-size: var(--text-sm);
260- color: var(--color-text-muted);
261- }
262-}
263-
264-/* ============================================================
265- @layer components
266- ============================================================ */
267-@layer components {
268-
269- /* --- Buttons --- */
270- .btn {
271- display: inline-flex;
272- align-items: center;
273- gap: var(--space-2);
274- padding: var(--space-2) var(--space-4);
275- border: 1px solid var(--color-border);
276- border-radius: var(--radius-md);
277- font-size: var(--text-sm);
278- font-weight: 500;
279- text-decoration: none;
280- cursor: pointer;
281- background: var(--color-bg-subtle);
282- color: var(--color-text);
283- transition: background 0.1s, border-color 0.1s;
284- white-space: nowrap;
285- }
286- .btn:hover { background: var(--color-bg-inset); border-color: var(--color-border); }
287- .btn:disabled { opacity: 0.5; cursor: not-allowed; }
288- .btn-primary {
289- background: var(--color-accent);
290- border-color: transparent;
291- color: #fff;
292- }
293- .btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 82%, #000); }
294- .btn-secondary {
295- background: var(--color-bg-subtle);
296- border-color: var(--color-border);
297- }
298- .btn-ghost {
299- background: transparent;
300- border-color: transparent;
301- }
302- .btn-ghost:hover { background: var(--color-bg-inset); }
303- /* Keep ghost transparent even when combined with size variants */
304- .btn-ghost.btn-xs,
305- .btn-ghost.btn-sm { background: transparent; border-color: transparent; }
306- .btn-ghost.btn-xs:hover,
307- .btn-ghost.btn-sm:hover { background: var(--color-bg-inset); }
308- .btn-sm { padding: var(--space-1) var(--space-3); font-size: var(--text-xs); }
309- .btn-block { width: 100%; justify-content: center; }
310-
311- /* --- Forms --- */
312- .form-group {
313- display: flex;
314- flex-direction: column;
315- gap: var(--space-2);
316- margin-bottom: var(--space-4);
317- }
318- .form-group label {
319- font-size: var(--text-sm);
320- font-weight: 500;
321- color: var(--color-text);
322- }
323- .form-group input:not([type="file"]):not([type="checkbox"]),
324- .form-group textarea,
325- .form-group select {
326- padding: var(--space-2) var(--space-3);
327- border: 1px solid var(--color-border);
328- border-radius: var(--radius-md);
329- background: var(--color-bg);
330- color: var(--color-text);
331- font-size: var(--text-sm);
332- }
333- .form-group input:not([type="file"]):not([type="checkbox"]):focus,
334- .form-group textarea:focus {
335- outline: 2px solid var(--color-accent);
336- outline-offset: -1px;
337- border-color: var(--color-accent);
338- }
339- .form-group input[type="file"] {
340- font-size: var(--text-sm);
341- color: var(--color-text);
342- padding: 0;
343- border: none;
344- background: none;
345- }
346- .form-group textarea { resize: vertical; min-height: 120px; }
347- .form-success {
348- background: var(--color-success-bg);
349- color: var(--color-success);
350- border: 1px solid var(--color-success);
351- border-radius: var(--radius-md);
352- padding: var(--space-3) var(--space-4);
353- font-size: var(--text-sm);
354- margin-bottom: var(--space-4);
355- }
356- .form-hint {
357- font-size: var(--text-sm);
358- color: var(--color-text-muted);
359- margin: 0;
360- }
361- .form-error {
362- background: var(--color-danger-bg);
363- color: var(--color-danger);
364- border: 1px solid var(--color-danger);
365- border-radius: var(--radius-md);
366- padding: var(--space-3) var(--space-4);
367- font-size: var(--text-sm);
368- margin-bottom: var(--space-4);
369- }
370- .form-actions {
371- display: flex;
372- gap: var(--space-3);
373- align-items: center;
374- flex-wrap: wrap;
375- }
376- .form-card {
377- background: var(--color-bg-subtle);
378- border: 1px solid var(--color-border);
379- border-radius: var(--radius-lg);
380- padding: var(--space-6);
381- margin-top: var(--space-4);
382- }
383- .checkbox-label {
384- display: flex;
385- align-items: center;
386- gap: var(--space-2);
387- cursor: pointer;
388- }
389-
390- /* --- Auth --- */
391- .auth-container {
392- max-width: 400px;
393- margin: var(--space-12) auto;
394- }
395- .auth-container .page-title { margin-bottom: var(--space-6); }
396- .auth-form { margin-bottom: var(--space-4); }
397- .auth-footer {
398- text-align: center;
399- font-size: var(--text-sm);
400- color: var(--color-text-muted);
401- margin-top: var(--space-4);
402- }
403- .passkey-btn { display: none; margin-top: var(--space-3); }
404-
405- /* --- Page header --- */
406- .page-header {
407- display: flex;
408- align-items: center;
409- justify-content: space-between;
410- margin-bottom: var(--space-6);
411- gap: var(--space-4);
412- }
413- .page-title {
414- font-size: var(--text-2xl);
415- color: var(--color-text);
416- margin-bottom: var(--space-4);
417- }
418- .page-title a { text-decoration: none; color: var(--color-link); }
419- .page-title a:hover { text-decoration: underline; }
420- .section-title {
421- font-size: var(--text-lg);
422- margin-bottom: var(--space-4);
423- padding-bottom: var(--space-2);
424- border-bottom: 1px solid var(--color-border);
425- }
426-
427- /* --- Repo header --- */
428- .repo-header {
429- margin-bottom: var(--space-2);
430- }
431- .repo-title-row {
432- display: flex;
433- align-items: center;
434- gap: var(--space-3);
435- flex-wrap: wrap;
436- }
437- .repo-title-row .page-title { margin-bottom: 0; }
438- .repo-header-title {
439- display: flex;
440- align-items: center;
441- gap: var(--space-3);
442- flex-wrap: wrap;
443- }
444- .repo-description {
445- color: var(--color-text-muted);
446- margin-top: var(--space-2);
447- }
448- .repo-header-meta {
449- display: flex;
450- align-items: center;
451- gap: var(--space-4);
452- margin-bottom: var(--space-3);
453- font-size: var(--text-sm);
454- color: var(--color-text-muted);
455- }
456- .icon { font-style: normal; }
457-
458- /* --- Branch selector --- */
459- .branch-selector {
460- display: inline-flex;
461- align-items: center;
462- gap: var(--space-2);
463- }
464- .branch-selector-icon {
465- color: var(--color-text-muted);
466- font-size: var(--text-sm);
467- user-select: none;
468- }
469- .branch-select {
470- padding: var(--space-1) var(--space-2);
471- padding-right: var(--space-5);
472- border: 1px solid var(--color-border);
473- border-radius: var(--radius-md);
474- background: var(--color-bg-subtle);
475- font-size: var(--text-sm);
476- font-family: var(--font-mono);
477- color: var(--color-text);
478- cursor: pointer;
479- appearance: auto;
480- max-width: 180px;
481- }
482- .branch-select:hover { border-color: var(--color-text-muted); }
483- .branch-select:focus { outline: 2px solid var(--color-accent); outline-offset: -1px; }
484-
485- /* toolbar row above file tree */
486- .tree-toolbar {
487- display: flex;
488- align-items: center;
489- justify-content: space-between;
490- margin-bottom: var(--space-3);
491- }
492-
493- /* commits-header flex row */
494- .commits-header {
495- display: flex;
496- align-items: center;
497- gap: var(--space-4);
498- margin-bottom: var(--space-4);
499- flex-wrap: wrap;
500- }
501- .commits-header .section-title { margin-bottom: 0; }
502-
503- /* --- Repo nav bar (tabs + private badge) --- */
504- .repo-nav-bar {
505- display: flex;
506- align-items: center;
507- justify-content: space-between;
508- border-bottom: 1px solid var(--color-border);
509- margin-bottom: var(--space-6);
510- gap: var(--space-4);
511- }
512-
513-
514- /* --- Repo tabs --- */
515- .repo-tabs {
516- display: flex;
517- gap: 0;
518- flex: 1;
519- overflow-x: auto;
520- overflow-y: hidden;
521- }
522- .repo-tab {
523- padding: var(--space-3) var(--space-4);
524- font-size: var(--text-sm);
525- text-decoration: none;
526- color: var(--color-text-muted);
527- border-bottom: 2px solid transparent;
528- margin-bottom: -1px;
529- transition: color 0.1s;
530- }
531- .repo-tab:hover { color: var(--color-text); }
532- .repo-tab.active {
533- color: var(--color-text);
534- font-weight: 600;
535- border-bottom-color: var(--color-accent);
536- }
537- /* Align active indicator with nav-bar border */
538- .repo-tab { margin-bottom: -1px; }
539-
540- /* --- Repo list --- */
541- .repo-list { display: flex; flex-direction: column; gap: var(--space-2); }
542- .repo-card {
543- display: flex;
544- align-items: flex-start;
545- justify-content: space-between;
546- padding: var(--space-4) var(--space-5);
547- border: 1px solid var(--color-border);
548- border-radius: var(--radius-lg);
549- background: var(--color-bg);
550- gap: var(--space-4);
551- transition: border-color 0.15s;
552- }
553- .repo-card:hover { border-color: var(--color-accent); }
554- .repo-card-main { flex: 1; min-width: 0; }
555- .repo-card-title {
556- display: flex;
557- align-items: center;
558- gap: var(--space-2);
559- flex-wrap: wrap;
560- margin-bottom: var(--space-1);
561- }
562- .repo-name {
563- font-size: var(--text-lg);
564- font-weight: 600;
565- color: var(--color-link);
566- text-decoration: none;
567- }
568- .repo-name:hover { text-decoration: underline; }
569- .repo-card-meta {
570- display: flex;
571- flex-direction: column;
572- align-items: flex-end;
573- gap: var(--space-1);
574- font-size: var(--text-xs);
575- color: var(--color-text-muted);
576- white-space: nowrap;
577- }
578- .repo-date { color: var(--color-text-muted); }
579-
580- /* --- Badges --- */
581- .badge {
582- display: inline-flex;
583- align-items: center;
584- padding: 1px var(--space-2);
585- border-radius: var(--radius-full);
586- font-size: var(--text-xs);
587- font-weight: 500;
588- border: 1px solid;
589- }
590- .badge-private {
591- color: var(--color-warning);
592- border-color: var(--color-warning);
593- background: var(--color-warning-bg);
594- }
595- .issue-badge, .patch-badge {
596- display: inline-flex;
597- align-items: center;
598- padding: var(--space-1) var(--space-3);
599- border-radius: var(--radius-full);
600- font-size: var(--text-xs);
601- font-weight: 500;
602- text-transform: capitalize;
603- }
604- .issue-badge.open, .patch-badge.open {
605- background: var(--color-success-bg);
606- color: var(--color-success);
607- }
608- .issue-badge.closed, .patch-badge.closed {
609- background: var(--color-danger-bg);
610- color: var(--color-danger);
611- }
612- .patch-badge.merged {
613- background: var(--color-merged-bg);
614- color: var(--color-merged);
615- }
616-
617- /* --- Empty state --- */
618- .empty-state {
619- text-align: center;
620- padding: var(--space-16) var(--space-4);
621- color: var(--color-text-muted);
622- }
623- .empty-state p { margin-bottom: var(--space-4); font-size: var(--text-lg); }
624-
625- /* --- Breadcrumb --- */
626- .breadcrumb {
627- display: flex;
628- align-items: center;
629- flex-wrap: wrap;
630- gap: var(--space-1);
631- font-size: var(--text-sm);
632- margin-bottom: var(--space-4);
633- }
634- .breadcrumb a { color: var(--color-link); text-decoration: none; }
635- .breadcrumb a:hover { text-decoration: underline; }
636- .breadcrumb-sep { color: var(--color-text-muted); }
637- .breadcrumb-current { color: var(--color-text-muted); }
638-
639- /* --- File tree --- */
640- .file-tree {
641- width: 100%;
642- border: 1px solid var(--color-border);
643- border-radius: var(--radius-lg);
644- border-collapse: collapse;
645- overflow: hidden;
646- margin-bottom: var(--space-6);
647- }
648- .file-tree-row {
649- border-bottom: 1px solid var(--color-border-muted);
650- }
651- .file-tree-row:last-child { border-bottom: none; }
652- .file-tree-row:hover { background: var(--color-bg-subtle); }
653- .file-icon, .file-name, .file-size {
654- padding: var(--space-2) var(--space-3);
655- font-size: var(--text-sm);
656- vertical-align: middle;
657- }
658- .file-icon {
659- width: 36px;
660- padding-right: var(--space-1);
661- text-align: center;
662- line-height: 0;
663- }
664- .file-name { width: 100%; }
665- .file-name a { color: var(--color-link); text-decoration: none; }
666- .file-name a:hover { text-decoration: underline; }
667- .file-name a.file-name-dir { font-weight: 500; }
668- .file-size {
669- text-align: right;
670- color: var(--color-text-muted);
671- font-size: var(--text-xs);
672- white-space: nowrap;
673- padding-right: var(--space-4);
674- }
675- .file-icon-dir { color: var(--color-link); }
676- .file-icon-file { color: var(--color-text-muted); }
677- .tree-icon { display: inline-block; vertical-align: text-bottom; }
678- .file-tree-row-up .file-name a { color: var(--color-text-muted); }
679- .file-tree-row-up .file-name a:hover { color: var(--color-link); }
680-
681- /* --- File blob --- */
682- .file-blob-header {
683- display: flex;
684- align-items: center;
685- justify-content: space-between;
686- padding: var(--space-3) var(--space-4);
687- background: var(--color-bg-subtle);
688- border: 1px solid var(--color-border);
689- border-radius: var(--radius-lg) var(--radius-lg) 0 0;
690- }
691- .file-blob-name { font-size: var(--text-sm); font-weight: 500; }
692- .file-blob-body {
693- border: 1px solid var(--color-border);
694- border-top: none;
695- border-radius: 0 0 var(--radius-lg) var(--radius-lg);
696- overflow: auto;
697- }
698- .file-blob-body .shiki-wrapper pre {
699- margin: 0;
700- padding: var(--space-4);
701- font-size: var(--text-sm);
702- line-height: 1.6;
703- min-width: max-content;
704- }
705- .file-download-notice {
706- padding: var(--space-8);
707- text-align: center;
708- color: var(--color-text-muted);
709- }
710- .file-download-notice p { margin-bottom: var(--space-4); }
711-
712- /* --- Commit list --- */
713- .commit-list { display: flex; flex-direction: column; }
714- .commit-item {
715- display: flex;
716- align-items: flex-start;
717- justify-content: space-between;
718- padding: var(--space-3) 0;
719- border-bottom: 1px solid var(--color-border-muted);
720- gap: var(--space-4);
721- flex-wrap: wrap;
722- }
723- .commit-item:last-child { border-bottom: none; }
724- .commit-main { flex: 1; min-width: 0; }
725- .commit-subject {
726- color: var(--color-text);
727- text-decoration: none;
728- font-size: var(--text-sm);
729- font-weight: 500;
730- }
731- .commit-subject:hover { color: var(--color-link); }
732- .commit-meta {
733- display: flex;
734- align-items: center;
735- gap: var(--space-3);
736- font-size: var(--text-xs);
737- color: var(--color-text-muted);
738- white-space: nowrap;
739- }
740- .commit-hash {
741- font-family: var(--font-mono);
742- color: var(--color-link);
743- text-decoration: none;
744- background: var(--color-bg-inset);
745- padding: 1px var(--space-2);
746- border-radius: var(--radius-sm);
747- }
748- .commit-hash:hover { text-decoration: underline; }
749- .mono { font-family: var(--font-mono); }
750-
751- /* --- Commit detail viewer --- */
752- .commit-page-top {
753- display: flex;
754- align-items: center;
755- justify-content: space-between;
756- margin-bottom: var(--space-4);
757- }
758- .commit-card {
759- border: 1px solid var(--color-border);
760- border-radius: var(--radius-lg);
761- margin-bottom: var(--space-4);
762- overflow: hidden;
763- }
764- .commit-card-subject {
765- font-size: var(--text-xl);
766- font-weight: 600;
767- padding: var(--space-5) var(--space-6);
768- border-bottom: 1px solid var(--color-border);
769- line-height: 1.3;
770- }
771- .commit-card-body {
772- padding: var(--space-4) var(--space-6);
773- font-family: inherit;
774- font-size: var(--text-sm);
775- color: var(--color-text-muted);
776- border-bottom: 1px solid var(--color-border);
777- overflow-x: auto;
778- }
779- .commit-card-meta {
780- padding: var(--space-4) var(--space-6);
781- display: flex;
782- flex-direction: column;
783- gap: var(--space-2);
784- background: var(--color-bg-subtle);
785- }
786- .commit-card-meta-row {
787- display: flex;
788- align-items: baseline;
789- gap: var(--space-3);
790- font-size: var(--text-sm);
791- flex-wrap: wrap;
792- }
793- .commit-meta-label {
794- font-weight: 600;
795- color: var(--color-text-muted);
796- min-width: 5rem;
797- font-size: var(--text-xs);
798- text-transform: uppercase;
799- letter-spacing: 0.04em;
800- }
801- .commit-sha-full {
802- font-size: var(--text-xs);
803- background: var(--color-bg-inset);
804- padding: 2px var(--space-2);
805- border-radius: var(--radius-sm);
806- word-break: break-all;
807- }
808- .commit-stats-bar {
809- font-size: var(--text-sm);
810- color: var(--color-text-muted);
811- margin-bottom: var(--space-4);
812- }
813-
814- /* --- Commit layout (sidebar + diffs) --- */
815- .commit-layout {
816- display: grid;
817- grid-template-columns: 1fr;
818- gap: var(--space-5);
819- align-items: start;
820- }
821- .commit-diffs { min-width: 0; }
822- @media (min-width: 900px) {
823- .commit-layout { grid-template-columns: 220px 1fr; }
824- .commit-layout:has(.file-nav-details:not([open])) { grid-template-columns: auto 1fr; }
825- }
826-
827- /* --- File nav sidebar --- */
828- .commit-file-nav {
829- position: sticky;
830- top: calc(56px + var(--space-4));
831- max-height: calc(100vh - 140px);
832- overflow-y: auto;
833- }
834- .file-nav-details {
835- border: 1px solid var(--color-border);
836- border-radius: var(--radius-lg);
837- background: var(--color-bg-subtle);
838- overflow: hidden;
839- }
840- .file-nav-toggle {
841- display: flex;
842- align-items: center;
843- gap: var(--space-2);
844- padding: var(--space-3) var(--space-4);
845- font-size: var(--text-sm);
846- font-weight: 500;
847- cursor: pointer;
848- list-style: none;
849- user-select: none;
850- border-bottom: 1px solid var(--color-border);
851- }
852- .file-nav-toggle::-webkit-details-marker { display: none; }
853- .file-nav-details:not([open]) .file-nav-toggle { border-bottom: none; }
854- .file-nav-toggle-icon {
855- font-size: 1em;
856- transition: transform 0.15s;
857- }
858- .file-nav-details:not([open]) .file-nav-toggle-icon { transform: rotate(-90deg); }
859- .file-nav-list {
860- padding: var(--space-1) 0;
861- list-style: none;
862- }
863- .file-nav-item {
864- display: flex;
865- align-items: center;
866- gap: var(--space-2);
867- padding: var(--space-1) var(--space-3);
868- font-size: var(--text-xs);
869- text-decoration: none;
870- color: var(--color-text);
871- border-radius: 0;
872- overflow: hidden;
873- }
874- .file-nav-item:hover { background: var(--color-bg-inset); }
875- .file-nav-status {
876- flex-shrink: 0;
877- width: 16px;
878- height: 16px;
879- display: inline-flex;
880- align-items: center;
881- justify-content: center;
882- border-radius: var(--radius-sm);
883- font-size: 9px;
884- font-weight: 700;
885- font-family: var(--font-mono);
886- }
887- .file-status-added { background: var(--color-success-bg); color: var(--color-success); }
888- .file-status-deleted { background: var(--color-danger-bg); color: var(--color-danger); }
889- .file-status-modified { background: var(--color-accent-bg); color: var(--color-accent); }
890- .file-status-renamed { background: var(--color-merged-bg); color: var(--color-merged); }
891- .file-status-copied { background: var(--color-merged-bg); color: var(--color-merged); }
892- .file-nav-name {
893- flex: 1;
894- overflow: hidden;
895- text-overflow: ellipsis;
896- white-space: nowrap;
897- font-family: var(--font-mono);
898- }
899- .file-nav-stat {
900- flex-shrink: 0;
901- display: flex;
902- gap: var(--space-1);
903- font-size: var(--text-xs);
904- font-family: var(--font-mono);
905- }
906- .nav-add { color: var(--color-success); }
907- .nav-del { color: var(--color-danger); }
908- .file-nav-dir > details > .file-nav-list { padding-left: var(--space-4); }
909- .file-nav-dir-toggle {
910- display: flex;
911- align-items: center;
912- gap: var(--space-1);
913- padding: var(--space-1) var(--space-3);
914- font-size: var(--text-xs);
915- font-family: var(--font-mono);
916- color: var(--color-text-muted);
917- cursor: pointer;
918- list-style: none;
919- user-select: none;
920- }
921- .file-nav-dir-toggle::-webkit-details-marker { display: none; }
922- .file-nav-dir > details:not([open]) .file-nav-toggle-icon { transform: rotate(-90deg); }
923-
924- /* --- Diff file cards --- */
925- .diff-file {
926- border: 1px solid var(--color-border);
927- border-radius: var(--radius-lg);
928- margin-bottom: var(--space-4);
929- overflow: hidden;
930- }
931- .diff-file-header {
932- display: flex;
933- align-items: center;
934- justify-content: space-between;
935- gap: var(--space-3);
936- padding: var(--space-3) var(--space-4);
937- background: var(--color-bg-subtle);
938- cursor: pointer;
939- list-style: none;
940- user-select: none;
941- flex-wrap: wrap;
942- }
943- .diff-file-header::-webkit-details-marker { display: none; }
944- .diff-file-header-left {
945- display: flex;
946- align-items: center;
947- gap: var(--space-2);
948- min-width: 0;
949- flex: 1;
950- }
951- .diff-file-header-right {
952- display: flex;
953- align-items: center;
954- gap: var(--space-2);
955- flex-shrink: 0;
956- flex-wrap: wrap;
957- }
958- .diff-file-toggle-icon {
959- font-size: 0.7em;
960- flex-shrink: 0;
961- transition: transform 0.15s;
962- }
963- .diff-file:not([open]) .diff-file-toggle-icon { transform: rotate(-90deg); }
964- .diff-status-badge {
965- flex-shrink: 0;
966- width: 18px;
967- height: 18px;
968- display: inline-flex;
969- align-items: center;
970- justify-content: center;
971- border-radius: var(--radius-sm);
972- font-size: 10px;
973- font-weight: 700;
974- font-family: var(--font-mono);
975- }
976- .diff-status-added { background: var(--color-success-bg); color: var(--color-success); }
977- .diff-status-deleted { background: var(--color-danger-bg); color: var(--color-danger); }
978- .diff-status-modified { background: var(--color-accent-bg); color: var(--color-accent); }
979- .diff-status-renamed { background: var(--color-merged-bg); color: var(--color-merged); }
980- .diff-status-copied { background: var(--color-merged-bg); color: var(--color-merged); }
981- .diff-file-path {
982- font-size: var(--text-sm);
983- font-weight: 500;
984- overflow: hidden;
985- text-overflow: ellipsis;
986- white-space: nowrap;
987- }
988- .diff-rename-arrow {
989- font-size: var(--text-xs);
990- color: var(--color-text-muted);
991- white-space: nowrap;
992- overflow: hidden;
993- text-overflow: ellipsis;
994- }
995- .diff-stat-add { color: var(--color-success); font-size: var(--text-sm); font-family: var(--font-mono); font-weight: 600; }
996- .diff-stat-del { color: var(--color-danger); font-size: var(--text-sm); font-family: var(--font-mono); font-weight: 600; }
997- .btn-xs {
998- padding: 1px var(--space-2);
999- font-size: var(--text-xs);
1000- border: 1px solid var(--color-border);
1001- border-radius: var(--radius-sm);
1002- background: var(--color-bg);
1003- color: var(--color-text);
1004- text-decoration: none;
1005- cursor: pointer;
1006- white-space: nowrap;
1007- font-family: var(--font-mono);
1008- display: inline-flex;
1009- align-items: center;
1010- transition: background 0.1s;
1011- }
1012- .btn-xs:hover { background: var(--color-bg-inset); }
1013-
1014- /* --- Diff file body --- */
1015- .diff-file-body { }
1016- .diff-binary-notice {
1017- padding: var(--space-4) var(--space-6);
1018- font-size: var(--text-sm);
1019- color: var(--color-text-muted);
1020- font-style: italic;
1021- }
1022- .diff-hunk { border-top: 1px solid var(--color-border-muted); overflow-x: auto; }
1023- .diff-hunk:first-child { border-top: none; }
1024- .diff-hunk-header {
1025- padding: var(--space-1) var(--space-4);
1026- font-family: var(--font-mono);
1027- font-size: var(--text-xs);
1028- font-weight: normal;
1029- text-align: left;
1030- background: color-mix(in srgb, var(--color-merged-bg) 50%, var(--color-bg));
1031- color: var(--color-merged);
1032- border-bottom: 1px solid var(--color-border-muted);
1033- white-space: pre;
1034- }
1035-
1036- /* --- Diff table --- */
1037- .diff-table {
1038- min-width: 100%;
1039- border-collapse: collapse;
1040- font-family: var(--font-mono);
1041- font-size: var(--text-xs);
1042- line-height: 1.5;
1043- }
1044- .diff-ln {
1045- width: 44px;
1046- min-width: 44px;
1047- text-align: right;
1048- padding: 0 var(--space-2);
1049- color: var(--color-text-muted);
1050- border-right: 1px solid var(--color-border-muted);
1051- user-select: none;
1052- vertical-align: top;
1053- white-space: nowrap;
1054- }
1055- .diff-sign {
1056- width: 18px;
1057- min-width: 18px;
1058- text-align: center;
1059- padding: 0 var(--space-1);
1060- user-select: none;
1061- vertical-align: top;
1062- }
1063- .diff-code {
1064- padding: 0 var(--space-3);
1065- white-space: pre;
1066- overflow: visible;
1067- vertical-align: top;
1068- word-break: normal;
1069- }
1070- /* Row type backgrounds */
1071- .diff-row-add { background: color-mix(in srgb, var(--color-success-bg) 70%, transparent); }
1072- .diff-row-del { background: color-mix(in srgb, var(--color-danger-bg) 70%, transparent); }
1073- .diff-row-add .diff-ln,
1074- .diff-row-add .diff-sign { background: color-mix(in srgb, var(--color-success-bg) 90%, transparent); }
1075- .diff-row-del .diff-ln,
1076- .diff-row-del .diff-sign { background: color-mix(in srgb, var(--color-danger-bg) 90%, transparent); }
1077- .diff-row-add .diff-sign { color: var(--color-success); font-weight: 700; }
1078- .diff-row-del .diff-sign { color: var(--color-danger); font-weight: 700; }
1079-
1080- /* Shiki dark-mode support in diff code cells */
1081- @media (prefers-color-scheme: dark) {
1082- .diff-code span[style] { color: var(--shiki-dark) !important; }
1083- }
1084-
1085- /* Shiki dark-mode support in file blob view */
1086- @media (prefers-color-scheme: dark) {
1087- .shiki-wrapper .shiki { background-color: var(--shiki-dark-bg) !important; color: var(--shiki-dark) !important; }
1088- .shiki-wrapper .shiki span { color: var(--shiki-dark) !important; }
1089- }
1090-
1091- /* --- Issue / Patch list --- */
1092- .list-header {
1093- display: flex;
1094- align-items: center;
1095- justify-content: space-between;
1096- padding: var(--space-3) var(--space-4);
1097- background: var(--color-bg-subtle);
1098- border: 1px solid var(--color-border);
1099- border-radius: var(--radius-lg) var(--radius-lg) 0 0;
1100- gap: var(--space-4);
1101- }
1102- .list-header-tabs { display: flex; gap: var(--space-2); }
1103- .list-tab {
1104- padding: var(--space-1) var(--space-3);
1105- border-radius: var(--radius-md);
1106- font-size: var(--text-sm);
1107- text-decoration: none;
1108- color: var(--color-text-muted);
1109- }
1110- .list-tab:hover { background: var(--color-bg-inset); color: var(--color-text); }
1111- .list-tab.active {
1112- background: var(--color-bg-inset);
1113- color: var(--color-text);
1114- font-weight: 600;
1115- }
1116- .issue-list { border: 1px solid var(--color-border); border-top: none; border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
1117- .issue-item {
1118- padding: var(--space-4) var(--space-4);
1119- border-bottom: 1px solid var(--color-border-muted);
1120- }
1121- .issue-item:last-child { border-bottom: none; }
1122- .issue-main {
1123- display: flex;
1124- align-items: center;
1125- gap: var(--space-2);
1126- margin-bottom: var(--space-1);
1127- flex-wrap: wrap;
1128- }
1129- .issue-title {
1130- font-weight: 500;
1131- text-decoration: none;
1132- color: var(--color-text);
1133- flex: 1;
1134- }
1135- .issue-title:hover { color: var(--color-link); }
1136- .issue-meta {
1137- font-size: var(--text-xs);
1138- color: var(--color-text-muted);
1139- display: flex;
1140- gap: var(--space-3);
1141- }
1142- .issue-status-dot, .patch-status-dot {
1143- width: 8px;
1144- height: 8px;
1145- border-radius: 50%;
1146- flex-shrink: 0;
1147- }
1148- .issue-status-dot.open, .patch-status-dot.open { background: var(--color-success); }
1149- .issue-status-dot.closed, .patch-status-dot.closed { background: var(--color-danger); }
1150- .patch-status-dot.merged { background: var(--color-merged); }
1151-
1152- /* --- Issue detail / Timeline --- */
1153- .issue-detail-header {
1154- display: flex;
1155- align-items: center;
1156- gap: var(--space-3);
1157- margin-bottom: var(--space-6);
1158- flex-wrap: wrap;
1159- }
1160- .issue-number {
1161- font-size: var(--text-2xl);
1162- font-weight: 600;
1163- color: var(--color-text-muted);
1164- white-space: nowrap;
1165- }
1166- .issue-detail-title {
1167- font-size: var(--text-2xl);
1168- font-weight: 600;
1169- flex: 1;
1170- }
1171- .issue-detail-meta-actions { margin-left: auto; display: flex; gap: var(--space-2); }
1172- .timeline-item {
1173- border: 1px solid var(--color-border);
1174- border-radius: var(--radius-lg);
1175- margin-bottom: var(--space-4);
1176- overflow: hidden;
1177- }
1178- .timeline-author {
1179- background: var(--color-bg-subtle);
1180- padding: var(--space-3) var(--space-4);
1181- font-size: var(--text-sm);
1182- display: flex;
1183- align-items: center;
1184- justify-content: space-between;
1185- flex-wrap: wrap;
1186- gap: var(--space-3);
1187- border-bottom: 1px solid var(--color-border);
1188- }
1189- .timeline-author time { color: var(--color-text-muted); font-size: var(--text-xs); }
1190- .timeline-author-right { margin-left: auto; display: flex; align-items: center; gap: var(--space-2); }
1191- .edited-indicator { color: var(--color-text-muted); font-size: var(--text-xs); cursor: default; }
1192-
1193- /* --- Avatars --- */
1194- .avatar {
1195- display: inline-flex;
1196- border-radius: 4px;
1197- overflow: hidden;
1198- flex-shrink: 0;
1199- vertical-align: middle;
1200- }
1201- .avatar-img {
1202- width: 100%;
1203- height: 100%;
1204- object-fit: cover;
1205- display: block;
1206- color: transparent;
1207- }
1208- .avatar-placeholder {
1209- display: inline-flex;
1210- align-items: center;
1211- justify-content: center;
1212- border-radius: 4px;
1213- background: var(--color-bg-inset);
1214- color: var(--color-text-muted);
1215- font-weight: 600;
1216- flex-shrink: 0;
1217- vertical-align: middle;
1218- }
1219- .avatar-settings {
1220- display: flex;
1221- align-items: flex-start;
1222- gap: var(--space-5);
1223- }
1224- .avatar-actions {
1225- display: flex;
1226- flex-direction: column;
1227- gap: var(--space-3);
1228- }
1229- .nav-user {
1230- display: inline-flex;
1231- align-items: center;
1232- gap: var(--space-2);
1233- }
1234- .timeline-body { padding: var(--space-4); }
1235- .timeline-item-new { padding: var(--space-4); }
1236- .timeline-item-new .section-title { font-size: var(--text-base); border: none; padding-bottom: 0; }
1237- /* Edit <details> (summary only) in .timeline-author; form is a sibling shown via :has() */
1238- .timeline-author-right .inline-edit-details { border: none; }
1239- .timeline-author-right .inline-edit-details > summary { padding: 0; list-style: none; }
1240- .timeline-author-right .inline-edit-details > summary::-webkit-details-marker { display: none; }
1241- .inline-edit-details .when-open { display: none; }
1242- .inline-edit-details[open] .when-closed { display: none; }
1243- .inline-edit-details[open] .when-open { display: inline; }
1244- .inline-edit-form-area { display: none; border-top: 1px solid var(--color-border-muted); }
1245- .timeline-item:has(.inline-edit-details[open]) .inline-edit-form-area { display: block; }
1246- .inline-edit-form { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
1247-
1248- /* --- Reactions --- */
1249- .reaction-bar {
1250- display: flex;
1251- flex-wrap: wrap;
1252- gap: var(--space-2);
1253- padding: var(--space-2) var(--space-4);
1254- border-top: 1px solid var(--color-border-muted);
1255- }
1256- .reaction-form { display: inline; }
1257- .reaction-btn {
1258- display: inline-flex;
1259- align-items: center;
1260- gap: var(--space-1);
1261- padding: var(--space-1) var(--space-3);
1262- border: 1px solid var(--color-border);
1263- border-radius: var(--radius-full);
1264- background: var(--color-bg-subtle);
1265- color: var(--color-text);
1266- font-size: var(--text-sm);
1267- cursor: pointer;
1268- transition: background 0.1s;
1269- }
1270- .reaction-btn:hover { background: var(--color-bg-inset); }
1271- .reaction-btn.reacted {
1272- background: var(--color-accent-bg);
1273- border-color: var(--color-accent);
1274- }
1275- .reaction-picker {
1276- position: relative;
1277- }
1278- .reaction-add-btn {
1279- display: inline-flex;
1280- align-items: center;
1281- justify-content: center;
1282- width: 28px;
1283- height: 28px;
1284- border: 1px dashed var(--color-border);
1285- border-radius: var(--radius-full);
1286- background: transparent;
1287- cursor: pointer;
1288- font-size: var(--text-base);
1289- color: var(--color-text-muted);
1290- list-style: none;
1291- }
1292- .reaction-add-btn::-webkit-details-marker { display: none; }
1293- .reaction-picker-dropdown {
1294- position: absolute;
1295- bottom: calc(100% + var(--space-2));
1296- left: 0;
1297- background: var(--color-bg);
1298- border: 1px solid var(--color-border);
1299- border-radius: var(--radius-lg);
1300- padding: var(--space-2);
1301- display: flex;
1302- gap: var(--space-1);
1303- z-index: 10;
1304- }
1305- .reaction-picker-btn {
1306- background: none;
1307- border: none;
1308- font-size: 1.25rem;
1309- cursor: pointer;
1310- padding: var(--space-1);
1311- border-radius: var(--radius-sm);
1312- }
1313- .reaction-picker-btn:hover { background: var(--color-bg-inset); }
1314-
1315- /* --- Patch subview tabs --- */
1316- .patch-tab-nav {
1317- display: flex;
1318- border-bottom: 1px solid var(--color-border);
1319- margin-bottom: var(--space-6);
1320- }
1321- .tab-count {
1322- display: inline-flex;
1323- align-items: center;
1324- justify-content: center;
1325- min-width: 1.4em;
1326- padding: 0 var(--space-1);
1327- margin-left: var(--space-1);
1328- font-size: var(--text-xs);
1329- font-weight: 500;
1330- background: var(--color-bg-inset);
1331- border-radius: 999px;
1332- color: var(--color-text-muted);
1333- }
1334-
1335- /* --- Patch apply status --- */
1336- .apply-status {
1337- margin: var(--space-4) 0;
1338- }
1339- .apply-result {
1340- display: flex;
1341- align-items: flex-start;
1342- gap: var(--space-2);
1343- padding: var(--space-3) var(--space-4);
1344- border-radius: var(--radius-md);
1345- font-size: var(--text-sm);
1346- flex-direction: column;
1347- }
1348- .apply-result > div { display: flex; gap: var(--space-2); align-items: center; }
1349- .apply-clean {
1350- background: var(--color-success-bg);
1351- color: var(--color-success);
1352- border: 1px solid var(--color-success);
1353- }
1354- .apply-conflict {
1355- background: var(--color-danger-bg);
1356- color: var(--color-danger);
1357- border: 1px solid var(--color-danger);
1358- }
1359- .apply-checking, .apply-unknown {
1360- color: var(--color-text-muted);
1361- font-style: italic;
1362- font-size: var(--text-sm);
1363- }
1364- .apply-output {
1365- font-family: var(--font-mono);
1366- font-size: var(--text-xs);
1367- white-space: pre-wrap;
1368- margin-top: var(--space-2);
1369- padding: var(--space-2);
1370- background: rgba(0,0,0,0.05);
1371- border-radius: var(--radius-sm);
1372- width: 100%;
1373- }
1374- .patch-actions {
1375- display: flex;
1376- gap: var(--space-3);
1377- margin: var(--space-4) 0;
1378- flex-wrap: wrap;
1379- }
1380- .patch-card-title-row {
1381- display: flex;
1382- align-items: flex-start;
1383- gap: var(--space-3);
1384- justify-content: space-between;
1385- margin-bottom: var(--space-3);
1386- }
1387- .patch-card-title-row .commit-card-subject { margin-bottom: 0; }
1388- .patch-card-description {
1389- margin-top: var(--space-4);
1390- padding-top: var(--space-4);
1391- border-top: 1px solid var(--color-border-muted);
1392- }
1393-
1394- /* --- Repo home layout --- */
1395- .repo-home-layout {
1396- display: grid;
1397- gap: var(--space-8);
1398- }
1399- @media (min-width: 768px) {
1400- .repo-home-layout {
1401- grid-template-columns: 1fr 1fr;
1402- }
1403- }
1404- .readme-section {
1405- border: 1px solid var(--color-border);
1406- border-radius: var(--radius-lg);
1407- overflow: hidden;
1408- }
1409- .readme-header {
1410- background: var(--color-bg-subtle);
1411- padding: var(--space-3) var(--space-4);
1412- font-size: var(--text-sm);
1413- font-weight: 500;
1414- border-bottom: 1px solid var(--color-border);
1415- }
1416- .readme-section .markdown-body {
1417- padding: var(--space-4);
1418- }
1419-
1420- /* --- Code setup block --- */
1421- .code-setup {
1422- background: var(--color-bg-subtle);
1423- border: 1px solid var(--color-border);
1424- border-radius: var(--radius-lg);
1425- padding: var(--space-4);
1426- font-size: var(--text-sm);
1427- margin-top: var(--space-4);
1428- overflow: auto;
1429- text-align: left;
1430- }
1431- .code-plain {
1432- background: var(--color-bg-subtle);
1433- padding: var(--space-4);
1434- overflow: auto;
1435- font-size: var(--text-sm);
1436- line-height: 1.6;
1437- }
1438-
1439- /* --- Markdown body --- */
1440- .markdown-body {
1441- font-size: var(--text-sm);
1442- line-height: 1.6;
1443- color: var(--color-text);
1444- }
1445- .markdown-body h1, .markdown-body h2, .markdown-body h3,
1446- .markdown-body h4, .markdown-body h5, .markdown-body h6 {
1447- margin-top: var(--space-6);
1448- margin-bottom: var(--space-3);
1449- }
1450- .markdown-body p { margin-bottom: var(--space-4); }
1451- .markdown-body a { color: var(--color-link); }
1452- .markdown-body code {
1453- background: var(--color-bg-inset);
1454- padding: 0.1em 0.3em;
1455- border-radius: var(--radius-sm);
1456- font-size: 0.9em;
1457- }
1458- .markdown-body pre {
1459- background: var(--color-bg-subtle);
1460- border: 1px solid var(--color-border);
1461- border-radius: var(--radius-md);
1462- padding: var(--space-4);
1463- overflow: auto;
1464- margin-bottom: var(--space-4);
1465- }
1466- .markdown-body pre code { background: none; padding: 0; }
1467- .markdown-body ul, .markdown-body ol {
1468- padding-left: var(--space-6);
1469- margin-bottom: var(--space-4);
1470- }
1471- .markdown-body ul { list-style: disc; }
1472- .markdown-body ol { list-style: decimal; }
1473- .markdown-body li { margin-bottom: var(--space-1); }
1474- .markdown-body blockquote {
1475- border-left: 3px solid var(--color-border);
1476- padding-left: var(--space-4);
1477- color: var(--color-text-muted);
1478- margin: var(--space-4) 0;
1479- }
1480- .markdown-body table {
1481- width: 100%;
1482- border-collapse: collapse;
1483- margin-bottom: var(--space-4);
1484- }
1485- .markdown-body th, .markdown-body td {
1486- padding: var(--space-2) var(--space-3);
1487- border: 1px solid var(--color-border);
1488- }
1489- .markdown-body th { background: var(--color-bg-subtle); font-weight: 600; }
1490- .markdown-body hr {
1491- border: none;
1492- border-top: 1px solid var(--color-border);
1493- margin: var(--space-6) 0;
1494- }
1495- .markdown-body img { max-width: 100%; border-radius: var(--radius-md); }
1496- .markdown-body details { margin-bottom: var(--space-4); }
1497- .markdown-body summary { cursor: pointer; font-weight: 500; }
1498-
1499- /* --- Search form --- */
1500- .search-form { margin-bottom: var(--space-5); }
1501- .search-input-wrap {
1502- display: flex;
1503- gap: var(--space-2);
1504- max-width: 480px;
1505- }
1506- .search-input {
1507- flex: 1;
1508- padding: var(--space-2) var(--space-3);
1509- border: 1px solid var(--color-border);
1510- border-radius: var(--radius-md);
1511- background: var(--color-bg);
1512- color: var(--color-text);
1513- font-size: var(--text-sm);
1514- }
1515- .search-input:focus {
1516- outline: 2px solid var(--color-accent);
1517- outline-offset: -1px;
1518- border-color: var(--color-accent);
1519- }
1520-
1521- /* --- Issue close bar --- */
1522- .issue-close-bar {
1523- display: flex;
1524- justify-content: flex-end;
1525- padding: var(--space-3) 0;
1526- border-top: 1px solid var(--color-border-muted);
1527- margin-top: var(--space-2);
1528- }
1529-
1530- /* --- Auth divider --- */
1531- .auth-divider {
1532- display: flex;
1533- align-items: center;
1534- gap: var(--space-3);
1535- margin: var(--space-4) 0;
1536- color: var(--color-text-muted);
1537- font-size: var(--text-sm);
1538- }
1539- .auth-divider::before,
1540- .auth-divider::after {
1541- content: '';
1542- flex: 1;
1543- height: 1px;
1544- background: var(--color-border);
1545- }
1546-
1547- /* --- Passkey UI in settings --- */
1548- .passkey-actions {
1549- display: flex;
1550- flex-direction: column;
1551- gap: var(--space-3);
1552- align-items: flex-start;
1553- margin-top: var(--space-3);
1554- }
1555- .passkey-status { font-size: var(--text-sm); }
1556-
1557- /* --- Danger zone --- */
1558- .danger-zone { margin-top: var(--space-8); }
1559- .danger-title { color: var(--color-danger); border-bottom-color: var(--color-danger); }
1560- .danger-card {
1561- border-color: var(--color-danger);
1562- background: var(--color-danger-bg);
1563- }
1564- .danger-item {
1565- display: flex;
1566- align-items: center;
1567- justify-content: space-between;
1568- gap: var(--space-4);
1569- flex-wrap: wrap;
1570- }
1571- .danger-item p { margin: var(--space-1) 0 0; }
1572- .btn-danger {
1573- background: var(--color-danger);
1574- border-color: transparent;
1575- color: #fff;
1576- }
1577- .btn-danger:hover { filter: brightness(0.9); }
1578-
1579- /* --- Release list items --- */
1580- .release-item-header {
1581- display: flex;
1582- gap: var(--space-3);
1583- align-items: flex-start;
1584- }
1585- .release-item-main { flex: 1; min-width: 0; }
1586- .release-item-title {
1587- font-size: var(--text-lg);
1588- font-weight: 700;
1589- text-decoration: none;
1590- color: var(--color-text);
1591- display: block;
1592- margin-bottom: var(--space-1);
1593- }
1594- .release-item-title:hover { color: var(--color-link); }
1595- .release-item-meta {
1596- display: flex;
1597- flex-wrap: wrap;
1598- gap: var(--space-3);
1599- align-items: center;
1600- font-size: var(--text-xs);
1601- color: var(--color-text-muted);
1602- }
1603- .release-item-date {
1604- display: flex;
1605- flex-direction: column;
1606- align-items: flex-end;
1607- gap: var(--space-2);
1608- font-size: var(--text-xs);
1609- color: var(--color-text-muted);
1610- white-space: nowrap;
1611- flex-shrink: 0;
1612- }
1613-
1614- /* --- Release notes preview in list --- */
1615- .release-notes-section { margin-top: var(--space-3); }
1616- .release-notes-label {
1617- font-size: var(--text-xs);
1618- font-weight: 600;
1619- color: var(--color-text-muted);
1620- text-transform: uppercase;
1621- letter-spacing: 0.05em;
1622- margin-bottom: var(--space-2);
1623- }
1624- .notes-expand > summary { list-style: none; cursor: pointer; outline: none; }
1625- .notes-expand > summary::-webkit-details-marker { display: none; }
1626- .notes-preview {
1627- max-height: calc(5 * 1.6em);
1628- overflow: hidden;
1629- position: relative;
1630- white-space: pre-wrap;
1631- font-size: var(--text-sm);
1632- color: var(--color-text);
1633- line-height: 1.6;
1634- word-break: break-word;
1635- margin-bottom: var(--space-1);
1636- }
1637- .notes-preview::after {
1638- content: "";
1639- position: absolute;
1640- bottom: 0;
1641- left: 0;
1642- right: 0;
1643- height: 2.5em;
1644- background: linear-gradient(transparent, var(--color-bg));
1645- pointer-events: none;
1646- }
1647- .notes-expand[open] .notes-preview { display: none; }
1648- .notes-expand[open] { display: flex; flex-direction: column; }
1649- .notes-expand[open] > summary { order: 1; }
1650- .notes-expand[open] > .notes-full { order: 0; }
1651- .notes-toggle-label {
1652- display: inline-block;
1653- font-size: var(--text-xs);
1654- color: var(--color-link);
1655- margin-top: var(--space-1);
1656- }
1657- .notes-toggle-label:hover { text-decoration: underline; }
1658- .notes-toggle-label::before { content: "Show more ↓"; }
1659- .notes-expand[open] .notes-toggle-label::before { content: "Show less ↑"; }
1660- .notes-full { padding-top: var(--space-3); padding-bottom: var(--space-1); }
1661-
1662- /* --- Release detail --- */
1663- .release-detail { padding: var(--space-6) 0; }
1664- .release-header { margin-bottom: var(--space-6); }
1665- .release-notes { margin-bottom: var(--space-6); }
1666- .release-assets { margin-bottom: var(--space-6); }
1667- .release-assets-heading {
1668- font-size: var(--text-base);
1669- font-weight: 600;
1670- margin-bottom: var(--space-3);
1671- }
1672- .asset-list {
1673- border: 1px solid var(--color-border);
1674- border-radius: var(--radius-lg);
1675- overflow: hidden;
1676- }
1677- .asset-item {
1678- display: flex;
1679- align-items: center;
1680- gap: var(--space-4);
1681- padding: var(--space-3) var(--space-4);
1682- border-bottom: 1px solid var(--color-border-muted);
1683- font-size: var(--text-sm);
1684- }
1685- .asset-item:last-child { border-bottom: none; }
1686- .asset-name {
1687- flex: 1;
1688- color: var(--color-link);
1689- text-decoration: none;
1690- font-family: var(--font-mono);
1691- font-size: var(--text-xs);
1692- }
1693- .asset-name:hover { text-decoration: underline; }
1694- .asset-name-pending {
1695- color: var(--color-text-muted);
1696- cursor: default;
1697- }
1698- .asset-item-pending { opacity: 0.6; }
1699- .asset-size, .asset-meta {
1700- color: var(--color-text-muted);
1701- font-size: var(--text-xs);
1702- white-space: nowrap;
1703- }
1704- .release-back { margin-top: var(--space-4); display: flex; gap: var(--space-3); align-items: center; justify-content: space-between; }
1705-}
1706-
1707-/* ============================================================
1708- @layer utilities
1709- ============================================================ */
1710-@layer utilities {
1711- .text-muted { color: var(--color-text-muted); }
1712- .text-sm { font-size: var(--text-sm); }
1713- .text-xs { font-size: var(--text-xs); }
1714- .font-mono { font-family: var(--font-mono); }
1715- .mt-1 { margin-top: var(--space-1); }
1716- .mt-2 { margin-top: var(--space-2); }
1717- .mt-4 { margin-top: var(--space-4); }
1718- .mt-6 { margin-top: var(--space-6); }
1719- .mb-4 { margin-bottom: var(--space-4); }
1720- .flex { display: flex; }
1721- .gap-2 { gap: var(--space-2); }
1722- .gap-4 { gap: var(--space-4); }
1723- .items-center { align-items: center; }
1724- .justify-between { justify-content: space-between; }
1725- .hidden { display: none; }
1726-}
1727-
1728-/* ============================================================
1729- Pagination
1730- ============================================================ */
1731-.commit-cursor-nav {
1732- display: flex;
1733- justify-content: space-between;
1734- border-top: 1px solid var(--color-border);
1735- margin-top: var(--space-2);
1736- padding: var(--space-4) 0 var(--space-2);
1737-}
1738-
1739-.commit-cursor-prev { flex: 1; }
1740-.commit-cursor-next { flex: 1; text-align: right; }
1741-
1742-.pagination {
1743- display: grid;
1744- grid-template-columns: 1fr auto 1fr;
1745- align-items: center;
1746- border-top: 1px solid var(--color-border);
1747- margin-top: var(--space-2);
1748- padding: var(--space-4) 0 var(--space-2);
1749-}
1750-
1751-.pagination-prev { justify-self: start; }
1752-.pagination-next { justify-self: end; }
1753-
1754-.pagination-info {
1755- justify-self: center;
1756- font-size: var(--text-sm);
1757- color: var(--color-text-muted);
1758-}
1759-
1760-.pagination-btn {
1761- display: inline-flex;
1762- align-items: center;
1763- gap: var(--space-1);
1764- padding: var(--space-1) var(--space-3);
1765- border: 1px solid var(--color-border);
1766- border-radius: var(--radius-md);
1767- font-size: var(--text-sm);
1768- font-weight: 500;
1769- text-decoration: none;
1770- color: var(--color-text);
1771- background: var(--color-bg-subtle);
1772- transition: background 0.1s, border-color 0.1s;
1773- white-space: nowrap;
1774-}
1775-
1776-.pagination-btn:hover {
1777- background: var(--color-bg-inset);
1778- border-color: var(--color-border);
1779- color: var(--color-text);
1780- text-decoration: none;
1781-}
1782-
1783- /* Settings */
1784- .theme-options { display: flex; gap: var(--space-4); margin-bottom: var(--space-4); flex-wrap: wrap; }
1785- .theme-option { display: flex; align-items: center; gap: var(--space-2); cursor: pointer; font-size: var(--text-sm); }
1786- .settings-form { display: flex; flex-direction: column; gap: var(--space-4); }
1787- .passkey-list { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-4); }
1788- .passkey-item { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; border-bottom: 1px solid var(--color-border-muted); font-size: var(--text-sm); }
1789- .passkey-item:last-child { border-bottom: none; }
1790- .passkey-date { flex: 1; color: var(--color-text-muted); }
1791- .ssh-key-info { flex: 1; display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
1792- .ssh-key-name { font-weight: 500; }
1793- .ssh-key-fingerprint { font-family: var(--font-mono); font-size: var(--text-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1794- .form-textarea { resize: vertical; min-height: 4.5rem; font-family: var(--font-mono); font-size: var(--text-sm); }
1795- /* Clone popup */
1796- .clone-popup-details { position: relative; display: inline-block; }
1797- .clone-popup-details > summary { list-style: none; }
1798- .clone-popup-details > summary::-webkit-details-marker { display: none; }
1799- .clone-popup { position: absolute; right: 1rem; top: calc(100% + var(--space-2)); background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-3); min-width: 22rem; z-index: 100; box-shadow: 0 4px 20px rgba(0,0,0,0.15); display: flex; flex-direction: column; gap: var(--space-2); }
1800- .clone-url-row { display: flex; align-items: center; gap: var(--space-2); }
1801- .clone-url-label { font-size: var(--text-xs); font-weight: 600; color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.05em; min-width: 2.75rem; }
1802- .clone-url-input { flex: 1; font-family: var(--font-mono); font-size: var(--text-sm); background: var(--color-bg-subtle); border: 1px solid var(--color-border-muted); border-radius: var(--radius-md); padding: var(--space-1) var(--space-2); color: var(--color-text); cursor: text; min-width: 0; }
1803- .user-list { display: flex; flex-direction: column; gap: var(--space-2); margin-bottom: var(--space-6); }
1804- .user-item { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) 0; border-bottom: 1px solid var(--color-border-muted); font-size: var(--text-sm); }
1805- .user-item:last-child { border-bottom: none; }
1806- .user-name { font-weight: 500; flex: 1; }
1807- .user-date { color: var(--color-text-muted); font-size: var(--text-xs); }
Ascripts/build-css.ts
@@ -0,0 +1,30 @@
1+import { watch } from "fs";
2+import { transform } from "lightningcss";
3+
4+const INPUT = "src/styles/main.css";
5+const OUTPUT = "public/assets/main.css";
6+
7+async function build() {
8+ const source = Bun.file(INPUT);
9+ const { code } = transform({
10+ filename: INPUT,
11+ code: await source.bytes(),
12+ minify: true,
13+ sourceMap: false,
14+ });
15+ await Bun.write(OUTPUT, code);
16+ console.log(`[css] built ${code.length} bytes`);
17+}
18+
19+await build();
20+
21+if (process.argv.includes("--watch")) {
22+ watch(INPUT, async () => {
23+ try {
24+ await build();
25+ } catch (e) {
26+ console.error("[css]", e);
27+ }
28+ });
29+ console.log(`[css] watching ${INPUT}`);
30+}
Ascripts/dev.ts
@@ -0,0 +1,10 @@
1+const server = Bun.spawn(["bun", "--watch", "src/index.tsx"], { stdio: ["inherit", "inherit", "inherit"] });
2+const css = Bun.spawn(["bun", "scripts/build-css.ts", "--watch"], { stdio: ["inherit", "inherit", "inherit"] });
3+
4+process.on("SIGINT", () => {
5+ server.kill();
6+ css.kill();
7+ process.exit(0);
8+});
9+
10+await Promise.all([server.exited, css.exited]);
Msrc/config.ts
@@ -15,6 +15,11 @@ export const PORT = parseInt(process.env.PORT ?? "", 10) || 3000;
1515 export const SSH_PORT = parseInt(process.env.SSH_PORT ?? "", 10) || 2222;
1616 export const REGISTRATION_DISABLED = !!process.env.REGISTRATION_DISABLED;
1717 export const BASE_URL = process.env.BASE_URL ?? `http://localhost:${PORT}`;
18+export const GIT_AUTHOR_NAME =
19+ process.env.GIT_AUTHOR_NAME ?? OWNER_DISPLAY_NAME;
20+export const GIT_AUTHOR_EMAIL =
21+ process.env.GIT_AUTHOR_EMAIL ??
22+ `${OWNER_DISPLAY_NAME}@${new URL(BASE_URL).hostname}`;
1823 export const DATA_DIR = path.resolve(process.env.DATA_DIR ?? "./data");
1924 export const HIGHLIGHT_WORKERS =
2025 parseInt(process.env.HIGHLIGHT_WORKERS ?? "", 10) || 4;
Msrc/routes/avatars.ts
@@ -29,7 +29,19 @@ export const avatarRoutes = new Elysia()
2929 if (!new URL(request.url).searchParams.has("v"))
3030 return new Response("Not found", { status: 404 });
3131
32- return new Response(Bun.file(avatarJxlPath(userId)), {
32+ let file = Bun.file(avatarJxlPath(userId));
33+ if (!(await file.exists())) {
34+ const user = await db
35+ .selectFrom("users")
36+ .select("username")
37+ .where("id", "=", userId)
38+ .executeTakeFirst();
39+ if (!user) return new Response("Not found", { status: 404 });
40+ await createDefaultAvatar(userId, user.username);
41+ file = Bun.file(avatarJxlPath(userId));
42+ }
43+
44+ return new Response(file, {
3345 headers: {
3446 "Content-Type": "image/jxl",
3547 "Cache-Control": CACHE,
Msrc/routes/issues.tsx
@@ -28,7 +28,9 @@ export const issueRoutes = new Elysia()
2828 const status =
2929 query.status === "closed"
3030 ? ("closed" as const)
31- : ("open" as const);
31+ : query.status === "completed"
32+ ? ("completed" as const)
33+ : ("open" as const);
3234 const page = Math.max(1, query.page ?? 1);
3335
3436 const allCounts = await db
@@ -62,6 +64,7 @@ export const issueRoutes = new Elysia()
6264 "issues.updated_at",
6365 "issues.edited_at",
6466 "users.username as author_username",
67+ "users.avatar_version as author_avatar_version",
6568 ])
6669 .where("issues.repo_id", "=", repo.id)
6770 .where("issues.status", "=", status)
@@ -82,6 +85,7 @@ export const issueRoutes = new Elysia()
8285 issues={
8386 issues as ((typeof issues)[0] & {
8487 author_username: string;
88+ author_avatar_version: number | null;
8589 })[]
8690 }
8791 status={status}
@@ -394,6 +398,34 @@ export const issueRoutes = new Elysia()
394398 },
395399 )
396400
401+ .post("/:repo/issues/:number/complete", async ({ params, cookie }) => {
402+ const user = await resolveSession(cookie.session.value);
403+ const deny = requireAdmin(user);
404+ if (deny) return deny;
405+ const repo = await getRepo(params.repo, true);
406+ if (!repo) return new Response("Not found", { status: 404 });
407+
408+ const issueNum = parseInt(params.number, 10);
409+ const issue = await db
410+ .selectFrom("issues")
411+ .select("id")
412+ .where("repo_id", "=", repo.id)
413+ .where("number", "=", issueNum)
414+ .executeTakeFirst();
415+ if (!issue) return new Response("Not found", { status: 404 });
416+
417+ await db
418+ .updateTable("issues")
419+ .set({ status: "completed", updated_at: new Date().toISOString() })
420+ .where("id", "=", issue.id)
421+ .execute();
422+
423+ return new Response(null, {
424+ status: 302,
425+ headers: { Location: `/${repo.name}/issues/${issueNum}` },
426+ });
427+ })
428+
397429 .post("/:repo/issues/:number/close", async ({ params, cookie }) => {
398430 const user = await resolveSession(cookie.session.value);
399431 const deny = requireAdmin(user);
Msrc/routes/patches.tsx
@@ -94,6 +94,7 @@ export const patchRoutes = new Elysia()
9494 "patches.updated_at",
9595 "patches.edited_at",
9696 "users.username as author_username",
97+ "users.avatar_version as author_avatar_version",
9798 ])
9899 .where("patches.repo_id", "=", repo.id)
99100 .where("patches.status", "=", status)
@@ -114,6 +115,7 @@ export const patchRoutes = new Elysia()
114115 patches={
115116 patches as ((typeof patches)[0] & {
116117 author_username: string;
118+ author_avatar_version: number | null;
117119 })[]
118120 }
119121 status={status}
Msrc/routes/repos.tsx
@@ -38,8 +38,8 @@ async function mimeForContent(content: Buffer): Promise<string> {
3838 const result = await fileTypeFromBuffer(content);
3939 if (result) return result.mime;
4040 return hasBinaryContent(content.subarray(0, 8000))
41- ? "text/plain; charset=utf-8"
42- : "application/octet-stream";
41+ ? "application/octet-stream"
42+ : "text/plain; charset=utf-8";
4343 }
4444
4545 async function readReadme(
Msrc/services/git.ts
@@ -1,6 +1,7 @@
11 import path from "node:path";
22 import { $ as _$ } from "bun";
33
4+import { GIT_AUTHOR_EMAIL, GIT_AUTHOR_NAME } from "../config.ts";
45 import { REPOS_DIR } from "../constants.ts";
56
67 const $ = _$.env({ ...process.env, LC_ALL: "C", LANG: "C" });
@@ -367,7 +368,17 @@ export const git = {
367368 : title;
368369 const msg = extractPatchSubject(patchContent) || fallback;
369370 const commit = (
370- await $`git -C ${p} commit-tree ${tree} -p ${parent} -m ${msg}`.text()
371+ await $`git -C ${p} commit-tree ${tree} -p ${parent} -m ${msg}`
372+ .env({
373+ ...process.env,
374+ LC_ALL: "C",
375+ LANG: "C",
376+ GIT_AUTHOR_NAME,
377+ GIT_AUTHOR_EMAIL,
378+ GIT_COMMITTER_NAME: GIT_AUTHOR_NAME,
379+ GIT_COMMITTER_EMAIL: GIT_AUTHOR_EMAIL,
380+ })
381+ .text()
371382 ).trim();
372383 const ref = (
373384 await $`git -C ${p} symbolic-ref HEAD`.text()
Msrc/services/highlight.ts
@@ -96,6 +96,58 @@ export type FileView =
9696
9797 const fileCache = new Map<string, FileView>();
9898
99+function shikiHtmlToTable(html: string): string {
100+ const styleMatch = html.match(/<pre[^>]*style="([^"]*)"/);
101+ const rawStyle = styleMatch?.[1] ?? "";
102+ // Convert Shiki's direct background-color/color to CSS custom properties so
103+ // the CSS (not the inline style) controls the background in dark mode.
104+ const lightBg =
105+ rawStyle.match(/(?:^|;)background-color:([^;]+)/)?.[1] ?? "#fff";
106+ const lightColor = rawStyle.match(/(?:^|;)color:([^;]+)/)?.[1] ?? "";
107+ const darkBg = rawStyle.match(/--shiki-dark-bg:([^;]+)/)?.[1] ?? "";
108+ const darkColor = rawStyle.match(/--shiki-dark:([^;]+)/)?.[1] ?? "";
109+ const varParts = [`--shiki-light-bg:${lightBg}`];
110+ if (lightColor) varParts.push(`--shiki-light:${lightColor}`);
111+ if (darkBg) varParts.push(`--shiki-dark-bg:${darkBg}`);
112+ if (darkColor) varParts.push(`--shiki-dark:${darkColor}`);
113+ const tableStyle = varParts.join(";");
114+
115+ const codeMatch = html.match(/<code>([\s\S]*?)<\/code>/);
116+ const codeContent = codeMatch?.[1] ?? "";
117+ const rawLines = codeContent.split("\n");
118+ // Shiki adds a trailing newline, so drop the last empty entry
119+ const lines =
120+ rawLines.length > 0 && rawLines[rawLines.length - 1] === ""
121+ ? rawLines.slice(0, -1)
122+ : rawLines;
123+ const rows = lines
124+ .map((line, i) => {
125+ const n = i + 1;
126+ return `<tr id="L${n}"><td class="blob-ln"><a href="#L${n}">${n}</a></td><td class="blob-code">${line}</td></tr>`;
127+ })
128+ .join("");
129+ return `<table class="blob-table" style="${tableStyle}"><tbody>${rows}</tbody></table>`;
130+}
131+
132+function plainTextToTable(text: string): string {
133+ const rawLines = text.split("\n");
134+ const lines =
135+ rawLines.length > 0 && rawLines[rawLines.length - 1] === ""
136+ ? rawLines.slice(0, -1)
137+ : rawLines;
138+ const rows = lines
139+ .map((line, i) => {
140+ const n = i + 1;
141+ const escaped = line
142+ .replace(/&/g, "&amp;")
143+ .replace(/</g, "&lt;")
144+ .replace(/>/g, "&gt;");
145+ return `<tr id="L${n}"><td class="blob-ln"><a href="#L${n}">${n}</a></td><td class="blob-code">${escaped}</td></tr>`;
146+ })
147+ .join("");
148+ return `<table class="blob-table"><tbody>${rows}</tbody></table>`;
149+}
150+
99151 export async function serveFile(
100152 content: Buffer,
101153 filename: string,
@@ -112,26 +164,25 @@ export async function serveFile(
112164 return { type: "download", size: content.length };
113165 }
114166 const text = content.toString("utf-8");
115- const lines = text.split("\n").length;
167+ const lineCount = text.split("\n").length;
116168 let view: FileView;
117169 try {
118170 const h = getHighlighter();
119171 const lang = detectLang(filename);
120- const html = h.codeToHtml(text, {
172+ const shikiHtml = h.codeToHtml(text, {
121173 lang,
122174 themes: { light: "github-light", dark: "github-dark" },
123175 });
124- view = { type: "inline", html, lines };
176+ view = {
177+ type: "inline",
178+ html: shikiHtmlToTable(shikiHtml),
179+ lines: lineCount,
180+ };
125181 } catch {
126- // fallback: plain pre
127- const escaped = text
128- .replace(/&/g, "&amp;")
129- .replace(/</g, "&lt;")
130- .replace(/>/g, "&gt;");
131182 view = {
132183 type: "inline",
133- html: `<pre class="code-plain"><code>${escaped}</code></pre>`,
134- lines,
184+ html: plainTextToTable(text),
185+ lines: lineCount,
135186 };
136187 }
137188 if (cacheKey) {
Asrc/styles/main.css
@@ -0,0 +1,2486 @@
1+/* View Transitions */
2+@view-transition {
3+ navigation: auto;
4+}
5+
6+.site-nav {
7+ view-transition-name: site-nav;
8+}
9+.repo-header {
10+ view-transition-name: repo-header;
11+}
12+.breadcrumb {
13+ view-transition-name: breadcrumb;
14+}
15+
16+/* ============================================================
17+ @layer reset
18+ ============================================================ */
19+@layer reset {
20+ *,
21+ *::before,
22+ *::after {
23+ box-sizing: border-box;
24+ margin: 0;
25+ padding: 0;
26+ }
27+ html {
28+ -webkit-text-size-adjust: 100%;
29+ }
30+ body {
31+ min-height: 100dvh;
32+ min-width: 500px;
33+ }
34+ img,
35+ video {
36+ max-width: 100%;
37+ height: auto;
38+ }
39+ button,
40+ input,
41+ textarea,
42+ select {
43+ font: inherit;
44+ }
45+ a {
46+ color: inherit;
47+ }
48+ ul,
49+ ol {
50+ list-style: none;
51+ }
52+ h1,
53+ h2,
54+ h3,
55+ h4,
56+ h5,
57+ h6 {
58+ font-weight: 600;
59+ line-height: 1.2;
60+ }
61+ pre,
62+ code {
63+ font-family: var(--font-mono);
64+ }
65+}
66+
67+/* ============================================================
68+ @layer tokens
69+ ============================================================ */
70+@layer tokens {
71+ :root {
72+ /* Colors — light */
73+ --color-bg: #ffffff;
74+ --color-bg-subtle: #f6f8fa;
75+ --color-bg-inset: #eef0f2;
76+ --color-border: #d0d7de;
77+ --color-border-muted: #e4e7eb;
78+ --color-text: #1f2328;
79+ --color-text-muted: #656d76;
80+ --color-link: #0969da;
81+ --color-link-hover: #0550ae;
82+
83+ --color-accent: #fd7e14;
84+ --color-accent-bg: #fff3cd;
85+
86+ --color-success: #1a7f37;
87+ --color-success-bg: #dafbe1;
88+ --color-danger: #cf222e;
89+ --color-danger-bg: #ffebe9;
90+ --color-warning: #9a6700;
91+ --color-warning-bg: #fff8c5;
92+ --color-merged: #8250df;
93+ --color-merged-bg: #fbefff;
94+
95+ /* Typography */
96+ --font-sans:
97+ system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
98+ --font-mono:
99+ ui-monospace, "Cascadia Code", "Source Code Pro", monospace;
100+
101+ --text-xs: 0.75rem;
102+ --text-sm: 0.875rem;
103+ --text-base: 1rem;
104+ --text-lg: 1.125rem;
105+ --text-xl: 1.25rem;
106+ --text-2xl: 1.5rem;
107+ --text-3xl: 1.875rem;
108+
109+ /* Spacing */
110+ --space-1: 0.25rem;
111+ --space-2: 0.5rem;
112+ --space-3: 0.75rem;
113+ --space-4: 1rem;
114+ --space-5: 1.25rem;
115+ --space-6: 1.5rem;
116+ --space-8: 2rem;
117+ --space-10: 2.5rem;
118+ --space-12: 3rem;
119+ --space-16: 4rem;
120+
121+ /* Border radius */
122+ --radius-sm: 4px;
123+ --radius-md: 6px;
124+ --radius-lg: 8px;
125+ --radius-full: 9999px;
126+
127+ /* Shadows */
128+ --shadow-sm: 0 1px 0 rgba(27, 31, 36, 0.04);
129+ --shadow-md: 0 3px 6px rgba(140, 149, 159, 0.15);
130+ --shadow-lg: 0 8px 24px rgba(140, 149, 159, 0.2);
131+ }
132+
133+ @media (prefers-color-scheme: dark) {
134+ :root {
135+ --color-bg: #0d1117;
136+ --color-bg-subtle: #161b22;
137+ --color-bg-inset: #1c2128;
138+ --color-border: #30363d;
139+ --color-border-muted: #21262d;
140+ --color-text: #e6edf3;
141+ --color-text-muted: #7d8590;
142+ --color-link: #58a6ff;
143+ --color-link-hover: #79c0ff;
144+
145+ --color-accent: #f0883e;
146+ --color-accent-bg: #3d2f00;
147+
148+ --color-success: #3fb950;
149+ --color-success-bg: #0d2b19;
150+ --color-danger: #f85149;
151+ --color-danger-bg: #2d1419;
152+ --color-warning: #d29922;
153+ --color-warning-bg: #2c2000;
154+ --color-merged: #bc8cff;
155+ --color-merged-bg: #1e0b3e;
156+ }
157+ }
158+
159+ /* Explicit theme overrides — applied via data-theme attribute set by cookie */
160+ :root[data-theme="dark"] {
161+ --color-bg: #0d1117;
162+ --color-bg-subtle: #161b22;
163+ --color-bg-inset: #1c2128;
164+ --color-border: #30363d;
165+ --color-border-muted: #21262d;
166+ --color-text: #e6edf3;
167+ --color-text-muted: #7d8590;
168+ --color-link: #58a6ff;
169+ --color-link-hover: #79c0ff;
170+ --color-accent: #f0883e;
171+ --color-accent-bg: #3d2f00;
172+ --color-success: #3fb950;
173+ --color-success-bg: #0d2b19;
174+ --color-danger: #f85149;
175+ --color-danger-bg: #2d1419;
176+ --color-warning: #d29922;
177+ --color-warning-bg: #2c2000;
178+ --color-merged: #bc8cff;
179+ --color-merged-bg: #1e0b3e;
180+ }
181+ :root[data-theme="light"] {
182+ --color-bg: #ffffff;
183+ --color-bg-subtle: #f6f8fa;
184+ --color-bg-inset: #eef0f2;
185+ --color-border: #d0d7de;
186+ --color-border-muted: #e4e7eb;
187+ --color-text: #1f2328;
188+ --color-text-muted: #656d76;
189+ --color-link: #0969da;
190+ --color-link-hover: #0550ae;
191+ --color-accent: #fd7e14;
192+ --color-accent-bg: #fff3cd;
193+ --color-success: #1a7f37;
194+ --color-success-bg: #dafbe1;
195+ --color-danger: #cf222e;
196+ --color-danger-bg: #ffebe9;
197+ --color-warning: #9a6700;
198+ --color-warning-bg: #fff8c5;
199+ --color-merged: #8250df;
200+ --color-merged-bg: #fbefff;
201+ }
202+}
203+
204+/* ============================================================
205+ @layer layout
206+ ============================================================ */
207+@layer layout {
208+ body {
209+ font-family: var(--font-sans);
210+ font-size: var(--text-base);
211+ color: var(--color-text);
212+ background: var(--color-bg);
213+ display: grid;
214+ grid-template-rows: auto 1fr auto;
215+ min-height: 100dvh;
216+ }
217+
218+ .site-header {
219+ background: var(--color-bg-subtle);
220+ border-bottom: 1px solid var(--color-border);
221+ position: sticky;
222+ top: 0;
223+ z-index: 100;
224+ }
225+
226+ .site-nav {
227+ max-width: 1200px;
228+ margin: 0 auto;
229+ padding: var(--space-3) var(--space-4);
230+ display: flex;
231+ align-items: center;
232+ gap: var(--space-4);
233+ }
234+
235+ .site-logo {
236+ display: flex;
237+ align-items: center;
238+ gap: var(--space-2);
239+ text-decoration: none;
240+ font-weight: 700;
241+ font-size: var(--text-lg);
242+ color: var(--color-text);
243+ }
244+
245+ .logo-icon {
246+ width: 1.6em;
247+ height: 1.6em;
248+ display: block;
249+ flex-shrink: 0;
250+ }
251+
252+ .nav-links {
253+ margin-left: auto;
254+ display: flex;
255+ align-items: center;
256+ gap: var(--space-3);
257+ }
258+
259+ .nav-user {
260+ font-size: var(--text-sm);
261+ color: var(--color-text-muted);
262+ }
263+
264+ .site-main {
265+ padding: var(--space-8) var(--space-4);
266+ min-width: 0;
267+ }
268+
269+ .site-footer {
270+ text-align: center;
271+ padding: var(--space-6) var(--space-4);
272+ font-size: var(--text-sm);
273+ color: var(--color-text-muted);
274+ border-top: 1px solid var(--color-border);
275+ }
276+
277+ .container {
278+ max-width: 1000px;
279+ margin: 0 auto;
280+ overflow-x: clip;
281+ }
282+
283+ .container-narrow {
284+ max-width: 680px;
285+ }
286+
287+ .inline-form {
288+ display: inline;
289+ }
290+
291+ /* --- Two-step delete confirmation --- */
292+ .confirm-details {
293+ position: relative;
294+ display: inline-block;
295+ }
296+ .confirm-details > summary {
297+ list-style: none;
298+ }
299+ .confirm-details > summary::-webkit-details-marker {
300+ display: none;
301+ }
302+ .confirm-popup {
303+ position: absolute;
304+ right: 0;
305+ top: calc(100% + var(--space-1));
306+ background: var(--color-bg);
307+ border: 1px solid var(--color-border);
308+ border-radius: var(--radius-md);
309+ padding: var(--space-3) var(--space-4);
310+ display: flex;
311+ align-items: center;
312+ gap: var(--space-3);
313+ white-space: nowrap;
314+ z-index: 100;
315+ font-size: var(--text-sm);
316+ color: var(--color-text-muted);
317+ }
318+}
319+
320+/* ============================================================
321+ @layer components
322+ ============================================================ */
323+@layer components {
324+ /* --- Buttons --- */
325+ .btn {
326+ display: inline-flex;
327+ align-items: center;
328+ gap: var(--space-2);
329+ padding: var(--space-2) var(--space-4);
330+ border: 1px solid var(--color-border);
331+ border-radius: var(--radius-md);
332+ font-size: var(--text-sm);
333+ font-weight: 500;
334+ text-decoration: none;
335+ cursor: pointer;
336+ background: var(--color-bg-subtle);
337+ color: var(--color-text);
338+ transition:
339+ background 0.1s,
340+ border-color 0.1s;
341+ white-space: nowrap;
342+ }
343+ .btn:hover {
344+ background: var(--color-bg-inset);
345+ border-color: var(--color-border);
346+ }
347+ .btn:disabled {
348+ opacity: 0.5;
349+ cursor: not-allowed;
350+ }
351+ .btn-primary {
352+ background: var(--color-accent);
353+ border-color: transparent;
354+ color: #fff;
355+ }
356+ .btn-primary:hover {
357+ background: color-mix(in srgb, var(--color-accent) 82%, #000);
358+ }
359+ .btn-secondary {
360+ background: var(--color-bg-subtle);
361+ border-color: var(--color-border);
362+ }
363+ .btn-ghost {
364+ background: transparent;
365+ border-color: transparent;
366+ }
367+ .btn-ghost:hover {
368+ background: var(--color-bg-inset);
369+ }
370+ /* Keep ghost transparent even when combined with size variants */
371+ .btn-ghost.btn-xs,
372+ .btn-ghost.btn-sm {
373+ background: transparent;
374+ border-color: transparent;
375+ }
376+ .btn-ghost.btn-xs:hover,
377+ .btn-ghost.btn-sm:hover {
378+ background: var(--color-bg-inset);
379+ }
380+ .btn-sm {
381+ padding: var(--space-1) var(--space-3);
382+ font-size: var(--text-xs);
383+ }
384+ .btn-block {
385+ width: 100%;
386+ justify-content: center;
387+ }
388+
389+ /* --- Forms --- */
390+ .form-group {
391+ display: flex;
392+ flex-direction: column;
393+ gap: var(--space-2);
394+ margin-bottom: var(--space-4);
395+ }
396+ .form-group label {
397+ font-size: var(--text-sm);
398+ font-weight: 500;
399+ color: var(--color-text);
400+ }
401+ .form-group input:not([type="file"]):not([type="checkbox"]),
402+ .form-group textarea,
403+ .form-group select {
404+ padding: var(--space-2) var(--space-3);
405+ border: 1px solid var(--color-border);
406+ border-radius: var(--radius-md);
407+ background: var(--color-bg);
408+ color: var(--color-text);
409+ font-size: var(--text-sm);
410+ }
411+ .form-group input:not([type="file"]):not([type="checkbox"]):focus,
412+ .form-group textarea:focus {
413+ outline: 2px solid var(--color-accent);
414+ outline-offset: -1px;
415+ border-color: var(--color-accent);
416+ }
417+ .form-group input[type="file"] {
418+ font-size: var(--text-sm);
419+ color: var(--color-text);
420+ padding: 0;
421+ border: none;
422+ background: none;
423+ }
424+ .form-group textarea {
425+ resize: vertical;
426+ min-height: 120px;
427+ }
428+ .form-success {
429+ background: var(--color-success-bg);
430+ color: var(--color-success);
431+ border: 1px solid var(--color-success);
432+ border-radius: var(--radius-md);
433+ padding: var(--space-3) var(--space-4);
434+ font-size: var(--text-sm);
435+ margin-bottom: var(--space-4);
436+ }
437+ .form-hint {
438+ font-size: var(--text-sm);
439+ color: var(--color-text-muted);
440+ margin: 0;
441+ }
442+ .form-error {
443+ background: var(--color-danger-bg);
444+ color: var(--color-danger);
445+ border: 1px solid var(--color-danger);
446+ border-radius: var(--radius-md);
447+ padding: var(--space-3) var(--space-4);
448+ font-size: var(--text-sm);
449+ margin-bottom: var(--space-4);
450+ }
451+ .form-actions {
452+ display: flex;
453+ gap: var(--space-3);
454+ align-items: center;
455+ flex-wrap: wrap;
456+ }
457+ .form-card {
458+ background: var(--color-bg-subtle);
459+ border: 1px solid var(--color-border);
460+ border-radius: var(--radius-lg);
461+ padding: var(--space-6);
462+ margin-top: var(--space-4);
463+ }
464+ .checkbox-label {
465+ display: flex;
466+ align-items: center;
467+ gap: var(--space-2);
468+ cursor: pointer;
469+ }
470+
471+ /* --- Page header --- */
472+ .page-header {
473+ display: flex;
474+ align-items: center;
475+ justify-content: space-between;
476+ margin-bottom: var(--space-6);
477+ gap: var(--space-4);
478+ }
479+ .page-title {
480+ font-size: var(--text-2xl);
481+ color: var(--color-text);
482+ margin-bottom: var(--space-4);
483+ }
484+ .page-title a {
485+ text-decoration: none;
486+ color: var(--color-link);
487+ }
488+ .page-title a:hover {
489+ text-decoration: underline;
490+ }
491+ .section-title {
492+ font-size: var(--text-lg);
493+ margin-bottom: var(--space-4);
494+ padding-bottom: var(--space-2);
495+ border-bottom: 1px solid var(--color-border);
496+ }
497+
498+ /* --- Auth --- */
499+ .auth-container {
500+ max-width: 400px;
501+ margin: var(--space-12) auto;
502+ }
503+ .auth-container .page-title {
504+ margin-bottom: var(--space-6);
505+ }
506+ .auth-form {
507+ margin-bottom: var(--space-4);
508+ }
509+ .auth-footer {
510+ text-align: center;
511+ font-size: var(--text-sm);
512+ color: var(--color-text-muted);
513+ margin-top: var(--space-4);
514+ }
515+ .passkey-btn {
516+ display: none;
517+ margin-top: var(--space-3);
518+ }
519+
520+ /* --- Repo header --- */
521+ .repo-header {
522+ margin-bottom: var(--space-2);
523+ }
524+ .repo-title-row {
525+ display: flex;
526+ align-items: center;
527+ gap: var(--space-3);
528+ flex-wrap: wrap;
529+ }
530+ .repo-title-row .page-title {
531+ margin-bottom: 0;
532+ }
533+ .repo-header-title {
534+ display: flex;
535+ align-items: center;
536+ gap: var(--space-3);
537+ flex-wrap: wrap;
538+ }
539+ .repo-description {
540+ color: var(--color-text-muted);
541+ margin-top: var(--space-2);
542+ }
543+ .repo-header-meta {
544+ display: flex;
545+ align-items: center;
546+ gap: var(--space-4);
547+ margin-bottom: var(--space-3);
548+ font-size: var(--text-sm);
549+ color: var(--color-text-muted);
550+ }
551+ .icon {
552+ font-style: normal;
553+ }
554+
555+ /* --- Branch selector --- */
556+ .branch-selector {
557+ display: inline-flex;
558+ align-items: center;
559+ gap: var(--space-2);
560+ }
561+ .branch-selector-icon {
562+ color: var(--color-text-muted);
563+ font-size: var(--text-sm);
564+ user-select: none;
565+ }
566+ .branch-select {
567+ padding: var(--space-1) var(--space-2);
568+ padding-right: var(--space-5);
569+ border: 1px solid var(--color-border);
570+ border-radius: var(--radius-md);
571+ background: var(--color-bg-subtle);
572+ font-size: var(--text-sm);
573+ font-family: var(--font-mono);
574+ color: var(--color-text);
575+ cursor: pointer;
576+ appearance: auto;
577+ max-width: 180px;
578+ }
579+ .branch-select:hover {
580+ border-color: var(--color-text-muted);
581+ }
582+ .branch-select:focus {
583+ outline: 2px solid var(--color-accent);
584+ outline-offset: -1px;
585+ }
586+
587+ /* toolbar row above file tree */
588+ .tree-toolbar {
589+ display: flex;
590+ align-items: center;
591+ justify-content: space-between;
592+ margin-bottom: var(--space-3);
593+ }
594+
595+ /* commits-header flex row */
596+ .commits-header {
597+ display: flex;
598+ align-items: center;
599+ gap: var(--space-4);
600+ margin-bottom: var(--space-4);
601+ flex-wrap: wrap;
602+ }
603+ .commits-header .section-title {
604+ margin-bottom: 0;
605+ }
606+
607+ /* --- Repo nav bar (tabs + private badge) --- */
608+ .repo-nav-bar {
609+ display: flex;
610+ align-items: center;
611+ justify-content: space-between;
612+ border-bottom: 1px solid var(--color-border);
613+ margin-bottom: var(--space-6);
614+ gap: var(--space-4);
615+ }
616+
617+ /* --- Repo tabs --- */
618+ .repo-tabs {
619+ display: flex;
620+ gap: 0;
621+ flex: 1;
622+ overflow-x: auto;
623+ overflow-y: hidden;
624+ }
625+ .repo-tab {
626+ padding: var(--space-3) var(--space-4);
627+ font-size: var(--text-sm);
628+ text-decoration: none;
629+ color: var(--color-text-muted);
630+ border-bottom: 2px solid transparent;
631+ margin-bottom: -1px;
632+ transition: color 0.1s;
633+ }
634+ .repo-tab:hover {
635+ color: var(--color-text);
636+ }
637+ .repo-tab.active {
638+ color: var(--color-text);
639+ font-weight: 600;
640+ border-bottom-color: var(--color-accent);
641+ }
642+ /* Align active indicator with nav-bar border */
643+ .repo-tab {
644+ margin-bottom: -1px;
645+ }
646+
647+ /* --- Repo list --- */
648+ .repo-list {
649+ display: flex;
650+ flex-direction: column;
651+ gap: var(--space-2);
652+ }
653+ .repo-card {
654+ display: flex;
655+ align-items: flex-start;
656+ justify-content: space-between;
657+ padding: var(--space-4) var(--space-5);
658+ border: 1px solid var(--color-border);
659+ border-radius: var(--radius-lg);
660+ background: var(--color-bg);
661+ gap: var(--space-4);
662+ transition: border-color 0.15s;
663+ }
664+ .repo-card:hover {
665+ border-color: var(--color-accent);
666+ }
667+ .repo-card-main {
668+ flex: 1;
669+ min-width: 0;
670+ }
671+ .repo-card-title {
672+ display: flex;
673+ align-items: center;
674+ gap: var(--space-2);
675+ flex-wrap: wrap;
676+ margin-bottom: var(--space-1);
677+ }
678+ .repo-name {
679+ font-size: var(--text-lg);
680+ font-weight: 600;
681+ color: var(--color-link);
682+ text-decoration: none;
683+ }
684+ .repo-name:hover {
685+ text-decoration: underline;
686+ }
687+ .repo-card-meta {
688+ display: flex;
689+ flex-direction: column;
690+ align-items: flex-end;
691+ gap: var(--space-1);
692+ font-size: var(--text-xs);
693+ color: var(--color-text-muted);
694+ white-space: nowrap;
695+ }
696+ .repo-date {
697+ color: var(--color-text-muted);
698+ }
699+
700+ /* --- Badges --- */
701+ .badge {
702+ display: inline-flex;
703+ align-items: center;
704+ padding: 1px var(--space-2);
705+ border-radius: var(--radius-full);
706+ font-size: var(--text-xs);
707+ font-weight: 500;
708+ border: 1px solid;
709+ }
710+ .badge-private {
711+ color: var(--color-warning);
712+ border-color: var(--color-warning);
713+ background: var(--color-warning-bg);
714+ }
715+ .issue-badge,
716+ .patch-badge {
717+ display: inline-flex;
718+ align-items: center;
719+ padding: var(--space-1) var(--space-3);
720+ border-radius: var(--radius-full);
721+ font-size: var(--text-xs);
722+ font-weight: 500;
723+ text-transform: capitalize;
724+ }
725+ .issue-badge.open,
726+ .patch-badge.open {
727+ background: var(--color-success-bg);
728+ color: var(--color-success);
729+ }
730+ .issue-badge.closed,
731+ .patch-badge.closed {
732+ background: var(--color-danger-bg);
733+ color: var(--color-danger);
734+ }
735+ .issue-badge.completed {
736+ background: var(--color-merged-bg);
737+ color: var(--color-merged);
738+ }
739+ .patch-badge.merged {
740+ background: var(--color-merged-bg);
741+ color: var(--color-merged);
742+ }
743+
744+ /* --- Empty state --- */
745+ .empty-state {
746+ text-align: center;
747+ padding: var(--space-16) var(--space-4);
748+ color: var(--color-text-muted);
749+ }
750+ .empty-state p {
751+ margin-bottom: var(--space-4);
752+ font-size: var(--text-lg);
753+ }
754+
755+ /* --- Breadcrumb --- */
756+ .breadcrumb {
757+ display: flex;
758+ align-items: center;
759+ flex-wrap: wrap;
760+ gap: var(--space-1);
761+ font-size: var(--text-sm);
762+ margin-bottom: var(--space-4);
763+ }
764+ .breadcrumb a {
765+ color: var(--color-link);
766+ text-decoration: none;
767+ }
768+ .breadcrumb a:hover {
769+ text-decoration: underline;
770+ }
771+ .breadcrumb-sep {
772+ color: var(--color-text-muted);
773+ }
774+ .breadcrumb-current {
775+ color: var(--color-text-muted);
776+ }
777+
778+ /* --- File tree --- */
779+ .file-tree {
780+ width: 100%;
781+ border: 1px solid var(--color-border);
782+ border-radius: var(--radius-lg);
783+ border-collapse: collapse;
784+ overflow: hidden;
785+ margin-bottom: var(--space-6);
786+ }
787+ .file-tree-row {
788+ border-bottom: 1px solid var(--color-border-muted);
789+ }
790+ .file-tree-row:last-child {
791+ border-bottom: none;
792+ }
793+ .file-tree-row:hover {
794+ background: var(--color-bg-subtle);
795+ }
796+ .file-icon,
797+ .file-name,
798+ .file-size {
799+ padding: var(--space-2) var(--space-3);
800+ font-size: var(--text-sm);
801+ vertical-align: middle;
802+ }
803+ .file-icon {
804+ width: 36px;
805+ padding-right: var(--space-1);
806+ text-align: center;
807+ line-height: 0;
808+ }
809+ .file-name {
810+ width: 100%;
811+ }
812+ .file-name a {
813+ color: var(--color-link);
814+ text-decoration: none;
815+ }
816+ .file-name a:hover {
817+ text-decoration: underline;
818+ }
819+ .file-name a.file-name-dir {
820+ font-weight: 500;
821+ }
822+ .file-size {
823+ text-align: right;
824+ color: var(--color-text-muted);
825+ font-size: var(--text-xs);
826+ white-space: nowrap;
827+ padding-right: var(--space-4);
828+ }
829+ .file-icon-dir {
830+ color: var(--color-link);
831+ }
832+ .file-icon-file {
833+ color: var(--color-text-muted);
834+ }
835+ .tree-icon {
836+ display: inline-block;
837+ vertical-align: text-bottom;
838+ }
839+ .file-tree-row-up .file-name a {
840+ color: var(--color-text-muted);
841+ }
842+ .file-tree-row-up .file-name a:hover {
843+ color: var(--color-link);
844+ }
845+
846+ /* --- File blob --- */
847+ .file-blob-header {
848+ display: flex;
849+ align-items: center;
850+ justify-content: space-between;
851+ padding: var(--space-3) var(--space-4);
852+ background: var(--color-bg-subtle);
853+ border: 1px solid var(--color-border);
854+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
855+ }
856+ .file-blob-name {
857+ font-size: var(--text-sm);
858+ font-weight: 500;
859+ }
860+ .file-blob-body {
861+ border: 1px solid var(--color-border);
862+ border-top: none;
863+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
864+ overflow: auto;
865+ }
866+ /* Blob line-number table */
867+ .blob-table {
868+ min-width: 100%;
869+ border-collapse: collapse;
870+ font-family: var(--font-mono);
871+ font-size: var(--text-sm);
872+ line-height: 1.6;
873+ background-color: var(--shiki-light-bg, #fff);
874+ color: var(--shiki-light, inherit);
875+ }
876+ .blob-ln {
877+ width: 1%;
878+ min-width: 48px;
879+ text-align: right;
880+ padding: 0 var(--space-3);
881+ color: var(--color-text-muted);
882+ border-right: 1px solid var(--color-border-muted);
883+ user-select: none;
884+ white-space: nowrap;
885+ vertical-align: top;
886+ }
887+ .blob-ln a {
888+ color: inherit;
889+ text-decoration: none;
890+ }
891+ .blob-ln a:hover {
892+ color: var(--color-link);
893+ }
894+ .blob-code {
895+ padding: 0 var(--space-4);
896+ white-space: pre;
897+ overflow: visible;
898+ vertical-align: top;
899+ }
900+ .blob-table tr:target {
901+ background: color-mix(in srgb, #ddf4ff 60%, transparent);
902+ }
903+ .blob-table tr:target .blob-ln {
904+ background: color-mix(in srgb, #ddf4ff 80%, transparent);
905+ }
906+ .file-download-notice {
907+ padding: var(--space-8);
908+ text-align: center;
909+ color: var(--color-text-muted);
910+ }
911+ .file-download-notice p {
912+ margin-bottom: var(--space-4);
913+ }
914+
915+ /* --- Commit list --- */
916+ .commit-list {
917+ display: flex;
918+ flex-direction: column;
919+ }
920+ .commit-item {
921+ display: flex;
922+ align-items: flex-start;
923+ justify-content: space-between;
924+ padding: var(--space-3) 0;
925+ border-bottom: 1px solid var(--color-border-muted);
926+ gap: var(--space-4);
927+ flex-wrap: wrap;
928+ }
929+ .commit-item:last-child {
930+ border-bottom: none;
931+ }
932+ .commit-main {
933+ flex: 1;
934+ min-width: 0;
935+ }
936+ .commit-subject {
937+ color: var(--color-text);
938+ text-decoration: none;
939+ font-size: var(--text-sm);
940+ font-weight: 500;
941+ }
942+ .commit-subject:hover {
943+ color: var(--color-link);
944+ }
945+ .commit-meta {
946+ display: flex;
947+ align-items: center;
948+ gap: var(--space-3);
949+ font-size: var(--text-xs);
950+ color: var(--color-text-muted);
951+ white-space: nowrap;
952+ }
953+ .commit-hash {
954+ font-family: var(--font-mono);
955+ color: var(--color-link);
956+ text-decoration: none;
957+ background: var(--color-bg-inset);
958+ padding: 1px var(--space-2);
959+ border-radius: var(--radius-sm);
960+ }
961+ .commit-hash:hover {
962+ text-decoration: underline;
963+ }
964+ .mono {
965+ font-family: var(--font-mono);
966+ }
967+
968+ /* --- Commit detail viewer --- */
969+ .commit-page-top {
970+ display: flex;
971+ align-items: center;
972+ justify-content: space-between;
973+ margin-bottom: var(--space-4);
974+ }
975+ .commit-card {
976+ border: 1px solid var(--color-border);
977+ border-radius: var(--radius-lg);
978+ margin-bottom: var(--space-4);
979+ overflow: hidden;
980+ }
981+ .commit-card-subject {
982+ font-size: var(--text-xl);
983+ font-weight: 600;
984+ padding: var(--space-5) var(--space-6);
985+ border-bottom: 1px solid var(--color-border);
986+ line-height: 1.3;
987+ }
988+ .commit-card-body {
989+ padding: var(--space-4) var(--space-6);
990+ font-family: inherit;
991+ font-size: var(--text-sm);
992+ color: var(--color-text-muted);
993+ border-bottom: 1px solid var(--color-border);
994+ overflow-x: auto;
995+ }
996+ .commit-card-meta {
997+ padding: var(--space-4) var(--space-6);
998+ display: flex;
999+ flex-direction: column;
1000+ gap: var(--space-2);
1001+ background: var(--color-bg-subtle);
1002+ }
1003+ .commit-card-meta-row {
1004+ display: flex;
1005+ align-items: baseline;
1006+ gap: var(--space-3);
1007+ font-size: var(--text-sm);
1008+ flex-wrap: wrap;
1009+ }
1010+ .commit-meta-label {
1011+ font-weight: 600;
1012+ color: var(--color-text-muted);
1013+ min-width: 5rem;
1014+ font-size: var(--text-xs);
1015+ text-transform: uppercase;
1016+ letter-spacing: 0.04em;
1017+ }
1018+ .commit-sha-full {
1019+ font-size: var(--text-xs);
1020+ background: var(--color-bg-inset);
1021+ padding: 2px var(--space-2);
1022+ border-radius: var(--radius-sm);
1023+ word-break: break-all;
1024+ }
1025+ .commit-stats-bar {
1026+ font-size: var(--text-sm);
1027+ color: var(--color-text-muted);
1028+ margin-bottom: var(--space-4);
1029+ }
1030+
1031+ /* --- Commit layout (sidebar + diffs) --- */
1032+ .commit-layout {
1033+ display: grid;
1034+ grid-template-columns: 1fr;
1035+ gap: var(--space-5);
1036+ align-items: start;
1037+ }
1038+ .commit-diffs {
1039+ min-width: 0;
1040+ }
1041+ @media (min-width: 900px) {
1042+ .commit-layout {
1043+ grid-template-columns: 220px 1fr;
1044+ }
1045+ .commit-layout:has(.file-nav-details:not([open])) {
1046+ grid-template-columns: auto 1fr;
1047+ }
1048+ }
1049+
1050+ /* --- File nav sidebar --- */
1051+ .commit-file-nav {
1052+ position: sticky;
1053+ top: calc(56px + var(--space-4));
1054+ max-height: calc(100vh - 140px);
1055+ overflow-y: auto;
1056+ }
1057+ .file-nav-details {
1058+ border: 1px solid var(--color-border);
1059+ border-radius: var(--radius-lg);
1060+ background: var(--color-bg-subtle);
1061+ overflow: hidden;
1062+ }
1063+ .file-nav-toggle {
1064+ display: flex;
1065+ align-items: center;
1066+ gap: var(--space-2);
1067+ padding: var(--space-3) var(--space-4);
1068+ font-size: var(--text-sm);
1069+ font-weight: 500;
1070+ cursor: pointer;
1071+ list-style: none;
1072+ user-select: none;
1073+ border-bottom: 1px solid var(--color-border);
1074+ }
1075+ .file-nav-toggle::-webkit-details-marker {
1076+ display: none;
1077+ }
1078+ .file-nav-details:not([open]) .file-nav-toggle {
1079+ border-bottom: none;
1080+ }
1081+ .file-nav-toggle-icon {
1082+ font-size: 1em;
1083+ transition: transform 0.15s;
1084+ }
1085+ .file-nav-details:not([open]) .file-nav-toggle-icon {
1086+ transform: rotate(-90deg);
1087+ }
1088+ .file-nav-list {
1089+ padding: var(--space-1) 0;
1090+ list-style: none;
1091+ }
1092+ .file-nav-item {
1093+ display: flex;
1094+ align-items: center;
1095+ gap: var(--space-2);
1096+ padding: var(--space-1) var(--space-3);
1097+ font-size: var(--text-xs);
1098+ text-decoration: none;
1099+ color: var(--color-text);
1100+ border-radius: 0;
1101+ overflow: hidden;
1102+ }
1103+ .file-nav-item:hover {
1104+ background: var(--color-bg-inset);
1105+ }
1106+ .file-nav-status {
1107+ flex-shrink: 0;
1108+ width: 16px;
1109+ height: 16px;
1110+ display: inline-flex;
1111+ align-items: center;
1112+ justify-content: center;
1113+ border-radius: var(--radius-sm);
1114+ font-size: 9px;
1115+ font-weight: 700;
1116+ font-family: var(--font-mono);
1117+ }
1118+ .file-status-added {
1119+ background: var(--color-success-bg);
1120+ color: var(--color-success);
1121+ }
1122+ .file-status-deleted {
1123+ background: var(--color-danger-bg);
1124+ color: var(--color-danger);
1125+ }
1126+ .file-status-modified {
1127+ background: var(--color-accent-bg);
1128+ color: var(--color-accent);
1129+ }
1130+ .file-status-renamed {
1131+ background: var(--color-merged-bg);
1132+ color: var(--color-merged);
1133+ }
1134+ .file-status-copied {
1135+ background: var(--color-merged-bg);
1136+ color: var(--color-merged);
1137+ }
1138+ .file-nav-name {
1139+ flex: 1;
1140+ overflow: hidden;
1141+ text-overflow: ellipsis;
1142+ white-space: nowrap;
1143+ font-family: var(--font-mono);
1144+ }
1145+ .file-nav-stat {
1146+ flex-shrink: 0;
1147+ display: flex;
1148+ gap: var(--space-1);
1149+ font-size: var(--text-xs);
1150+ font-family: var(--font-mono);
1151+ }
1152+ .nav-add {
1153+ color: var(--color-success);
1154+ }
1155+ .nav-del {
1156+ color: var(--color-danger);
1157+ }
1158+ .file-nav-dir > details > .file-nav-list {
1159+ padding-left: var(--space-4);
1160+ }
1161+ .file-nav-dir-toggle {
1162+ display: flex;
1163+ align-items: center;
1164+ gap: var(--space-1);
1165+ padding: var(--space-1) var(--space-3);
1166+ font-size: var(--text-xs);
1167+ font-family: var(--font-mono);
1168+ color: var(--color-text-muted);
1169+ cursor: pointer;
1170+ list-style: none;
1171+ user-select: none;
1172+ }
1173+ .file-nav-dir-toggle::-webkit-details-marker {
1174+ display: none;
1175+ }
1176+ .file-nav-dir > details:not([open]) .file-nav-toggle-icon {
1177+ transform: rotate(-90deg);
1178+ }
1179+
1180+ /* --- Diff file cards --- */
1181+ .diff-file {
1182+ border: 1px solid var(--color-border);
1183+ border-radius: var(--radius-lg);
1184+ margin-bottom: var(--space-4);
1185+ overflow: hidden;
1186+ }
1187+ .diff-file-header {
1188+ display: flex;
1189+ align-items: center;
1190+ justify-content: space-between;
1191+ gap: var(--space-3);
1192+ padding: var(--space-3) var(--space-4);
1193+ background: var(--color-bg-subtle);
1194+ cursor: pointer;
1195+ list-style: none;
1196+ user-select: none;
1197+ flex-wrap: wrap;
1198+ }
1199+ .diff-file-header::-webkit-details-marker {
1200+ display: none;
1201+ }
1202+ .diff-file-header-left {
1203+ display: flex;
1204+ align-items: center;
1205+ gap: var(--space-2);
1206+ min-width: 0;
1207+ flex: 1;
1208+ }
1209+ .diff-file-header-right {
1210+ display: flex;
1211+ align-items: center;
1212+ gap: var(--space-2);
1213+ flex-shrink: 0;
1214+ flex-wrap: wrap;
1215+ }
1216+ .diff-file-toggle-icon {
1217+ font-size: 0.7em;
1218+ flex-shrink: 0;
1219+ transition: transform 0.15s;
1220+ }
1221+ .diff-file:not([open]) .diff-file-toggle-icon {
1222+ transform: rotate(-90deg);
1223+ }
1224+ .diff-status-badge {
1225+ flex-shrink: 0;
1226+ width: 18px;
1227+ height: 18px;
1228+ display: inline-flex;
1229+ align-items: center;
1230+ justify-content: center;
1231+ border-radius: var(--radius-sm);
1232+ font-size: 10px;
1233+ font-weight: 700;
1234+ font-family: var(--font-mono);
1235+ }
1236+ .diff-status-added {
1237+ background: var(--color-success-bg);
1238+ color: var(--color-success);
1239+ }
1240+ .diff-status-deleted {
1241+ background: var(--color-danger-bg);
1242+ color: var(--color-danger);
1243+ }
1244+ .diff-status-modified {
1245+ background: var(--color-accent-bg);
1246+ color: var(--color-accent);
1247+ }
1248+ .diff-status-renamed {
1249+ background: var(--color-merged-bg);
1250+ color: var(--color-merged);
1251+ }
1252+ .diff-status-copied {
1253+ background: var(--color-merged-bg);
1254+ color: var(--color-merged);
1255+ }
1256+ .diff-file-path {
1257+ font-size: var(--text-sm);
1258+ font-weight: 500;
1259+ overflow: hidden;
1260+ text-overflow: ellipsis;
1261+ white-space: nowrap;
1262+ }
1263+ .diff-rename-arrow {
1264+ font-size: var(--text-xs);
1265+ color: var(--color-text-muted);
1266+ white-space: nowrap;
1267+ overflow: hidden;
1268+ text-overflow: ellipsis;
1269+ }
1270+ .diff-stat-add {
1271+ color: var(--color-success);
1272+ font-size: var(--text-sm);
1273+ font-family: var(--font-mono);
1274+ font-weight: 600;
1275+ }
1276+ .diff-stat-del {
1277+ color: var(--color-danger);
1278+ font-size: var(--text-sm);
1279+ font-family: var(--font-mono);
1280+ font-weight: 600;
1281+ }
1282+ .btn-xs {
1283+ padding: 1px var(--space-2);
1284+ font-size: var(--text-xs);
1285+ border: 1px solid var(--color-border);
1286+ border-radius: var(--radius-sm);
1287+ background: var(--color-bg);
1288+ color: var(--color-text);
1289+ text-decoration: none;
1290+ cursor: pointer;
1291+ white-space: nowrap;
1292+ font-family: var(--font-mono);
1293+ display: inline-flex;
1294+ align-items: center;
1295+ transition: background 0.1s;
1296+ }
1297+ .btn-xs:hover {
1298+ background: var(--color-bg-inset);
1299+ }
1300+
1301+ /* --- Diff file body --- */
1302+ .diff-binary-notice {
1303+ padding: var(--space-4) var(--space-6);
1304+ font-size: var(--text-sm);
1305+ color: var(--color-text-muted);
1306+ font-style: italic;
1307+ }
1308+ .diff-hunk {
1309+ border-top: 1px solid var(--color-border-muted);
1310+ overflow-x: auto;
1311+ }
1312+ .diff-hunk:first-child {
1313+ border-top: none;
1314+ }
1315+ .diff-hunk-header {
1316+ padding: var(--space-1) var(--space-4);
1317+ font-family: var(--font-mono);
1318+ font-size: var(--text-xs);
1319+ font-weight: normal;
1320+ text-align: left;
1321+ background: color-mix(
1322+ in srgb,
1323+ var(--color-merged-bg) 50%,
1324+ var(--color-bg)
1325+ );
1326+ color: var(--color-merged);
1327+ border-bottom: 1px solid var(--color-border-muted);
1328+ white-space: pre;
1329+ }
1330+
1331+ /* --- Diff table --- */
1332+ .diff-table {
1333+ min-width: 100%;
1334+ border-collapse: collapse;
1335+ font-family: var(--font-mono);
1336+ font-size: var(--text-xs);
1337+ line-height: 1.5;
1338+ }
1339+ .diff-ln {
1340+ width: 44px;
1341+ min-width: 44px;
1342+ text-align: right;
1343+ padding: 0 var(--space-2);
1344+ color: var(--color-text-muted);
1345+ border-right: 1px solid var(--color-border-muted);
1346+ user-select: none;
1347+ vertical-align: top;
1348+ white-space: nowrap;
1349+ }
1350+ .diff-sign {
1351+ width: 18px;
1352+ min-width: 18px;
1353+ text-align: center;
1354+ padding: 0 var(--space-1);
1355+ user-select: none;
1356+ vertical-align: top;
1357+ }
1358+ .diff-code {
1359+ padding: 0 var(--space-3);
1360+ white-space: pre;
1361+ overflow: visible;
1362+ vertical-align: top;
1363+ word-break: normal;
1364+ }
1365+ /* Row type backgrounds */
1366+ .diff-row-add {
1367+ background: color-mix(
1368+ in srgb,
1369+ var(--color-success-bg) 70%,
1370+ transparent
1371+ );
1372+ }
1373+ .diff-row-del {
1374+ background: color-mix(in srgb, var(--color-danger-bg) 70%, transparent);
1375+ }
1376+ .diff-row-add .diff-ln,
1377+ .diff-row-add .diff-sign {
1378+ background: color-mix(
1379+ in srgb,
1380+ var(--color-success-bg) 90%,
1381+ transparent
1382+ );
1383+ }
1384+ .diff-row-del .diff-ln,
1385+ .diff-row-del .diff-sign {
1386+ background: color-mix(in srgb, var(--color-danger-bg) 90%, transparent);
1387+ }
1388+ .diff-row-add .diff-sign {
1389+ color: var(--color-success);
1390+ font-weight: 700;
1391+ }
1392+ .diff-row-del .diff-sign {
1393+ color: var(--color-danger);
1394+ font-weight: 700;
1395+ }
1396+
1397+ /* Shiki dark-mode support in diff code cells */
1398+ @media (prefers-color-scheme: dark) {
1399+ .diff-code span[style] {
1400+ /* biome-ignore lint/complexity/noImportantStyles: required to override shiki inline styles */
1401+ color: var(--shiki-dark) !important;
1402+ }
1403+ }
1404+
1405+ /* Shiki dark-mode support in file blob view */
1406+ @media (prefers-color-scheme: dark) {
1407+ .blob-table {
1408+ background-color: var(--shiki-dark-bg);
1409+ color: var(--shiki-dark);
1410+ }
1411+ .blob-table span[style] {
1412+ /* biome-ignore lint/complexity/noImportantStyles: required to override shiki inline styles */
1413+ color: var(--shiki-dark) !important;
1414+ }
1415+ .blob-table tr:target {
1416+ background: color-mix(in srgb, #1c3a52 60%, transparent);
1417+ }
1418+ .blob-table tr:target .blob-ln {
1419+ background: color-mix(in srgb, #1c3a52 80%, transparent);
1420+ }
1421+ }
1422+ :root[data-theme="dark"] .blob-table {
1423+ background-color: var(--shiki-dark-bg);
1424+ color: var(--shiki-dark);
1425+ }
1426+ :root[data-theme="dark"] .blob-table span[style] {
1427+ /* biome-ignore lint/complexity/noImportantStyles: required to override shiki inline styles */
1428+ color: var(--shiki-dark) !important;
1429+ }
1430+
1431+ /* --- Issue / Patch list --- */
1432+ .list-header {
1433+ display: flex;
1434+ align-items: center;
1435+ justify-content: space-between;
1436+ padding: var(--space-3) var(--space-4);
1437+ background: var(--color-bg-subtle);
1438+ border: 1px solid var(--color-border);
1439+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
1440+ gap: var(--space-4);
1441+ }
1442+ .list-header-tabs {
1443+ display: flex;
1444+ gap: var(--space-2);
1445+ }
1446+ .list-tab {
1447+ padding: var(--space-1) var(--space-3);
1448+ border-radius: var(--radius-md);
1449+ font-size: var(--text-sm);
1450+ text-decoration: none;
1451+ color: var(--color-text-muted);
1452+ }
1453+ .list-tab:hover {
1454+ background: var(--color-bg-inset);
1455+ color: var(--color-text);
1456+ }
1457+ .list-tab.active {
1458+ background: var(--color-bg-inset);
1459+ color: var(--color-text);
1460+ font-weight: 600;
1461+ }
1462+ .issue-list {
1463+ border: 1px solid var(--color-border);
1464+ border-top: none;
1465+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
1466+ }
1467+ .issue-item {
1468+ padding: var(--space-4) var(--space-4);
1469+ border-bottom: 1px solid var(--color-border-muted);
1470+ }
1471+ .issue-item:last-child {
1472+ border-bottom: none;
1473+ }
1474+ .issue-main {
1475+ display: flex;
1476+ align-items: center;
1477+ gap: var(--space-2);
1478+ margin-bottom: var(--space-1);
1479+ flex-wrap: wrap;
1480+ }
1481+ .issue-title {
1482+ font-weight: 500;
1483+ text-decoration: none;
1484+ color: var(--color-text);
1485+ flex: 1;
1486+ }
1487+ .issue-title:hover {
1488+ color: var(--color-link);
1489+ }
1490+ .issue-meta {
1491+ font-size: var(--text-xs);
1492+ color: var(--color-text-muted);
1493+ display: flex;
1494+ gap: var(--space-3);
1495+ }
1496+ .issue-status-dot,
1497+ .patch-status-dot {
1498+ width: 8px;
1499+ height: 8px;
1500+ border-radius: 50%;
1501+ flex-shrink: 0;
1502+ }
1503+ .issue-status-dot.open,
1504+ .patch-status-dot.open {
1505+ background: var(--color-success);
1506+ }
1507+ .issue-status-dot.closed,
1508+ .patch-status-dot.closed {
1509+ background: var(--color-danger);
1510+ }
1511+ .issue-status-dot.completed {
1512+ background: var(--color-merged);
1513+ }
1514+ .patch-status-dot.merged {
1515+ background: var(--color-merged);
1516+ }
1517+
1518+ /* --- Issue detail / Timeline --- */
1519+ .issue-detail-header {
1520+ display: flex;
1521+ align-items: center;
1522+ gap: var(--space-3);
1523+ margin-bottom: var(--space-6);
1524+ flex-wrap: wrap;
1525+ }
1526+ .issue-number {
1527+ font-size: var(--text-2xl);
1528+ font-weight: 600;
1529+ color: var(--color-text-muted);
1530+ white-space: nowrap;
1531+ }
1532+ .issue-detail-title {
1533+ font-size: var(--text-2xl);
1534+ font-weight: 600;
1535+ flex: 1;
1536+ }
1537+ .issue-detail-meta-actions {
1538+ margin-left: auto;
1539+ display: flex;
1540+ gap: var(--space-2);
1541+ }
1542+ .timeline-item {
1543+ border: 1px solid var(--color-border);
1544+ border-radius: var(--radius-lg);
1545+ margin-bottom: var(--space-4);
1546+ overflow: hidden;
1547+ }
1548+ .timeline-author {
1549+ background: var(--color-bg-subtle);
1550+ padding: var(--space-3) var(--space-4);
1551+ font-size: var(--text-sm);
1552+ display: flex;
1553+ align-items: center;
1554+ justify-content: space-between;
1555+ flex-wrap: wrap;
1556+ gap: var(--space-3);
1557+ border-bottom: 1px solid var(--color-border);
1558+ }
1559+ .timeline-author time {
1560+ color: var(--color-text-muted);
1561+ font-size: var(--text-xs);
1562+ }
1563+ .timeline-author-right {
1564+ margin-left: auto;
1565+ display: flex;
1566+ align-items: center;
1567+ gap: var(--space-2);
1568+ }
1569+ .edited-indicator {
1570+ color: var(--color-text-muted);
1571+ font-size: var(--text-xs);
1572+ cursor: default;
1573+ }
1574+
1575+ /* --- Avatars --- */
1576+ .avatar {
1577+ display: inline-flex;
1578+ border-radius: 4px;
1579+ overflow: hidden;
1580+ flex-shrink: 0;
1581+ vertical-align: middle;
1582+ }
1583+ .avatar-img {
1584+ width: 100%;
1585+ height: 100%;
1586+ object-fit: cover;
1587+ display: block;
1588+ color: transparent;
1589+ }
1590+ .avatar-placeholder {
1591+ display: inline-flex;
1592+ align-items: center;
1593+ justify-content: center;
1594+ border-radius: 4px;
1595+ background: var(--color-bg-inset);
1596+ color: var(--color-text-muted);
1597+ font-weight: 600;
1598+ flex-shrink: 0;
1599+ vertical-align: middle;
1600+ }
1601+ .avatar-settings {
1602+ display: flex;
1603+ align-items: flex-start;
1604+ gap: var(--space-5);
1605+ }
1606+ .avatar-actions {
1607+ display: flex;
1608+ flex-direction: column;
1609+ gap: var(--space-3);
1610+ }
1611+ .nav-user {
1612+ display: inline-flex;
1613+ align-items: center;
1614+ gap: var(--space-2);
1615+ }
1616+ .timeline-body {
1617+ padding: var(--space-4);
1618+ }
1619+ .timeline-item-new {
1620+ padding: var(--space-4);
1621+ }
1622+ .timeline-item-new .section-title {
1623+ font-size: var(--text-base);
1624+ border: none;
1625+ padding-bottom: 0;
1626+ }
1627+ /* Edit <details> (summary only) in .timeline-author; form is a sibling shown via :has() */
1628+ .timeline-author-right .inline-edit-details {
1629+ border: none;
1630+ }
1631+ .timeline-author-right .inline-edit-details > summary {
1632+ padding: 0;
1633+ list-style: none;
1634+ }
1635+ .timeline-author-right
1636+ .inline-edit-details
1637+ > summary::-webkit-details-marker {
1638+ display: none;
1639+ }
1640+ .inline-edit-details .when-open {
1641+ display: none;
1642+ }
1643+ .inline-edit-details[open] .when-closed {
1644+ display: none;
1645+ }
1646+ .inline-edit-details[open] .when-open {
1647+ display: inline;
1648+ }
1649+ .inline-edit-form-area {
1650+ display: none;
1651+ border-top: 1px solid var(--color-border-muted);
1652+ }
1653+ .timeline-item:has(.inline-edit-details[open]) .inline-edit-form-area {
1654+ display: block;
1655+ }
1656+ .inline-edit-form {
1657+ padding: var(--space-4);
1658+ display: flex;
1659+ flex-direction: column;
1660+ gap: var(--space-3);
1661+ }
1662+
1663+ /* --- Reactions --- */
1664+ .reaction-bar {
1665+ display: flex;
1666+ flex-wrap: wrap;
1667+ gap: var(--space-2);
1668+ padding: var(--space-2) var(--space-4);
1669+ border-top: 1px solid var(--color-border-muted);
1670+ }
1671+ .reaction-form {
1672+ display: inline;
1673+ }
1674+ .reaction-btn {
1675+ display: inline-flex;
1676+ align-items: center;
1677+ gap: var(--space-1);
1678+ padding: var(--space-1) var(--space-3);
1679+ border: 1px solid var(--color-border);
1680+ border-radius: var(--radius-full);
1681+ background: var(--color-bg-subtle);
1682+ color: var(--color-text);
1683+ font-size: var(--text-sm);
1684+ cursor: pointer;
1685+ transition: background 0.1s;
1686+ }
1687+ .reaction-btn:hover {
1688+ background: var(--color-bg-inset);
1689+ }
1690+ .reaction-btn.reacted {
1691+ background: var(--color-accent-bg);
1692+ border-color: var(--color-accent);
1693+ }
1694+ .reaction-picker {
1695+ position: relative;
1696+ }
1697+ .reaction-add-btn {
1698+ display: inline-flex;
1699+ align-items: center;
1700+ justify-content: center;
1701+ width: 28px;
1702+ height: 28px;
1703+ border: 1px dashed var(--color-border);
1704+ border-radius: var(--radius-full);
1705+ background: transparent;
1706+ cursor: pointer;
1707+ font-size: var(--text-base);
1708+ color: var(--color-text-muted);
1709+ list-style: none;
1710+ }
1711+ .reaction-add-btn::-webkit-details-marker {
1712+ display: none;
1713+ }
1714+ .reaction-picker-dropdown {
1715+ position: absolute;
1716+ bottom: calc(100% + var(--space-2));
1717+ left: 0;
1718+ background: var(--color-bg);
1719+ border: 1px solid var(--color-border);
1720+ border-radius: var(--radius-lg);
1721+ padding: var(--space-2);
1722+ display: flex;
1723+ gap: var(--space-1);
1724+ z-index: 10;
1725+ }
1726+ .reaction-picker-btn {
1727+ background: none;
1728+ border: none;
1729+ font-size: 1.25rem;
1730+ cursor: pointer;
1731+ padding: var(--space-1);
1732+ border-radius: var(--radius-sm);
1733+ }
1734+ .reaction-picker-btn:hover {
1735+ background: var(--color-bg-inset);
1736+ }
1737+
1738+ /* --- Patch subview tabs --- */
1739+ .patch-tab-nav {
1740+ display: flex;
1741+ border-bottom: 1px solid var(--color-border);
1742+ margin-bottom: var(--space-6);
1743+ }
1744+ .tab-count {
1745+ display: inline-flex;
1746+ align-items: center;
1747+ justify-content: center;
1748+ min-width: 1.4em;
1749+ padding: 0 var(--space-1);
1750+ margin-left: var(--space-1);
1751+ font-size: var(--text-xs);
1752+ font-weight: 500;
1753+ background: var(--color-bg-inset);
1754+ border-radius: 999px;
1755+ color: var(--color-text-muted);
1756+ }
1757+
1758+ /* --- Patch apply status --- */
1759+ .apply-status {
1760+ margin: var(--space-4) 0;
1761+ }
1762+ .apply-result {
1763+ display: flex;
1764+ align-items: flex-start;
1765+ gap: var(--space-2);
1766+ padding: var(--space-3) var(--space-4);
1767+ border-radius: var(--radius-md);
1768+ font-size: var(--text-sm);
1769+ flex-direction: column;
1770+ }
1771+ .apply-result > div {
1772+ display: flex;
1773+ gap: var(--space-2);
1774+ align-items: center;
1775+ }
1776+ .apply-clean {
1777+ background: var(--color-success-bg);
1778+ color: var(--color-success);
1779+ border: 1px solid var(--color-success);
1780+ }
1781+ .apply-conflict {
1782+ background: var(--color-danger-bg);
1783+ color: var(--color-danger);
1784+ border: 1px solid var(--color-danger);
1785+ }
1786+ .apply-checking,
1787+ .apply-unknown {
1788+ color: var(--color-text-muted);
1789+ font-style: italic;
1790+ font-size: var(--text-sm);
1791+ }
1792+ .apply-output {
1793+ font-family: var(--font-mono);
1794+ font-size: var(--text-xs);
1795+ white-space: pre-wrap;
1796+ margin-top: var(--space-2);
1797+ padding: var(--space-2);
1798+ background: rgba(0, 0, 0, 0.05);
1799+ border-radius: var(--radius-sm);
1800+ width: 100%;
1801+ }
1802+ .patch-actions {
1803+ display: flex;
1804+ gap: var(--space-3);
1805+ margin: var(--space-4) 0;
1806+ flex-wrap: wrap;
1807+ }
1808+ .patch-card-title-row {
1809+ display: flex;
1810+ align-items: flex-start;
1811+ gap: var(--space-3);
1812+ justify-content: space-between;
1813+ margin-bottom: var(--space-3);
1814+ }
1815+ .patch-card-title-row .commit-card-subject {
1816+ margin-bottom: 0;
1817+ }
1818+ .patch-card-description {
1819+ margin-top: var(--space-4);
1820+ padding-top: var(--space-4);
1821+ border-top: 1px solid var(--color-border-muted);
1822+ }
1823+
1824+ /* --- Repo home layout --- */
1825+ .repo-home-layout {
1826+ display: grid;
1827+ gap: var(--space-8);
1828+ }
1829+ @media (min-width: 768px) {
1830+ .repo-home-layout {
1831+ grid-template-columns: 1fr 1fr;
1832+ }
1833+ }
1834+ .readme-section {
1835+ border: 1px solid var(--color-border);
1836+ border-radius: var(--radius-lg);
1837+ overflow: hidden;
1838+ }
1839+ .readme-header {
1840+ background: var(--color-bg-subtle);
1841+ padding: var(--space-3) var(--space-4);
1842+ font-size: var(--text-sm);
1843+ font-weight: 500;
1844+ border-bottom: 1px solid var(--color-border);
1845+ }
1846+ /* --- Code setup block --- */
1847+ .code-setup {
1848+ background: var(--color-bg-subtle);
1849+ border: 1px solid var(--color-border);
1850+ border-radius: var(--radius-lg);
1851+ padding: var(--space-4);
1852+ font-size: var(--text-sm);
1853+ margin-top: var(--space-4);
1854+ overflow: auto;
1855+ text-align: left;
1856+ }
1857+ .code-plain {
1858+ background: var(--color-bg-subtle);
1859+ padding: var(--space-4);
1860+ overflow: auto;
1861+ font-size: var(--text-sm);
1862+ line-height: 1.6;
1863+ }
1864+
1865+ /* --- Markdown body --- */
1866+ .markdown-body {
1867+ font-size: var(--text-sm);
1868+ line-height: 1.6;
1869+ color: var(--color-text);
1870+ }
1871+ .markdown-body h1,
1872+ .markdown-body h2,
1873+ .markdown-body h3,
1874+ .markdown-body h4,
1875+ .markdown-body h5,
1876+ .markdown-body h6 {
1877+ margin-top: var(--space-6);
1878+ margin-bottom: var(--space-3);
1879+ }
1880+ .markdown-body p {
1881+ margin-bottom: var(--space-4);
1882+ }
1883+ .markdown-body a {
1884+ color: var(--color-link);
1885+ }
1886+ .markdown-body code {
1887+ background: var(--color-bg-inset);
1888+ padding: 0.1em 0.3em;
1889+ border-radius: var(--radius-sm);
1890+ font-size: 0.9em;
1891+ }
1892+ .markdown-body pre {
1893+ background: var(--color-bg-subtle);
1894+ border: 1px solid var(--color-border);
1895+ border-radius: var(--radius-md);
1896+ padding: var(--space-4);
1897+ overflow: auto;
1898+ margin-bottom: var(--space-4);
1899+ }
1900+ .markdown-body pre code {
1901+ background: none;
1902+ padding: 0;
1903+ }
1904+ .markdown-body ul,
1905+ .markdown-body ol {
1906+ padding-left: var(--space-6);
1907+ margin-bottom: var(--space-4);
1908+ }
1909+ .markdown-body ul {
1910+ list-style: disc;
1911+ }
1912+ .markdown-body ol {
1913+ list-style: decimal;
1914+ }
1915+ .markdown-body li {
1916+ margin-bottom: var(--space-1);
1917+ }
1918+ .markdown-body blockquote {
1919+ border-left: 3px solid var(--color-border);
1920+ padding-left: var(--space-4);
1921+ color: var(--color-text-muted);
1922+ margin: var(--space-4) 0;
1923+ }
1924+ .markdown-body table {
1925+ width: 100%;
1926+ border-collapse: collapse;
1927+ margin-bottom: var(--space-4);
1928+ }
1929+ .markdown-body th,
1930+ .markdown-body td {
1931+ padding: var(--space-2) var(--space-3);
1932+ border: 1px solid var(--color-border);
1933+ }
1934+ .markdown-body th {
1935+ background: var(--color-bg-subtle);
1936+ font-weight: 600;
1937+ }
1938+ .markdown-body hr {
1939+ border: none;
1940+ border-top: 1px solid var(--color-border);
1941+ margin: var(--space-6) 0;
1942+ }
1943+ .markdown-body img {
1944+ max-width: 100%;
1945+ border-radius: var(--radius-md);
1946+ }
1947+ .markdown-body details {
1948+ margin-bottom: var(--space-4);
1949+ }
1950+ .markdown-body summary {
1951+ cursor: pointer;
1952+ font-weight: 500;
1953+ }
1954+ .readme-section .markdown-body {
1955+ padding: var(--space-4);
1956+ }
1957+
1958+ /* --- Search form --- */
1959+ .search-form {
1960+ margin-bottom: var(--space-5);
1961+ }
1962+ .search-input-wrap {
1963+ display: flex;
1964+ gap: var(--space-2);
1965+ max-width: 480px;
1966+ }
1967+ .search-input {
1968+ flex: 1;
1969+ padding: var(--space-2) var(--space-3);
1970+ border: 1px solid var(--color-border);
1971+ border-radius: var(--radius-md);
1972+ background: var(--color-bg);
1973+ color: var(--color-text);
1974+ font-size: var(--text-sm);
1975+ }
1976+ .search-input:focus {
1977+ outline: 2px solid var(--color-accent);
1978+ outline-offset: -1px;
1979+ border-color: var(--color-accent);
1980+ }
1981+
1982+ /* --- Issue close bar --- */
1983+ .issue-close-bar {
1984+ display: flex;
1985+ justify-content: flex-end;
1986+ padding: var(--space-3) 0;
1987+ border-top: 1px solid var(--color-border-muted);
1988+ margin-top: var(--space-2);
1989+ }
1990+
1991+ /* --- Auth divider --- */
1992+ .auth-divider {
1993+ display: flex;
1994+ align-items: center;
1995+ gap: var(--space-3);
1996+ margin: var(--space-4) 0;
1997+ color: var(--color-text-muted);
1998+ font-size: var(--text-sm);
1999+ }
2000+ .auth-divider::before,
2001+ .auth-divider::after {
2002+ content: "";
2003+ flex: 1;
2004+ height: 1px;
2005+ background: var(--color-border);
2006+ }
2007+
2008+ /* --- Passkey UI in settings --- */
2009+ .passkey-actions {
2010+ display: flex;
2011+ flex-direction: column;
2012+ gap: var(--space-3);
2013+ align-items: flex-start;
2014+ margin-top: var(--space-3);
2015+ }
2016+ .passkey-status {
2017+ font-size: var(--text-sm);
2018+ }
2019+
2020+ /* --- Danger zone --- */
2021+ .danger-zone {
2022+ margin-top: var(--space-8);
2023+ }
2024+ .danger-title {
2025+ color: var(--color-danger);
2026+ border-bottom-color: var(--color-danger);
2027+ }
2028+ .danger-card {
2029+ border-color: var(--color-danger);
2030+ background: var(--color-danger-bg);
2031+ }
2032+ .danger-item {
2033+ display: flex;
2034+ align-items: center;
2035+ justify-content: space-between;
2036+ gap: var(--space-4);
2037+ flex-wrap: wrap;
2038+ }
2039+ .danger-item p {
2040+ margin: var(--space-1) 0 0;
2041+ }
2042+ .btn-danger {
2043+ background: var(--color-danger);
2044+ border-color: transparent;
2045+ color: #fff;
2046+ }
2047+ .btn-danger:hover {
2048+ filter: brightness(0.9);
2049+ }
2050+
2051+ /* --- Release list items --- */
2052+ .release-item-header {
2053+ display: flex;
2054+ gap: var(--space-3);
2055+ align-items: flex-start;
2056+ }
2057+ .release-item-main {
2058+ flex: 1;
2059+ min-width: 0;
2060+ }
2061+ .release-item-title {
2062+ font-size: var(--text-lg);
2063+ font-weight: 700;
2064+ text-decoration: none;
2065+ color: var(--color-text);
2066+ display: block;
2067+ margin-bottom: var(--space-1);
2068+ }
2069+ .release-item-title:hover {
2070+ color: var(--color-link);
2071+ }
2072+ .release-item-meta {
2073+ display: flex;
2074+ flex-wrap: wrap;
2075+ gap: var(--space-3);
2076+ align-items: center;
2077+ font-size: var(--text-xs);
2078+ color: var(--color-text-muted);
2079+ }
2080+ .release-item-date {
2081+ display: flex;
2082+ flex-direction: column;
2083+ align-items: flex-end;
2084+ gap: var(--space-2);
2085+ font-size: var(--text-xs);
2086+ color: var(--color-text-muted);
2087+ white-space: nowrap;
2088+ flex-shrink: 0;
2089+ }
2090+
2091+ /* --- Release notes preview in list --- */
2092+ .release-notes-section {
2093+ margin-top: var(--space-3);
2094+ }
2095+ .release-notes-label {
2096+ font-size: var(--text-xs);
2097+ font-weight: 600;
2098+ color: var(--color-text-muted);
2099+ text-transform: uppercase;
2100+ letter-spacing: 0.05em;
2101+ margin-bottom: var(--space-2);
2102+ }
2103+ .notes-expand > summary {
2104+ list-style: none;
2105+ cursor: pointer;
2106+ outline: none;
2107+ }
2108+ .notes-expand > summary::-webkit-details-marker {
2109+ display: none;
2110+ }
2111+ .notes-preview {
2112+ max-height: calc(5 * 1.6em);
2113+ overflow: hidden;
2114+ position: relative;
2115+ white-space: pre-wrap;
2116+ font-size: var(--text-sm);
2117+ color: var(--color-text);
2118+ line-height: 1.6;
2119+ word-break: break-word;
2120+ margin-bottom: var(--space-1);
2121+ }
2122+ .notes-preview::after {
2123+ content: "";
2124+ position: absolute;
2125+ bottom: 0;
2126+ left: 0;
2127+ right: 0;
2128+ height: 2.5em;
2129+ background: linear-gradient(transparent, var(--color-bg));
2130+ pointer-events: none;
2131+ }
2132+ .notes-expand[open] .notes-preview {
2133+ display: none;
2134+ }
2135+ .notes-full {
2136+ padding-top: var(--space-3);
2137+ padding-bottom: var(--space-1);
2138+ }
2139+ .notes-expand[open] {
2140+ display: flex;
2141+ flex-direction: column;
2142+ }
2143+ .notes-expand[open] > summary {
2144+ order: 1;
2145+ }
2146+ .notes-expand[open] > .notes-full {
2147+ order: 0;
2148+ }
2149+ .notes-toggle-label {
2150+ display: inline-block;
2151+ font-size: var(--text-xs);
2152+ color: var(--color-link);
2153+ margin-top: var(--space-1);
2154+ }
2155+ .notes-toggle-label:hover {
2156+ text-decoration: underline;
2157+ }
2158+ .notes-toggle-label::before {
2159+ content: "Show more ↓";
2160+ }
2161+ .notes-expand[open] .notes-toggle-label::before {
2162+ content: "Show less ↑";
2163+ }
2164+
2165+ /* --- Release detail --- */
2166+ .release-detail {
2167+ padding: var(--space-6) 0;
2168+ }
2169+ .release-header {
2170+ margin-bottom: var(--space-6);
2171+ }
2172+ .release-notes {
2173+ margin-bottom: var(--space-6);
2174+ }
2175+ .release-assets {
2176+ margin-bottom: var(--space-6);
2177+ }
2178+ .release-assets-heading {
2179+ font-size: var(--text-base);
2180+ font-weight: 600;
2181+ margin-bottom: var(--space-3);
2182+ }
2183+ .asset-list {
2184+ border: 1px solid var(--color-border);
2185+ border-radius: var(--radius-lg);
2186+ overflow: hidden;
2187+ }
2188+ .asset-item {
2189+ display: flex;
2190+ align-items: center;
2191+ gap: var(--space-4);
2192+ padding: var(--space-3) var(--space-4);
2193+ border-bottom: 1px solid var(--color-border-muted);
2194+ font-size: var(--text-sm);
2195+ }
2196+ .asset-item:last-child {
2197+ border-bottom: none;
2198+ }
2199+ .asset-name {
2200+ flex: 1;
2201+ color: var(--color-link);
2202+ text-decoration: none;
2203+ font-family: var(--font-mono);
2204+ font-size: var(--text-xs);
2205+ }
2206+ .asset-name:hover {
2207+ text-decoration: underline;
2208+ }
2209+ .asset-name-pending {
2210+ color: var(--color-text-muted);
2211+ cursor: default;
2212+ }
2213+ .asset-item-pending {
2214+ opacity: 0.6;
2215+ }
2216+ .asset-size,
2217+ .asset-meta {
2218+ color: var(--color-text-muted);
2219+ font-size: var(--text-xs);
2220+ white-space: nowrap;
2221+ }
2222+ .release-back {
2223+ margin-top: var(--space-4);
2224+ display: flex;
2225+ gap: var(--space-3);
2226+ align-items: center;
2227+ justify-content: space-between;
2228+ }
2229+}
2230+
2231+/* ============================================================
2232+ @layer utilities
2233+ ============================================================ */
2234+@layer utilities {
2235+ .text-muted {
2236+ color: var(--color-text-muted);
2237+ }
2238+ .text-sm {
2239+ font-size: var(--text-sm);
2240+ }
2241+ .text-xs {
2242+ font-size: var(--text-xs);
2243+ }
2244+ .font-mono {
2245+ font-family: var(--font-mono);
2246+ }
2247+ .mt-1 {
2248+ margin-top: var(--space-1);
2249+ }
2250+ .mt-2 {
2251+ margin-top: var(--space-2);
2252+ }
2253+ .mt-4 {
2254+ margin-top: var(--space-4);
2255+ }
2256+ .mt-6 {
2257+ margin-top: var(--space-6);
2258+ }
2259+ .mb-4 {
2260+ margin-bottom: var(--space-4);
2261+ }
2262+ .flex {
2263+ display: flex;
2264+ }
2265+ .gap-2 {
2266+ gap: var(--space-2);
2267+ }
2268+ .gap-4 {
2269+ gap: var(--space-4);
2270+ }
2271+ .items-center {
2272+ align-items: center;
2273+ }
2274+ .justify-between {
2275+ justify-content: space-between;
2276+ }
2277+ .hidden {
2278+ display: none;
2279+ }
2280+}
2281+
2282+/* ============================================================
2283+ Pagination
2284+ ============================================================ */
2285+.commit-cursor-nav {
2286+ display: flex;
2287+ justify-content: space-between;
2288+ border-top: 1px solid var(--color-border);
2289+ margin-top: var(--space-2);
2290+ padding: var(--space-4) 0 var(--space-2);
2291+}
2292+
2293+.commit-cursor-prev {
2294+ flex: 1;
2295+}
2296+.commit-cursor-next {
2297+ flex: 1;
2298+ text-align: right;
2299+}
2300+
2301+.pagination {
2302+ display: grid;
2303+ grid-template-columns: 1fr auto 1fr;
2304+ align-items: center;
2305+ border-top: 1px solid var(--color-border);
2306+ margin-top: var(--space-2);
2307+ padding: var(--space-4) 0 var(--space-2);
2308+}
2309+
2310+.pagination-prev {
2311+ justify-self: start;
2312+}
2313+.pagination-next {
2314+ justify-self: end;
2315+}
2316+
2317+.pagination-info {
2318+ justify-self: center;
2319+ font-size: var(--text-sm);
2320+ color: var(--color-text-muted);
2321+}
2322+
2323+.pagination-btn {
2324+ display: inline-flex;
2325+ align-items: center;
2326+ gap: var(--space-1);
2327+ padding: var(--space-1) var(--space-3);
2328+ border: 1px solid var(--color-border);
2329+ border-radius: var(--radius-md);
2330+ font-size: var(--text-sm);
2331+ font-weight: 500;
2332+ text-decoration: none;
2333+ color: var(--color-text);
2334+ background: var(--color-bg-subtle);
2335+ transition:
2336+ background 0.1s,
2337+ border-color 0.1s;
2338+ white-space: nowrap;
2339+}
2340+
2341+.pagination-btn:hover {
2342+ background: var(--color-bg-inset);
2343+ border-color: var(--color-border);
2344+ color: var(--color-text);
2345+ text-decoration: none;
2346+}
2347+
2348+/* Settings */
2349+.theme-options {
2350+ display: flex;
2351+ gap: var(--space-4);
2352+ margin-bottom: var(--space-4);
2353+ flex-wrap: wrap;
2354+}
2355+.theme-option {
2356+ display: flex;
2357+ align-items: center;
2358+ gap: var(--space-2);
2359+ cursor: pointer;
2360+ font-size: var(--text-sm);
2361+}
2362+.settings-form {
2363+ display: flex;
2364+ flex-direction: column;
2365+ gap: var(--space-4);
2366+}
2367+.passkey-list {
2368+ display: flex;
2369+ flex-direction: column;
2370+ gap: var(--space-2);
2371+ margin-bottom: var(--space-4);
2372+}
2373+.passkey-item {
2374+ display: flex;
2375+ align-items: center;
2376+ gap: var(--space-3);
2377+ padding: var(--space-2) 0;
2378+ border-bottom: 1px solid var(--color-border-muted);
2379+ font-size: var(--text-sm);
2380+}
2381+.passkey-item:last-child {
2382+ border-bottom: none;
2383+}
2384+.passkey-date {
2385+ flex: 1;
2386+ color: var(--color-text-muted);
2387+}
2388+.ssh-key-info {
2389+ flex: 1;
2390+ display: flex;
2391+ flex-direction: column;
2392+ gap: var(--space-1);
2393+ min-width: 0;
2394+}
2395+.ssh-key-name {
2396+ font-weight: 500;
2397+}
2398+.ssh-key-fingerprint {
2399+ font-family: var(--font-mono);
2400+ font-size: var(--text-xs);
2401+ overflow: hidden;
2402+ text-overflow: ellipsis;
2403+ white-space: nowrap;
2404+}
2405+.form-textarea {
2406+ resize: vertical;
2407+ min-height: 4.5rem;
2408+ font-family: var(--font-mono);
2409+ font-size: var(--text-sm);
2410+}
2411+/* Clone popup */
2412+.clone-popup-details {
2413+ position: relative;
2414+ display: inline-block;
2415+}
2416+.clone-popup-details > summary {
2417+ list-style: none;
2418+}
2419+.clone-popup-details > summary::-webkit-details-marker {
2420+ display: none;
2421+}
2422+.clone-popup {
2423+ position: absolute;
2424+ right: 1rem;
2425+ top: calc(100% + var(--space-2));
2426+ background: var(--color-bg);
2427+ border: 1px solid var(--color-border);
2428+ border-radius: var(--radius-lg);
2429+ padding: var(--space-3);
2430+ min-width: 22rem;
2431+ z-index: 100;
2432+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
2433+ display: flex;
2434+ flex-direction: column;
2435+ gap: var(--space-2);
2436+}
2437+.clone-url-row {
2438+ display: flex;
2439+ align-items: center;
2440+ gap: var(--space-2);
2441+}
2442+.clone-url-label {
2443+ font-size: var(--text-xs);
2444+ font-weight: 600;
2445+ color: var(--color-text-muted);
2446+ text-transform: uppercase;
2447+ letter-spacing: 0.05em;
2448+ min-width: 2.75rem;
2449+}
2450+.clone-url-input {
2451+ flex: 1;
2452+ font-family: var(--font-mono);
2453+ font-size: var(--text-sm);
2454+ background: var(--color-bg-subtle);
2455+ border: 1px solid var(--color-border-muted);
2456+ border-radius: var(--radius-md);
2457+ padding: var(--space-1) var(--space-2);
2458+ color: var(--color-text);
2459+ cursor: text;
2460+ min-width: 0;
2461+}
2462+.user-list {
2463+ display: flex;
2464+ flex-direction: column;
2465+ gap: var(--space-2);
2466+ margin-bottom: var(--space-6);
2467+}
2468+.user-item {
2469+ display: flex;
2470+ align-items: center;
2471+ gap: var(--space-3);
2472+ padding: var(--space-2) 0;
2473+ border-bottom: 1px solid var(--color-border-muted);
2474+ font-size: var(--text-sm);
2475+}
2476+.user-item:last-child {
2477+ border-bottom: none;
2478+}
2479+.user-name {
2480+ font-weight: 500;
2481+ flex: 1;
2482+}
2483+.user-date {
2484+ color: var(--color-text-muted);
2485+ font-size: var(--text-xs);
2486+}
Msrc/views/issues/IssueDetail.tsx
@@ -56,6 +56,20 @@ export function IssueDetail({
5656 </span>
5757 {canEditIssue && (
5858 <div class="issue-detail-meta-actions">
59+ {user?.isAdmin && issue.status === "open" && (
60+ <form
61+ method="POST"
62+ action={`/${repo.name}/issues/${issue.number}/complete`}
63+ class="inline-form"
64+ >
65+ <button
66+ type="submit"
67+ class="btn btn-sm btn-primary"
68+ >
69+ Completed
70+ </button>
71+ </form>
72+ )}
5973 {user?.isAdmin && (
6074 <form
6175 method="POST"
Msrc/views/issues/IssueList.tsx
@@ -1,6 +1,7 @@
11 import type { IssueRow, RepositoryRow } from "../../db/index.ts";
22 import { displayName } from "../../lib/users.ts";
33 import type { SessionUser } from "../../middleware/session.ts";
4+import { Avatar } from "../Avatar.tsx";
45 import { Layout } from "../layout.tsx";
56 import { Pagination, type PaginationInfo } from "../Pagination.tsx";
67 import { RepoHeader } from "../repos/RepoHeader.tsx";
@@ -9,8 +10,8 @@ import { RepoNav } from "../repos/RepoNav.tsx";
910 interface IssueListProps {
1011 user: SessionUser | null;
1112 repo: RepositoryRow;
12- issues: (IssueRow & { author_username: string })[];
13- status: "open" | "closed";
13+ issues: (IssueRow & { author_username: string; author_avatar_version: number | null })[];
14+ status: "open" | "closed" | "completed";
1415 counts: Record<string, number>;
1516 pagination: PaginationInfo;
1617 }
@@ -39,6 +40,17 @@ export function IssueList({
3940 <span class="tab-count">{counts.open}</span>
4041 )}
4142 </a>
43+ <a
44+ href={`/${repo.name}/issues?status=completed`}
45+ class={`list-tab${status === "completed" ? " active" : ""}`}
46+ >
47+ Completed{" "}
48+ {(counts.completed ?? 0) > 0 && (
49+ <span class="tab-count">
50+ {counts.completed}
51+ </span>
52+ )}
53+ </a>
4254 <a
4355 href={`/${repo.name}/issues?status=closed`}
4456 class={`list-tab${status === "closed" ? " active" : ""}`}
@@ -81,6 +93,11 @@ export function IssueList({
8193 </span>
8294 </div>
8395 <div class="issue-meta">
96+ <Avatar
97+ userId={issue.author_id}
98+ version={issue.author_avatar_version}
99+ size={20}
100+ />
84101 <span class="issue-author">
85102 opened by{" "}
86103 {displayName(issue.author_username)}
Msrc/views/patches/PatchDetail.tsx
@@ -250,16 +250,18 @@ export function PatchDetail({
250250 <div
251251 class={`apply-result apply-${applyResult.status}`}
252252 >
253- <span class="apply-icon">
254- {applyResult.status === "clean"
255- ? "✓"
256- : "✗"}
257- </span>
258- <span>
259- {applyResult.status === "clean"
260- ? "Applies cleanly"
261- : "Has conflicts"}
262- </span>
253+ <div>
254+ <span class="apply-icon">
255+ {applyResult.status === "clean"
256+ ? "✓"
257+ : "✗"}
258+ </span>
259+ <span>
260+ {applyResult.status === "clean"
261+ ? "Applies cleanly"
262+ : "Has conflicts"}
263+ </span>
264+ </div>
263265 {applyResult.output && (
264266 <pre class="apply-output">
265267 {applyResult.output}
Msrc/views/patches/PatchList.tsx
@@ -1,6 +1,7 @@
11 import type { PatchRow, RepositoryRow } from "../../db/index.ts";
22 import { displayName } from "../../lib/users.ts";
33 import type { SessionUser } from "../../middleware/session.ts";
4+import { Avatar } from "../Avatar.tsx";
45 import { Layout } from "../layout.tsx";
56 import { Pagination, type PaginationInfo } from "../Pagination.tsx";
67 import { RepoHeader } from "../repos/RepoHeader.tsx";
@@ -9,7 +10,7 @@ import { RepoNav } from "../repos/RepoNav.tsx";
910 interface PatchListProps {
1011 user: SessionUser | null;
1112 repo: RepositoryRow;
12- patches: (PatchRow & { author_username: string })[];
13+ patches: (PatchRow & { author_username: string; author_avatar_version: number | null })[];
1314 status: string;
1415 counts: Record<string, number>;
1516 pagination: PaginationInfo;
@@ -90,6 +91,11 @@ export function PatchList({
9091 </span>
9192 </div>
9293 <div class="issue-meta">
94+ <Avatar
95+ userId={patch.author_id}
96+ version={patch.author_avatar_version}
97+ size={20}
98+ />
9399 <span>
94100 by {displayName(patch.author_username)}
95101 </span>
Msrc/views/releases/ReleaseDetail.tsx
@@ -126,17 +126,24 @@ export function ReleaseDetail({
126126 ["tar.gz", ".tar.gz"],
127127 ["tar.zst", ".tar.zst"],
128128 ] as const
129- ).map(([format, ext]) => (
130- <li class="asset-item asset-item-pending">
131- <span class="asset-name asset-name-pending">
132- {`${repo.name}-${release.commit_hash.slice(0, 8)}${ext}`}
133- </span>
134- <span class="asset-meta">
135- Source code ({format}) —
136- generating…
137- </span>
138- </li>
139- ))}
129+ )
130+ .filter(
131+ ([format]) =>
132+ !sourceArchives.some(
133+ (a) => a.format === format,
134+ ),
135+ )
136+ .map(([format, ext]) => (
137+ <li class="asset-item asset-item-pending">
138+ <span class="asset-name asset-name-pending">
139+ {`${repo.name}-${release.commit_hash.slice(0, 8)}${ext}`}
140+ </span>
141+ <span class="asset-meta">
142+ Source code ({format}) —
143+ generating…
144+ </span>
145+ </li>
146+ ))}
140147 </ul>
141148 </div>
142149 )}
Msrc/views/repos/RepoHome.tsx
@@ -1,4 +1,4 @@
1-import { BASE_URL, SSH_PORT } from "../../config.ts";
1+import { BASE_URL, SSH_DISABLED, SSH_PORT } from "../../config.ts";
22 import type { RepositoryRow } from "../../db/index.ts";
33 import type { SessionUser } from "../../middleware/session.ts";
44 import type { TreeEntry } from "../../services/git.ts";
@@ -71,15 +71,19 @@ git push origin main`}</code>
7171 class="clone-url-input"
7272 />
7373 </div>
74- <div class="clone-url-row">
75- <span class="clone-url-label">SSH</span>
76- <input
77- type="text"
78- readonly
79- value={sshUrl(repo.name)}
80- class="clone-url-input"
81- />
82- </div>
74+ {!SSH_DISABLED && (
75+ <div class="clone-url-row">
76+ <span class="clone-url-label">
77+ SSH
78+ </span>
79+ <input
80+ type="text"
81+ readonly
82+ value={sshUrl(repo.name)}
83+ class="clone-url-input"
84+ />
85+ </div>
86+ )}
8387 </div>
8488 </details>
8589 </div>
Msrc/views/repos/RepoList.tsx
@@ -44,14 +44,7 @@ export function RepoList({ user, repos, search, pagination }: RepoListProps) {
4444 No repositories match <strong>{search}</strong>.
4545 </p>
4646 ) : (
47- <>
48- <p>No repositories yet.</p>
49- {user?.isAdmin && (
50- <a href="/new" class="btn btn-primary">
51- Create your first repository
52- </a>
53- )}
54- </>
47+ <p>No repositories yet.</p>
5548 )}
5649 </div>
5750 ) : (