SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%

feat: design v2 — gradient design system, code-block kit (lang badge + copy), inline PDF viewer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 12 h ago (Aug 10, 2026) parent 94ad012

Showing 8 changed files with +479 and −170

modified src/render/markdown.mjs +13 −1
@@ -69,7 +69,19 @@ function buildParser(state) {
69 69 } else {
70 70 const lang = resolveLang(info);
71 71 const highlighted = highlightSync(token.content.replace(/\n$/, ''), lang);
72 html = `<div class="code-block"${info ? ` data-lang="${escapeHtml(info)}"` : ''}>${highlighted}</div>`;
72 + const label = escapeHtml(info || 'text');
73 + html = [
74 + `<div class="code-block"${info ? ` data-lang="${escapeHtml(info)}"` : ''}>`,
75 + '<div class="code-head">',
76 + `<span class="code-lang">${label}</span>`,
77 + '<button type="button" class="code-copy" data-code-copy aria-label="Copy code">',
78 + '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg>',
79 + '<span class="code-copy-label">Copy</span>',
80 + '</button>',
81 + '</div>',
82 + highlighted,
83 + '</div>',
84 + ].join('');
73 85 }
74 86 const index = state.fences.push(html) - 1;
75 87 return `<pre ${FENCE_PLACEHOLDER_ATTR}="${index}"></pre>`;
modified src/server.mjs +2 −1
@@ -40,7 +40,8 @@ const CSP = [
40 40 "style-src 'self' 'unsafe-inline'",
41 41 "font-src 'self'",
42 42 "connect-src 'self'",
43 "object-src 'none'",
43 + // 'self' (not 'none') so the blob view can embed same-origin PDFs.
44 + "object-src 'self'",
44 45 "base-uri 'self'",
45 46 "form-action 'self'",
46 47 "frame-ancestors 'none'",
modified src/web/assets/css/app.css +325 −151
@@ -5,18 +5,21 @@
5 5 * Author : Simon-Pierre Boucher
6 6 * Contact : contact@spboucher.ai
7 7 * File : src/web/assets/css/app.css
8 * Purpose : Application styles — layout, nav, cards, tables, diffs
8 + * Purpose : Application styles v2 — layout, depth, motion, every page
9 9 * License : MIT © Simon-Pierre Boucher
10 10 * ─────────────────────────────────────────────
11 11 */
12 12
13 13 *, *::before, *::after { box-sizing: border-box; }
14 14
15 html { -webkit-text-size-adjust: 100%; }
15 +html { -webkit-text-size-adjust: 100%; scroll-padding-top: 76px; }
16 16
17 17 body {
18 18 margin: 0;
19 background: var(--bg);
19 + background:
20 + radial-gradient(1100px 500px at 12% -10%, var(--bg-glow-1), transparent 60%),
21 + radial-gradient(900px 420px at 88% -6%, var(--bg-glow-2), transparent 55%),
22 + var(--bg);
20 23 color: var(--text);
21 24 font-family: var(--font-ui);
22 25 font-size: 15px;
@@ -24,15 +27,19 @@ body {
24 27 min-height: 100vh;
25 28 display: flex;
26 29 flex-direction: column;
30 + font-feature-settings: 'cv02', 'cv03', 'ss01';
27 31 }
28 32
29 a { color: var(--accent); text-decoration: none; }
30 a:hover { text-decoration: underline; }
33 +a { color: var(--accent); text-decoration: none; transition: color 0.15s ease; }
34 +a:hover { color: var(--accent-hover); text-decoration: underline; text-underline-offset: 3px; }
31 35 code, pre, kbd { font-family: var(--font-mono); }
32 36 button { font-family: inherit; }
37 +::selection { background: var(--accent-soft); }
38 +
39 +:focus-visible { outline: none; box-shadow: var(--ring); border-radius: 6px; }
33 40
34 41 .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; width: 100%; }
35 main.container { flex: 1; padding-top: 24px; padding-bottom: 48px; }
42 +main.container { flex: 1; padding-top: 28px; padding-bottom: 56px; }
36 43
37 44 .sr-only, .skip-link {
38 45 position: absolute; width: 1px; height: 1px; overflow: hidden;
@@ -48,30 +55,39 @@ main.container { flex: 1; padding-top: 24px; padding-bottom: 48px; }
48 55 /* ── Top nav ─────────────────────────────── */
49 56 .topnav {
50 57 position: sticky; top: 0; z-index: 50;
51 background: color-mix(in srgb, var(--bg) 88%, transparent);
52 backdrop-filter: blur(10px);
58 + background: var(--nav-bg);
59 + backdrop-filter: blur(14px) saturate(1.4);
60 + -webkit-backdrop-filter: blur(14px) saturate(1.4);
53 61 border-bottom: 1px solid var(--border);
54 62 }
55 .topnav-inner { display: flex; align-items: center; gap: 16px; height: 56px; }
56 .brand { display: flex; align-items: center; gap: 9px; color: var(--text); font-weight: 700; }
57 .brand:hover { text-decoration: none; }
58 .brand-mark { flex: none; }
59 .global-search { flex: 1; max-width: 420px; }
63 +.topnav-inner { display: flex; align-items: center; gap: 16px; height: 58px; }
64 +.brand {
65 + display: flex; align-items: center; gap: 10px;
66 + color: var(--text-bright); font-weight: 700; letter-spacing: -0.01em;
67 +}
68 +.brand:hover { text-decoration: none; color: var(--text-bright); }
69 +.brand-mark { flex: none; filter: drop-shadow(0 2px 8px var(--accent-glow)); transition: transform 0.2s ease; }
70 +.brand:hover .brand-mark { transform: rotate(-6deg) scale(1.06); }
71 +.global-search { flex: 1; max-width: 440px; position: relative; }
60 72 .global-search input, .repo-filters input, .search-page-form input {
61 73 width: 100%; background: var(--surface); color: var(--text);
62 74 border: 1px solid var(--border); border-radius: var(--radius);
63 padding: 7px 12px; font-size: 14px; outline: none;
75 + padding: 8px 13px; font-size: 14px; outline: none;
76 + transition: border-color 0.15s ease, box-shadow 0.15s ease;
64 77 }
65 78 .global-search input:focus, .repo-filters input:focus, .search-page-form input:focus {
66 border-color: var(--accent);
79 + border-color: var(--accent); box-shadow: var(--ring);
67 80 }
68 81 .topnav-links { margin-left: auto; display: flex; align-items: center; gap: 12px; }
69 .topnav-ext { color: var(--muted); font-size: 14px; }
82 +.topnav-ext { color: var(--muted); font-size: 14px; font-weight: 500; }
83 +.topnav-ext:hover { color: var(--text); text-decoration: none; }
70 84 .icon-btn {
71 85 background: var(--surface); color: var(--muted); border: 1px solid var(--border);
72 border-radius: var(--radius); padding: 7px 9px; cursor: pointer; line-height: 0;
86 + border-radius: var(--radius); padding: 8px 10px; cursor: pointer; line-height: 0;
87 + transition: border-color 0.15s ease, color 0.15s ease, transform 0.12s ease;
73 88 }
74 89 .icon-btn:hover { border-color: var(--border-strong); color: var(--text); }
90 +.icon-btn:active { transform: scale(0.94); }
75 91 html[data-theme='dark'] .icon-moon { display: none; }
76 92 html[data-theme='light'] .icon-sun { display: none; }
77 93
@@ -80,71 +96,116 @@ html[data-theme='light'] .icon-sun { display: none; }
80 96 display: inline-flex; align-items: center; gap: 6px;
81 97 background: var(--surface); color: var(--text);
82 98 border: 1px solid var(--border); border-radius: var(--radius);
83 padding: 6px 14px; font-size: 14px; cursor: pointer;
99 + padding: 6px 14px; font-size: 14px; font-weight: 500; cursor: pointer;
100 + box-shadow: var(--shadow-1);
101 + transition: border-color 0.15s ease, transform 0.12s ease, background 0.15s ease, box-shadow 0.15s ease;
102 +}
103 +.btn:hover {
104 + border-color: var(--border-strong); background: var(--surface-2);
105 + text-decoration: none; color: var(--text); transform: translateY(-1px);
106 +}
107 +.btn:active { transform: translateY(0) scale(0.98); }
108 +.btn-primary {
109 + background: var(--gradient-brand); border-color: transparent; color: #fff;
110 + box-shadow: 0 4px 16px -4px var(--accent-glow);
84 111 }
85 .btn:hover { border-color: var(--border-strong); text-decoration: none; }
86 .btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
87 .btn-primary:hover { filter: brightness(1.08); }
88 .btn-sm { padding: 4px 10px; font-size: 13px; }
112 +.btn-primary:hover { background: var(--gradient-brand); color: #fff; filter: brightness(1.08); }
113 +.btn-sm { padding: 4px 11px; font-size: 13px; }
89 114 .copy-btn.copied { border-color: var(--accent-2); color: var(--accent-2); }
90 115
91 116 .chip {
92 117 display: inline-flex; align-items: center;
93 border-radius: 999px; padding: 1px 10px; font-size: 12px;
118 + border-radius: 999px; padding: 2px 11px; font-size: 12px; font-weight: 500;
94 119 background: var(--chip-bg); color: var(--accent); border: 1px solid transparent;
120 + transition: border-color 0.15s ease, transform 0.12s ease;
95 121 }
96 .chip-topic:hover { border-color: var(--accent); text-decoration: none; }
122 +.chip-topic:hover { border-color: var(--accent); text-decoration: none; transform: translateY(-1px); }
97 123 .chip-public { color: var(--muted); background: transparent; border-color: var(--border-strong); }
98 .chip-license { color: var(--accent-2); background: rgba(34, 211, 170, 0.1); }
99 .chip-default { color: var(--accent-2); background: rgba(34, 211, 170, 0.1); }
100 .chip-pin { color: var(--accent-2); background: transparent; }
124 +.chip-license { color: var(--accent-2); background: var(--accent-2-soft); }
125 +.chip-default { color: var(--accent-2); background: var(--accent-2-soft); }
126 +.chip-pin { color: var(--warning); background: transparent; }
101 127 .chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
102 128 .count {
103 129 background: var(--surface-2); border-radius: 999px; padding: 0 8px;
104 font-size: 12px; color: var(--muted);
130 + font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums;
105 131 }
106 132
107 133 /* ── Hero ────────────────────────────────── */
108 134 .hero {
135 + position: relative;
109 136 display: flex; align-items: center; justify-content: space-between;
110 flex-wrap: wrap; gap: 20px; padding: 28px 0 20px;
111 border-bottom: 1px solid var(--border); margin-bottom: 28px;
137 + flex-wrap: wrap; gap: 22px; padding: 40px 0 30px;
138 + border-bottom: 1px solid var(--border); margin-bottom: 30px;
112 139 }
113 .hero-id { display: flex; align-items: center; gap: 18px; }
140 +.hero::after {
141 + content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
142 + background: var(--gradient-brand); opacity: 0.5;
143 + mask: linear-gradient(90deg, black, transparent 70%);
144 + -webkit-mask: linear-gradient(90deg, black, transparent 70%);
145 +}
146 +.hero-id { display: flex; align-items: center; gap: 20px; }
114 147 .hero-avatar {
115 width: 64px; height: 64px; border-radius: 18px; background: var(--accent);
116 color: #fff; font-weight: 700; font-size: 20px;
148 + width: 72px; height: 72px; border-radius: 20px;
149 + background: var(--gradient-brand);
150 + color: #fff; font-weight: 700; font-size: 22px; letter-spacing: 0.02em;
117 151 display: flex; align-items: center; justify-content: center; flex: none;
152 + box-shadow: 0 10px 30px -8px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.25);
153 +}
154 +.hero-name {
155 + margin: 0; font-size: 28px; font-weight: 700; letter-spacing: -0.02em;
156 + background: var(--gradient-text);
157 + -webkit-background-clip: text; background-clip: text;
158 + -webkit-text-fill-color: transparent; color: transparent;
118 159 }
119 .hero-name { margin: 0; font-size: 24px; }
120 .hero-tagline { margin: 2px 0 0; color: var(--muted); }
160 +.hero-tagline { margin: 4px 0 0; color: var(--muted); }
121 161 .hero-badges { display: flex; gap: 10px; flex-wrap: wrap; }
122 162 .badge-stat {
123 163 background: var(--surface); border: 1px solid var(--border);
124 border-radius: var(--radius); padding: 8px 14px; font-size: 14px; color: var(--muted);
164 + border-radius: var(--radius); padding: 10px 16px; font-size: 14px; color: var(--muted);
165 + box-shadow: var(--shadow-1); font-variant-numeric: tabular-nums;
125 166 }
126 .badge-stat strong { color: var(--text); margin-right: 4px; }
167 +.badge-stat strong { color: var(--text-bright); margin-right: 5px; font-size: 16px; }
127 168
128 .section-title { font-size: 17px; margin: 0 0 14px; display: flex; align-items: center; gap: 8px; }
169 +.section-title {
170 + font-size: 17px; font-weight: 600; letter-spacing: -0.01em;
171 + margin: 0 0 14px; display: flex; align-items: center; gap: 8px;
172 +}
129 173
130 174 /* ── Repo cards / home ───────────────────── */
131 175 .pinned-grid {
132 176 display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
133 gap: 14px; margin-bottom: 32px;
177 + gap: 14px; margin-bottom: 34px;
134 178 }
135 .home-columns { display: grid; grid-template-columns: 1fr 300px; gap: 32px; align-items: start; }
179 +.home-columns { display: grid; grid-template-columns: 1fr 300px; gap: 36px; align-items: start; }
136 180 .repo-list { display: flex; flex-direction: column; gap: 12px; }
137 181 .repo-card {
182 + position: relative;
138 183 background: var(--surface); border: 1px solid var(--border);
139 border-radius: var(--radius); padding: 16px 18px;
140 transition: border-color 0.15s ease;
184 + border-radius: var(--radius-lg); padding: 17px 19px;
185 + box-shadow: var(--shadow-1);
186 + transition: border-color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
187 + overflow: hidden;
141 188 }
142 .repo-card:hover { border-color: var(--border-strong); }
189 +.repo-card::before {
190 + content: ''; position: absolute; inset: 0 0 auto 0; height: 2px;
191 + background: var(--gradient-brand); opacity: 0;
192 + transition: opacity 0.18s ease;
193 +}
194 +.repo-card:hover {
195 + border-color: var(--border-strong); transform: translateY(-2px);
196 + box-shadow: var(--shadow-card);
197 +}
198 +.repo-card:hover::before { opacity: 1; }
143 199 .repo-card-name { margin: 0 0 4px; font-size: 16px; display: flex; align-items: center; gap: 8px; }
200 +.repo-card-name a { color: var(--text-bright); font-weight: 600; }
201 +.repo-card-name a:hover { color: var(--accent); text-decoration: none; }
144 202 .repo-card-desc { margin: 0 0 10px; color: var(--muted); font-size: 14px; }
145 203 .repo-card .chip-row { margin-bottom: 10px; }
146 204 .repo-card-meta { display: flex; gap: 16px; color: var(--muted); font-size: 13px; flex-wrap: wrap; }
147 .lang-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex: none; }
205 +.lang-dot {
206 + width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex: none;
207 + box-shadow: 0 0 6px 0 currentColor; opacity: 0.95;
208 +}
148 209 .lang-dot-label { display: inline-flex; align-items: center; gap: 6px; }
149 210
150 211 .repo-controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
@@ -152,26 +213,41 @@ html[data-theme='light'] .icon-sun { display: none; }
152 213 .repo-filters input { max-width: 200px; }
153 214 .repo-filters select, .ref-select {
154 215 background: var(--surface); color: var(--text); border: 1px solid var(--border);
155 border-radius: var(--radius); padding: 7px 10px; font-size: 14px;
216 + border-radius: var(--radius); padding: 8px 11px; font-size: 14px; cursor: pointer;
217 + transition: border-color 0.15s ease;
156 218 }
157 .empty-state { text-align: center; padding: 40px 0; color: var(--muted); }
219 +.repo-filters select:hover, .ref-select:hover { border-color: var(--border-strong); }
220 +.empty-state { text-align: center; padding: 44px 0; color: var(--muted); }
158 221
159 222 /* ── Activity feed ───────────────────────── */
160 .activity-feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; font-size: 14px; }
161 .activity-feed li { display: flex; gap: 10px; align-items: baseline; }
162 .activity-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent-2); flex: none; position: relative; top: -1px; }
223 +.activity-feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; font-size: 14px; }
224 +.activity-feed li {
225 + display: flex; gap: 12px; align-items: baseline; position: relative;
226 + padding: 7px 0 7px 4px;
227 +}
228 +.activity-feed li:not(:last-child)::before {
229 + content: ''; position: absolute; left: 7px; top: 22px; bottom: -8px;
230 + width: 2px; background: var(--border); border-radius: 1px;
231 +}
232 +.activity-dot {
233 + width: 8px; height: 8px; border-radius: 50%;
234 + background: var(--accent-2); flex: none; position: relative; top: -1px;
235 + box-shadow: 0 0 8px 0 var(--accent-2);
236 +}
163 237 .ref-inline { font-size: 12px; color: var(--muted); }
164 238
165 239 /* ── Heatmap ─────────────────────────────── */
166 .heatmap-section { margin-top: 40px; }
240 +.heatmap-section { margin-top: 44px; }
167 241 .heatmap-scroll { overflow-x: auto; padding-bottom: 6px; }
168 242 .heatmap-label { fill: var(--muted); font-size: 9px; font-family: var(--font-ui); }
243 +.heatmap-cell { transition: transform 0.1s ease; transform-origin: center; transform-box: fill-box; }
244 +.heatmap-cell:hover { transform: scale(1.35); stroke: var(--accent); stroke-width: 1; }
169 245 .heatmap-cell[data-level='0'] { fill: var(--heat-0); }
170 246 .heatmap-cell[data-level='1'] { fill: var(--heat-1); }
171 247 .heatmap-cell[data-level='2'] { fill: var(--heat-2); }
172 248 .heatmap-cell[data-level='3'] { fill: var(--heat-3); }
173 249 .heatmap-cell[data-level='4'] { fill: var(--heat-4); }
174 .heatmap-legend { display: flex; align-items: center; gap: 4px; color: var(--muted); font-size: 12px; margin-top: 6px; }
250 +.heatmap-legend { display: flex; align-items: center; gap: 4px; color: var(--muted); font-size: 12px; margin-top: 8px; }
175 251 .heatmap-cell-demo { width: 11px; height: 11px; border-radius: 2px; display: inline-block; }
176 252 .heatmap-cell-demo[data-level='0'] { background: var(--heat-0); }
177 253 .heatmap-cell-demo[data-level='1'] { background: var(--heat-1); }
@@ -180,169 +256,239 @@ html[data-theme='light'] .icon-sun { display: none; }
180 256 .heatmap-cell-demo[data-level='4'] { background: var(--heat-4); }
181 257
182 258 /* ── Repo header ─────────────────────────── */
183 .repo-header { padding-bottom: 0; margin-bottom: 20px; border-bottom: 1px solid var(--border); }
184 .repo-title { font-size: 20px; margin: 4px 0 6px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-weight: 500; }
259 +.repo-header { padding-bottom: 0; margin-bottom: 22px; border-bottom: 1px solid var(--border); }
260 +.repo-title {
261 + font-size: 21px; margin: 6px 0 6px; display: flex; align-items: center;
262 + gap: 8px; flex-wrap: wrap; font-weight: 500; letter-spacing: -0.01em;
263 +}
185 264 .repo-owner { color: var(--muted); }
186 .repo-sep { color: var(--muted); }
187 .repo-name { font-weight: 700; }
188 .repo-desc { margin: 0 0 8px; color: var(--muted); max-width: 780px; }
265 +.repo-owner:hover { color: var(--accent); text-decoration: none; }
266 +.repo-sep { color: var(--faint); }
267 +.repo-name { font-weight: 700; color: var(--text-bright); }
268 +.repo-name:hover { color: var(--accent); text-decoration: none; }
269 +.repo-desc { margin: 0 0 10px; color: var(--muted); max-width: 780px; font-size: 15px; }
189 270 .repo-meta-row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin-bottom: 10px; }
190 .repo-homepage { font-size: 14px; }
191 .lang-bar { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--surface-2); margin: 6px 0; }
192 .lang-bar-seg { display: block; height: 100%; min-width: 2px; }
271 +.repo-homepage { font-size: 14px; font-weight: 500; }
272 +.lang-bar {
273 + display: flex; height: 8px; border-radius: 4px; overflow: hidden;
274 + background: var(--surface-2); margin: 8px 0; gap: 1px;
275 +}
276 +.lang-bar-seg { display: block; height: 100%; min-width: 2px; transition: filter 0.15s ease; }
277 +.lang-bar-seg:hover { filter: brightness(1.25); }
193 278 .lang-legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 13px; margin-bottom: 12px; }
194 .repo-tabs { display: flex; gap: 4px; align-items: center; margin-top: 8px; overflow-x: auto; }
279 +.repo-tabs { display: flex; gap: 2px; align-items: center; margin-top: 10px; overflow-x: auto; }
195 280 .repo-tab {
196 color: var(--muted); padding: 8px 14px; border-radius: var(--radius) var(--radius) 0 0;
197 border-bottom: 2px solid transparent; display: inline-flex; gap: 7px; align-items: center;
198 white-space: nowrap; font-size: 14px;
199 }
200 .repo-tab:hover { color: var(--text); text-decoration: none; }
201 .repo-tab.active { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }
281 + color: var(--muted); padding: 9px 15px; position: relative;
282 + display: inline-flex; gap: 7px; align-items: center;
283 + white-space: nowrap; font-size: 14px; font-weight: 500;
284 + border-radius: var(--radius) var(--radius) 0 0;
285 + transition: color 0.15s ease, background 0.15s ease;
286 +}
287 +.repo-tab::after {
288 + content: ''; position: absolute; left: 10px; right: 10px; bottom: 0; height: 2px;
289 + background: var(--gradient-brand); border-radius: 2px 2px 0 0;
290 + opacity: 0; transform: scaleX(0.5);
291 + transition: opacity 0.18s ease, transform 0.18s ease;
292 +}
293 +.repo-tab:hover { color: var(--text); background: var(--surface); text-decoration: none; }
294 +.repo-tab.active { color: var(--text-bright); font-weight: 600; }
295 +.repo-tab.active::after { opacity: 1; transform: scaleX(1); }
202 296 .repo-tab-meta { margin-left: auto; font-size: 13px; white-space: nowrap; }
203 297
204 298 /* ── Repo toolbar / clone box ────────────── */
205 .repo-toolbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between; margin-bottom: 14px; }
206 .clone-box { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
299 +.repo-toolbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between; margin-bottom: 16px; }
300 +.clone-box {
301 + display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
302 + background: var(--surface); border: 1px solid var(--border);
303 + border-radius: var(--radius-lg); padding: 5px;
304 + box-shadow: var(--shadow-1);
305 +}
207 306 .clone-url {
208 background: var(--surface); border: 1px solid var(--border); color: var(--muted);
209 border-radius: var(--radius); padding: 6px 10px; font-family: var(--font-mono);
210 font-size: 13px; width: 300px; max-width: 60vw;
307 + background: transparent; border: none; color: var(--muted);
308 + padding: 6px 10px; font-family: var(--font-mono);
309 + font-size: 13px; width: 300px; max-width: 60vw; outline: none;
211 310 }
212 311 .clone-snippet {
213 312 background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--radius);
214 313 padding: 14px 16px; font-size: 13px; overflow-x: auto; display: inline-block; text-align: left;
215 314 }
216 315 .path-breadcrumb { font-family: var(--font-mono); font-size: 14px; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
217 .crumb-sep { color: var(--muted); }
316 +.crumb-sep { color: var(--faint); }
218 317
219 318 /* ── File table ──────────────────────────── */
220 319 .file-table {
221 320 width: 100%; border-collapse: collapse; border: 1px solid var(--border);
222 border-radius: var(--radius); overflow: hidden; background: var(--surface);
223 font-size: 14px; margin-bottom: 24px;
321 + border-radius: var(--radius-lg); overflow: hidden; background: var(--surface);
322 + font-size: 14px; margin-bottom: 26px; box-shadow: var(--shadow-1);
224 323 }
225 .file-table td { padding: 8px 14px; border-top: 1px solid var(--border); }
324 +.file-table td { padding: 9px 14px; border-top: 1px solid var(--border); }
226 325 .file-table tr:first-child td { border-top: 0; }
326 +.file-row { transition: background 0.12s ease; }
227 327 .file-row:hover { background: var(--surface-2); }
228 328 .file-name { white-space: nowrap; width: 32%; }
229 329 .file-name a { color: var(--text); }
230 .file-name a:hover { color: var(--accent); }
231 .file-icon { vertical-align: -3px; margin-right: 7px; color: var(--muted); }
330 +.file-name a:hover { color: var(--accent); text-decoration: none; }
331 +.file-icon { vertical-align: -3px; margin-right: 8px; color: var(--muted); }
232 332 .icon-dir { color: var(--accent); }
233 333 .file-commit { overflow: hidden; text-overflow: ellipsis; max-width: 400px; white-space: nowrap; }
234 334 .file-commit a:hover { color: var(--accent); }
235 .file-date { text-align: right; white-space: nowrap; font-size: 13px; }
335 +.file-date { text-align: right; white-space: nowrap; font-size: 13px; font-variant-numeric: tabular-nums; }
236 336
237 337 /* ── README / markdown section ───────────── */
238 338 .readme-section {
239 border: 1px solid var(--border); border-radius: var(--radius);
240 background: var(--surface); margin-bottom: 32px;
339 + border: 1px solid var(--border); border-radius: var(--radius-lg);
340 + background: var(--surface); margin-bottom: 32px; box-shadow: var(--shadow-1);
241 341 }
242 342 .readme-header {
243 display: flex; align-items: center; gap: 8px; padding: 10px 20px;
343 + display: flex; align-items: center; gap: 8px; padding: 11px 20px;
244 344 border-bottom: 1px solid var(--border); color: var(--muted); font-size: 13px;
245 position: sticky; top: 56px; background: var(--surface); border-radius: var(--radius) var(--radius) 0 0;
345 + position: sticky; top: 58px; background: var(--surface); z-index: 10;
346 + border-radius: var(--radius-lg) var(--radius-lg) 0 0;
246 347 }
247 .readme-section .markdown-body { padding: 24px 32px 32px; }
248 .empty-repo { text-align: center; padding: 48px 20px; }
348 +.readme-section .markdown-body { padding: 26px 34px 34px; }
349 +.empty-repo { text-align: center; padding: 52px 20px; }
249 350 .notice {
250 351 background: var(--accent-soft); border: 1px solid var(--accent);
251 352 border-radius: var(--radius); padding: 8px 14px; font-size: 13px; margin: 12px;
252 353 }
253 354
254 355 /* ── Blob view ───────────────────────────── */
255 .blob-card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
356 +.blob-card {
357 + border: 1px solid var(--border); border-radius: var(--radius-lg);
358 + background: var(--surface); overflow: hidden; box-shadow: var(--shadow-1);
359 +}
256 360 .blob-header {
257 361 display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
258 362 padding: 10px 16px; border-bottom: 1px solid var(--border);
259 position: sticky; top: 56px; background: var(--surface); z-index: 10;
363 + position: sticky; top: 58px; background: var(--surface); z-index: 10;
260 364 }
261 365 .blob-meta { display: flex; gap: 8px; align-items: center; font-size: 13px; }
262 366 .blob-actions { margin-left: auto; display: flex; gap: 6px; flex-wrap: wrap; }
263 .blob-image { text-align: center; padding: 32px; }
264 .blob-image img { max-width: 100%; border-radius: var(--radius); }
265 .blob-binary { text-align: center; padding: 48px 20px; color: var(--muted); }
266 .blob-markdown { padding: 24px 32px 32px; }
367 +.blob-image { text-align: center; padding: 32px; background: var(--surface-2); }
368 +.blob-image img { max-width: 100%; border-radius: var(--radius); box-shadow: var(--shadow-2); }
369 +.blob-binary { text-align: center; padding: 52px 20px; color: var(--muted); }
370 +.blob-markdown { padding: 26px 34px 34px; }
371 +
372 +/* PDF viewer */
373 +.pdf-viewer { background: var(--surface-2); }
374 +.pdf-frame {
375 + display: block; width: 100%; height: calc(100vh - 220px); min-height: 560px;
376 + border: 0; background: var(--surface-2);
377 +}
378 +.pdf-fallback { text-align: center; padding: 52px 20px; color: var(--muted); }
267 379
268 .code-view { overflow-x: auto; }
380 +.code-view { overflow-x: auto; background: var(--code-bg); }
269 381 .code-view pre.shiki {
270 margin: 0; padding: 12px 0; background: transparent !important;
271 font-size: 13px; line-height: 1.6; min-width: max-content;
382 + margin: 0; padding: 14px 0; background: transparent !important;
383 + font-size: 13px; line-height: 1.65; min-width: max-content;
384 +}
385 +.code-view .line { display: block; padding: 0 16px 0 0; min-height: 1.65em; transition: background 0.12s ease; }
386 +.code-view .line:hover { background: var(--surface-2); }
387 +.code-view .line:target { background: var(--accent-soft); animation: line-flash 1.2s ease 1; }
388 +.code-view .line.range-hl { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
389 +@keyframes line-flash {
390 + 0% { background: var(--accent-glow); }
391 + 100% { background: var(--accent-soft); }
272 392 }
273 .code-view .line { display: block; padding: 0 16px 0 0; min-height: 1.6em; }
274 .code-view .line:target { background: var(--accent-soft); }
275 .code-view .line.range-hl { background: var(--accent-soft); }
276 393 .code-view .ln {
277 display: inline-block; width: 52px; padding-right: 16px; text-align: right;
278 color: var(--muted); user-select: none; font-size: 12px; text-decoration: none;
279 position: sticky; left: 0; background: var(--surface);
394 + display: inline-block; width: 56px; padding-right: 18px; text-align: right;
395 + color: var(--faint); user-select: none; font-size: 12px; text-decoration: none;
396 + position: sticky; left: 0; background: var(--code-bg);
397 + font-variant-numeric: tabular-nums;
280 398 }
281 399 .code-view .ln:hover { color: var(--accent); }
282 400 .plain-readme { padding: 20px; overflow-x: auto; font-size: 13px; }
283 401
284 402 /* ── Commits ─────────────────────────────── */
285 .commit-list { list-style: none; margin: 0 0 20px; padding: 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
286 .commit-row { display: flex; gap: 12px; align-items: flex-start; padding: 12px 16px; border-top: 1px solid var(--border); }
403 +.commit-list {
404 + list-style: none; margin: 0 0 20px; padding: 0;
405 + border: 1px solid var(--border); border-radius: var(--radius-lg);
406 + background: var(--surface); box-shadow: var(--shadow-1); overflow: hidden;
407 +}
408 +.commit-row {
409 + display: flex; gap: 12px; align-items: flex-start; padding: 13px 16px;
410 + border-top: 1px solid var(--border); transition: background 0.12s ease;
411 +}
412 +.commit-row:hover { background: var(--surface-2); }
287 413 .commit-row:first-child { border-top: 0; }
288 .commit-avatar { flex: none; border-radius: 6px; overflow: hidden; background: var(--surface-2); line-height: 0; padding: 2px; border: 1px solid var(--border); }
414 +.commit-avatar {
415 + flex: none; border-radius: 8px; overflow: hidden; background: var(--surface-2);
416 + line-height: 0; padding: 3px; border: 1px solid var(--border);
417 +}
289 418 .commit-main { flex: 1; min-width: 0; }
290 419 .commit-subject { font-weight: 600; display: flex; gap: 8px; align-items: baseline; }
291 420 .commit-subject a { color: var(--text); }
292 .commit-subject a:hover { color: var(--accent); }
293 .commit-body-toggle summary { cursor: pointer; color: var(--muted); background: var(--surface-2); border-radius: 4px; padding: 0 8px; display: inline-block; }
421 +.commit-subject a:hover { color: var(--accent); text-decoration: none; }
422 +.commit-body-toggle summary {
423 + cursor: pointer; color: var(--muted); background: var(--surface-2);
424 + border-radius: 4px; padding: 0 8px; display: inline-block; list-style: none;
425 +}
426 +.commit-body-toggle summary::-webkit-details-marker { display: none; }
294 427 .commit-body { font-size: 13px; color: var(--muted); white-space: pre-wrap; margin: 8px 0 0; }
295 428 .commit-meta { font-size: 13px; margin-top: 3px; display: flex; flex-wrap: wrap; gap: 6px; }
296 429 .sha-chip {
297 430 font-family: var(--font-mono); font-size: 12px; background: var(--surface-2);
298 color: var(--accent); border: 1px solid var(--border); border-radius: 6px;
299 padding: 3px 8px; cursor: pointer; flex: none;
431 + color: var(--accent); border: 1px solid var(--border); border-radius: 7px;
432 + padding: 3px 9px; cursor: pointer; flex: none;
433 + transition: border-color 0.15s ease, transform 0.12s ease;
300 434 }
301 .sha-chip:hover { border-color: var(--accent); }
302 .pagination { display: flex; gap: 10px; justify-content: center; margin: 20px 0; }
435 +.sha-chip:hover { border-color: var(--accent); transform: translateY(-1px); }
436 +.pagination { display: flex; gap: 10px; justify-content: center; margin: 22px 0; }
303 437
304 438 /* ── Commit detail / diffs ───────────────── */
305 439 .commit-detail-header {
306 border: 1px solid var(--border); border-radius: var(--radius);
307 background: var(--surface); padding: 16px 20px; margin-bottom: 18px;
440 + border: 1px solid var(--border); border-radius: var(--radius-lg);
441 + background: var(--surface); padding: 18px 22px; margin-bottom: 18px;
442 + box-shadow: var(--shadow-1);
308 443 }
309 .commit-detail-subject { margin: 0 0 8px; font-size: 18px; }
444 +.commit-detail-subject { margin: 0 0 8px; font-size: 19px; letter-spacing: -0.01em; }
310 445 .commit-detail-meta { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; font-size: 14px; }
311 446 .commit-stats { margin: 12px 0 0; font-size: 14px; color: var(--muted); }
312 .diff-add { color: var(--add-text); font-weight: 600; }
313 .diff-del { color: var(--del-text); font-weight: 600; }
447 +.diff-add { color: var(--add-text); font-weight: 600; font-variant-numeric: tabular-nums; }
448 +.diff-del { color: var(--del-text); font-weight: 600; font-variant-numeric: tabular-nums; }
314 449
315 .diff-file { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); margin-bottom: 16px; overflow: hidden; }
450 +.diff-file {
451 + border: 1px solid var(--border); border-radius: var(--radius-lg);
452 + background: var(--surface); margin-bottom: 16px; overflow: hidden;
453 + box-shadow: var(--shadow-1);
454 +}
316 455 .diff-file-header {
317 display: flex; gap: 10px; align-items: center; padding: 9px 14px;
456 + display: flex; gap: 10px; align-items: center; padding: 10px 14px;
318 457 cursor: pointer; background: var(--surface-2); font-size: 13px; list-style: none;
319 position: sticky; top: 56px; z-index: 5;
458 + position: sticky; top: 58px; z-index: 5;
459 + transition: background 0.12s ease;
320 460 }
461 +.diff-file-header:hover { background: var(--surface-3); }
321 462 .diff-file-header::-webkit-details-marker { display: none; }
322 .diff-file-header::before { content: '▾'; color: var(--muted); }
463 +.diff-file-header::before { content: '▾'; color: var(--muted); transition: transform 0.15s ease; }
323 464 .diff-file:not([open]) .diff-file-header::before { content: '▸'; }
324 465 .diff-path { font-size: 13px; word-break: break-all; }
325 466 .diff-counts { margin-left: auto; white-space: nowrap; }
326 .diff-status { border-radius: 4px; padding: 1px 7px; font-size: 11px; text-transform: uppercase; font-weight: 700; }
467 +.diff-status {
468 + border-radius: 5px; padding: 1px 8px; font-size: 11px;
469 + text-transform: uppercase; font-weight: 700; letter-spacing: 0.04em;
470 +}
327 471 .diff-status-added { background: var(--add-bg); color: var(--add-text); }
328 472 .diff-status-deleted { background: var(--del-bg); color: var(--del-text); }
329 473 .diff-status-modified { background: var(--accent-soft); color: var(--accent); }
330 474 .diff-status-renamed { background: var(--chip-bg); color: var(--accent-2); }
331 475
332 .diff-table-wrap { overflow-x: auto; }
333 .diff-table { border-collapse: collapse; width: 100%; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55; }
476 +.diff-table-wrap { overflow-x: auto; background: var(--code-bg); }
477 +.diff-table { border-collapse: collapse; width: 100%; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.6; }
334 478 .diff-gutter {
335 479 width: 44px; min-width: 44px; text-align: right; padding: 0 8px;
336 color: var(--muted); user-select: none; border-right: 1px solid var(--border);
337 font-size: 11.5px; vertical-align: top;
480 + color: var(--faint); user-select: none; border-right: 1px solid var(--border);
481 + font-size: 11.5px; vertical-align: top; font-variant-numeric: tabular-nums;
338 482 }
339 483 .diff-code { padding: 0 10px; white-space: pre; }
340 484 .diff-sign { display: inline-block; width: 14px; user-select: none; color: var(--muted); }
341 485 .diff-line-add { background: var(--add-bg); }
486 +.diff-line-add .diff-gutter { background: var(--add-bg-strong); border-right-color: transparent; }
342 487 .diff-line-add .diff-sign { color: var(--add-text); }
343 488 .diff-line-del { background: var(--del-bg); }
489 +.diff-line-del .diff-gutter { background: var(--del-bg-strong); border-right-color: transparent; }
344 490 .diff-line-del .diff-sign { color: var(--del-text); }
345 .diff-hunk-header td { background: var(--hunk-bg); color: var(--muted); padding: 3px 10px; font-size: 12px; }
491 +.diff-hunk-header td { background: var(--hunk-bg); color: var(--muted); padding: 4px 10px; font-size: 12px; }
346 492 .diff-binary { padding: 16px; }
347 493
348 494 /* ── Blame ───────────────────────────────── */
@@ -354,8 +500,8 @@ html[data-theme='light'] .icon-sun { display: none; }
354 500 }
355 501 .blame-commit-inner { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
356 502 .blame-date { font-size: 12px; width: 100%; padding-left: 30px; }
357 .blame-ln { width: 44px; text-align: right; color: var(--muted); padding: 0 8px; user-select: none; font-family: var(--font-mono); font-size: 12px; }
358 .blame-code pre { margin: 0; font-size: 12.5px; line-height: 1.55; white-space: pre; }
503 +.blame-ln { width: 44px; text-align: right; color: var(--faint); padding: 0 8px; user-select: none; font-family: var(--font-mono); font-size: 12px; }
504 +.blame-code pre { margin: 0; font-size: 12.5px; line-height: 1.6; white-space: pre; }
359 505 .blame-line[data-age='0'] .blame-commit { box-shadow: inset 3px 0 0 var(--accent); }
360 506 .blame-line[data-age='1'] .blame-commit { box-shadow: inset 3px 0 0 color-mix(in srgb, var(--accent) 85%, transparent); }
361 507 .blame-line[data-age='2'] .blame-commit { box-shadow: inset 3px 0 0 color-mix(in srgb, var(--accent) 70%, transparent); }
@@ -370,25 +516,60 @@ html[data-theme='light'] .icon-sun { display: none; }
370 516 /* ── Branch/tag tables ───────────────────── */
371 517 .ref-table {
372 518 width: 100%; border-collapse: collapse; background: var(--surface);
373 border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
374 font-size: 14px;
519 + border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden;
520 + font-size: 14px; box-shadow: var(--shadow-1);
375 521 }
376 .ref-table th { text-align: left; padding: 10px 14px; font-size: 13px; color: var(--muted); border-bottom: 1px solid var(--border); }
377 .ref-table td { padding: 10px 14px; border-top: 1px solid var(--border); }
522 +.ref-table th { text-align: left; padding: 11px 14px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); border-bottom: 1px solid var(--border); }
523 +.ref-table td { padding: 11px 14px; border-top: 1px solid var(--border); }
524 +.ref-table tbody tr { transition: background 0.12s ease; }
525 +.ref-table tbody tr:hover { background: var(--surface-2); }
378 526 .ref-name { font-family: var(--font-mono); font-size: 13px; }
379 527 .ahead-behind { display: inline-flex; gap: 10px; font-family: var(--font-mono); font-size: 13px; }
380 528 .archive-links { display: flex; gap: 6px; }
381 529
530 +/* ── Releases ────────────────────────────── */
531 +.release-list { display: flex; flex-direction: column; gap: 18px; }
532 +.release-card {
533 + position: relative;
534 + background: var(--surface); border: 1px solid var(--border);
535 + border-radius: var(--radius-lg); padding: 18px 22px;
536 + box-shadow: var(--shadow-1);
537 + transition: border-color 0.18s ease, box-shadow 0.18s ease;
538 +}
539 +.release-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-card); }
540 +.release-header { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
541 +.release-tag { margin: 0; font-size: 17px; font-family: var(--font-mono); }
542 +.release-subject { color: var(--muted); }
543 +.release-assets { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
544 +.release-assets li {
545 + display: flex; align-items: center; gap: 10px;
546 + background: var(--surface-2); border: 1px solid var(--border);
547 + border-radius: var(--radius); padding: 9px 14px; font-size: 14px;
548 + transition: border-color 0.15s ease;
549 +}
550 +.release-assets li:hover { border-color: var(--accent); }
551 +.release-assets li svg { color: var(--accent-2); flex: none; }
552 +
382 553 /* ── Search page ─────────────────────────── */
383 554 .search-page-form { display: flex; gap: 8px; margin-bottom: 28px; max-width: 560px; }
384 .search-results { list-style: none; margin: 0 0 32px; padding: 0; display: flex; flex-direction: column; gap: 16px; }
385 .search-results li { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 18px; }
555 +.search-results { list-style: none; margin: 0 0 32px; padding: 0; display: flex; flex-direction: column; gap: 14px; }
556 +.search-results li {
557 + background: var(--surface); border: 1px solid var(--border);
558 + border-radius: var(--radius-lg); padding: 15px 19px; box-shadow: var(--shadow-1);
559 + transition: border-color 0.15s ease, transform 0.15s ease;
560 +}
561 +.search-results li:hover { border-color: var(--border-strong); transform: translateY(-1px); }
386 562 .search-hit-name { font-weight: 600; font-size: 15px; }
387 563 .search-snippet { color: var(--muted); font-size: 13px; margin: 6px 0 0; }
388 564
389 565 /* ── Error pages ─────────────────────────── */
390 .error-page { text-align: center; padding: 72px 20px; }
391 .error-code { font-size: 96px; font-weight: 700; color: var(--accent); font-family: var(--font-mono); line-height: 1; }
566 +.error-page { text-align: center; padding: 80px 20px; }
567 +.error-code {
568 + font-size: 104px; font-weight: 700; font-family: var(--font-mono); line-height: 1;
569 + background: var(--gradient-brand);
570 + -webkit-background-clip: text; background-clip: text;
571 + -webkit-text-fill-color: transparent; color: transparent;
572 +}
392 573 .error-message { font-size: 18px; color: var(--muted); margin: 16px 0 8px; }
393 574 .error-art { display: inline-block; text-align: left; color: var(--muted); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 18px; font-size: 13px; margin: 16px 0 24px; }
394 575
@@ -401,8 +582,17 @@ html[data-theme='light'] .icon-sun { display: none; }
401 582 /* ── Footer ──────────────────────────────── */
402 583 .footer {
403 584 border-top: 1px solid var(--border); color: var(--muted);
404 padding: 20px 0; font-size: 13px; text-align: center;
585 + padding: 22px 0; font-size: 13px; text-align: center;
586 +}
587 +
588 +/* ── Page entrance ───────────────────────── */
589 +@keyframes rise-in {
590 + from { opacity: 0; transform: translateY(6px); }
591 + to { opacity: 1; transform: none; }
405 592 }
593 +main.container > * { animation: rise-in 0.35s ease both; }
594 +main.container > *:nth-child(2) { animation-delay: 0.04s; }
595 +main.container > *:nth-child(3) { animation-delay: 0.08s; }
406 596
407 597 /* ── Responsive ──────────────────────────── */
408 598 @media (max-width: 960px) {
@@ -418,21 +608,5 @@ html[data-theme='light'] .icon-sun { display: none; }
418 608 .repo-toolbar { flex-direction: column; align-items: stretch; }
419 609 .file-date { display: none; }
420 610 .readme-section .markdown-body, .blob-markdown { padding: 16px; }
611 + .pdf-frame { height: 70vh; min-height: 420px; }
421 612 }
422
423 /* ── Releases ────────────────────────────── */
424 .release-list { display: flex; flex-direction: column; gap: 16px; }
425 .release-card {
426 background: var(--surface); border: 1px solid var(--border);
427 border-radius: var(--radius); padding: 16px 20px;
428 }
429 .release-header { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
430 .release-tag { margin: 0; font-size: 17px; font-family: var(--font-mono); }
431 .release-subject { color: var(--muted); }
432 .release-assets { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
433 .release-assets li {
434 display: flex; align-items: center; gap: 10px;
435 background: var(--surface-2); border: 1px solid var(--border);
436 border-radius: var(--radius); padding: 8px 14px; font-size: 14px;
437 }
438 .release-assets li svg { color: var(--accent-2); flex: none; }
modified src/web/assets/css/markdown.css +35 −2
@@ -84,16 +84,49 @@
84 84 background: var(--code-bg);
85 85 border: 1px solid var(--border);
86 86 border-radius: var(--radius);
87 overflow-x: auto;
88 87 margin-bottom: 16px;
89 88 position: relative;
89 + overflow: hidden;
90 +}
91 +.markdown-body .code-block .code-head {
92 + display: flex;
93 + align-items: center;
94 + justify-content: space-between;
95 + gap: 10px;
96 + padding: 6px 8px 6px 14px;
97 + background: var(--code-head);
98 + border-bottom: 1px solid var(--border);
99 + font-size: 12px;
100 +}
101 +.markdown-body .code-lang {
102 + font-family: var(--font-mono);
103 + color: var(--muted);
104 + text-transform: lowercase;
105 + letter-spacing: 0.03em;
106 +}
107 +.markdown-body .code-copy,
108 +.code-copy {
109 + display: inline-flex;
110 + align-items: center;
111 + gap: 6px;
112 + background: transparent;
113 + color: var(--muted);
114 + border: 1px solid transparent;
115 + border-radius: 7px;
116 + padding: 4px 9px;
117 + font-size: 12px;
118 + cursor: pointer;
119 + transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
90 120 }
121 +.code-copy:hover { color: var(--text); border-color: var(--border-strong); background: var(--surface); }
122 +.code-copy.copied { color: var(--accent-2); border-color: var(--accent-2); }
91 123 .markdown-body .code-block pre.shiki {
92 124 margin: 0;
93 125 padding: 14px 16px;
94 126 background: transparent !important;
95 127 font-size: 85%;
96 line-height: 1.55;
128 + line-height: 1.6;
129 + overflow-x: auto;
97 130 }
98 131 .markdown-body pre:not(.shiki) {
99 132 background: var(--code-bg);
modified src/web/assets/css/tokens.css +57 −12
@@ -5,7 +5,7 @@
5 5 * Author : Simon-Pierre Boucher
6 6 * Contact : contact@spboucher.ai
7 7 * File : src/web/assets/css/tokens.css
8 * Purpose : Design tokens — colors, radius, typography, themes
8 + * Purpose : Design tokens v2 — colors, gradients, depth, themes
9 9 * License : MIT © Simon-Pierre Boucher
10 10 * ─────────────────────────────────────────────
11 11 */
@@ -56,23 +56,37 @@
56 56 :root,
57 57 html[data-theme='dark'] {
58 58 --bg: #0b0e14;
59 + --bg-glow-1: rgba(79, 140, 255, 0.055);
60 + --bg-glow-2: rgba(34, 211, 170, 0.04);
59 61 --surface: #11151c;
60 62 --surface-2: #161b24;
63 + --surface-3: #1b2130;
61 64 --border: #1f2530;
62 65 --border-strong: #2a3242;
63 66 --text: #e6e9ef;
67 + --text-bright: #f4f6fa;
64 68 --muted: #8b93a3;
69 + --faint: #5c6474;
65 70 --accent: #4f8cff;
71 + --accent-hover: #6ea1ff;
66 72 --accent-soft: rgba(79, 140, 255, 0.14);
73 + --accent-glow: rgba(79, 140, 255, 0.35);
67 74 --accent-2: #22d3aa;
75 + --accent-2-soft: rgba(34, 211, 170, 0.12);
76 + --gradient-brand: linear-gradient(120deg, #4f8cff 0%, #22d3aa 100%);
77 + --gradient-text: linear-gradient(100deg, #e6e9ef 20%, #9db6e8 60%, #4f8cff 100%);
68 78 --danger: #ff5d5d;
69 79 --success: #3fb950;
70 --add-bg: rgba(63, 185, 80, 0.15);
80 + --warning: #d29922;
81 + --add-bg: rgba(63, 185, 80, 0.14);
82 + --add-bg-strong: rgba(63, 185, 80, 0.28);
71 83 --add-text: #56d364;
72 --del-bg: rgba(248, 81, 73, 0.15);
84 + --del-bg: rgba(248, 81, 73, 0.13);
85 + --del-bg-strong: rgba(248, 81, 73, 0.28);
73 86 --del-text: #ff7b72;
74 --hunk-bg: rgba(79, 140, 255, 0.08);
87 + --hunk-bg: rgba(79, 140, 255, 0.07);
75 88 --code-bg: #0d1117;
89 + --code-head: #131922;
76 90 --chip-bg: rgba(79, 140, 255, 0.12);
77 91 --heat-0: #161b24;
78 92 --heat-1: #0e4429;
@@ -80,37 +94,61 @@ html[data-theme='dark'] {
80 94 --heat-3: #26a641;
81 95 --heat-4: #39d353;
82 96 --radius: 10px;
97 + --radius-lg: 14px;
83 98 --font-ui: 'Inter', -apple-system, 'Segoe UI', sans-serif;
84 99 --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
85 --shadow-pop: 0 0 0 1px var(--border);
100 + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.35);
101 + --shadow-2: 0 6px 24px -8px rgba(0, 0, 0, 0.55);
102 + --shadow-card: 0 14px 40px -18px rgba(0, 0, 0, 0.6);
103 + --ring: 0 0 0 3px rgba(79, 140, 255, 0.32);
104 + --nav-bg: rgba(11, 14, 20, 0.82);
86 105 color-scheme: dark;
87 106 }
88 107
89 108 html[data-theme='light'] {
90 --bg: #f7f8fa;
109 + --bg: #f6f8fb;
110 + --bg-glow-1: rgba(47, 111, 224, 0.05);
111 + --bg-glow-2: rgba(13, 158, 128, 0.04);
91 112 --surface: #ffffff;
92 --surface-2: #f0f2f5;
93 --border: #d9dee6;
94 --border-strong: #c2cad6;
113 + --surface-2: #f1f4f8;
114 + --surface-3: #e9edf4;
115 + --border: #dbe1ea;
116 + --border-strong: #c3ccd9;
95 117 --text: #1c2330;
118 + --text-bright: #0d1320;
96 119 --muted: #57606e;
120 + --faint: #8a93a3;
97 121 --accent: #2f6fe0;
122 + --accent-hover: #1e5ecf;
98 123 --accent-soft: rgba(47, 111, 224, 0.1);
124 + --accent-glow: rgba(47, 111, 224, 0.25);
99 125 --accent-2: #0d9e80;
126 + --accent-2-soft: rgba(13, 158, 128, 0.1);
127 + --gradient-brand: linear-gradient(120deg, #2f6fe0 0%, #0d9e80 100%);
128 + --gradient-text: linear-gradient(100deg, #1c2330 20%, #29456f 60%, #2f6fe0 100%);
100 129 --danger: #d33636;
101 130 --success: #1a7f37;
102 --add-bg: rgba(26, 127, 55, 0.12);
131 + --warning: #9a6700;
132 + --add-bg: rgba(26, 127, 55, 0.1);
133 + --add-bg-strong: rgba(26, 127, 55, 0.22);
103 134 --add-text: #1a7f37;
104 --del-bg: rgba(207, 34, 46, 0.12);
135 + --del-bg: rgba(207, 34, 46, 0.09);
136 + --del-bg-strong: rgba(207, 34, 46, 0.2);
105 137 --del-text: #cf222e;
106 --hunk-bg: rgba(47, 111, 224, 0.07);
138 + --hunk-bg: rgba(47, 111, 224, 0.06);
107 139 --code-bg: #f6f8fa;
140 + --code-head: #eef1f5;
108 141 --chip-bg: rgba(47, 111, 224, 0.1);
109 142 --heat-0: #ebedf0;
110 143 --heat-1: #9be9a8;
111 144 --heat-2: #40c463;
112 145 --heat-3: #30a14e;
113 146 --heat-4: #216e39;
147 + --shadow-1: 0 1px 2px rgba(16, 24, 40, 0.06);
148 + --shadow-2: 0 6px 24px -10px rgba(16, 24, 40, 0.16);
149 + --shadow-card: 0 14px 40px -20px rgba(16, 24, 40, 0.22);
150 + --ring: 0 0 0 3px rgba(47, 111, 224, 0.25);
151 + --nav-bg: rgba(246, 248, 251, 0.85);
114 152 color-scheme: light;
115 153 }
116 154
@@ -125,3 +163,10 @@ html[data-theme='light'] .shiki span {
125 163 color: var(--shiki-light) !important;
126 164 background-color: transparent !important;
127 165 }
166 +
167 +@media (prefers-reduced-motion: reduce) {
168 + *, *::before, *::after {
169 + animation-duration: 0.01ms !important;
170 + transition-duration: 0.01ms !important;
171 + }
172 +}
modified src/web/assets/js/app.js +23 −0
@@ -54,6 +54,29 @@
54 54 });
55 55 });
56 56
57 + /* ── Copy code blocks (fences + blob view) ── */
58 + document.addEventListener('click', function (event) {
59 + var btn = event.target.closest('[data-code-copy]');
60 + if (!btn) return;
61 + var scope = btn.getAttribute('data-code-target');
62 + var container = scope ? document.querySelector(scope) : btn.closest('.code-block');
63 + var pre = container ? container.querySelector('pre') : null;
64 + if (!pre) return;
65 + // Strip line-number anchors from the copied text.
66 + var clone = pre.cloneNode(true);
67 + clone.querySelectorAll('a.ln').forEach(function (a) { a.remove(); });
68 + navigator.clipboard.writeText(clone.textContent).then(function () {
69 + btn.classList.add('copied');
70 + var label = btn.querySelector('.code-copy-label');
71 + var prev = label ? label.textContent : null;
72 + if (label) label.textContent = 'Copied!';
73 + setTimeout(function () {
74 + btn.classList.remove('copied');
75 + if (label && prev) label.textContent = prev;
76 + }, 1400);
77 + });
78 + });
79 +
57 80 /* ── Home: filter + sort repo list ────────── */
58 81 var list = document.getElementById('repo-list');
59 82 if (list) {
modified src/web/routes.mjs +9 −2
@@ -344,11 +344,16 @@ export async function registerWeb(app, ctx) {
344 344 // Stored-XSS guard: never serve repo HTML as HTML.
345 345 if (['.html', '.htm', '.xhtml'].includes(ext)) mime = 'text/plain; charset=utf-8';
346 346 const immutable = /^[0-9a-f]{40}$/.test(resolved.ref) || /^[0-9a-f]{7,40}$/.test(resolved.ref);
347 + // PDFs must not be CSP-sandboxed or the built-in browser viewer refuses to render.
348 + const csp = ext === '.pdf'
349 + ? "default-src 'none'; object-src 'self'; style-src 'unsafe-inline'"
350 + : "default-src 'none'; style-src 'unsafe-inline'; sandbox";
347 351 reply
348 352 .type(mime)
349 353 .header('X-Content-Type-Options', 'nosniff')
350 .header('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; sandbox")
354 + .header('Content-Security-Policy', csp)
351 355 .header('Cache-Control', immutable ? 'public, max-age=31536000, immutable' : 'public, max-age=60');
356 + if (ext === '.pdf') reply.header('Content-Disposition', 'inline');
352 357 return reply.send(blob.content);
353 358 });
354 359
@@ -472,7 +477,9 @@ export async function registerWeb(app, ctx) {
472 477 notebookNote: false,
473 478 };
474 479
475 if (blob.binary || IMAGE_EXT.has(ext)) {
480 + if (ext === '.pdf') {
481 + view.kind = 'pdf';
482 + } else if (blob.binary || IMAGE_EXT.has(ext)) {
476 483 view.kind = 'binary';
477 484 view.isImage = IMAGE_EXT.has(ext);
478 485 } else if (blob.size > BLOB_RENDER_LIMIT) {
modified src/web/views/blob.njk +15 −1
@@ -33,6 +33,11 @@
33 33 <a class="btn btn-sm" href="/{{ overview.name }}/blob/{{ currentRef }}/{{ path }}" rel="nofollow">Rendered</a>
34 34 {% endif %}
35 35 {% endif %}
36 + {% if view.kind == 'code' %}
37 + <button class="btn btn-sm code-copy" type="button" data-code-copy data-code-target="#code-view" title="Copy file contents">
38 + <span class="code-copy-label">Copy file</span>
39 + </button>
40 + {% endif %}
36 41 <a class="btn btn-sm" href="{{ rawUrl }}" rel="nofollow">Raw</a>
37 42 <a class="btn btn-sm" href="/{{ overview.name }}/blame/{{ currentRef }}/{{ path }}" rel="nofollow">Blame</a>
38 43 <a class="btn btn-sm" href="/{{ overview.name }}/commits/{{ currentRef }}?path={{ path | urlencode }}" rel="nofollow">History</a>
@@ -46,7 +51,16 @@
46 51 <p class="notice">Jupyter notebook shown as formatted JSON. Download the <a href="{{ rawUrl }}">raw file</a> to open it in Jupyter.</p>
47 52 {% endif %}
48 53
49 {% if view.kind == 'binary' %}
54 + {% if view.kind == 'pdf' %}
55 + <div class="pdf-viewer">
56 + <object class="pdf-frame" data="{{ rawUrl }}" type="application/pdf" aria-label="PDF viewer — {{ fileName }}">
57 + <div class="pdf-fallback">
58 + <p>Your browser cannot display PDFs inline.</p>
59 + <a class="btn btn-primary" href="{{ rawUrl }}" download>Download {{ fileName }}</a>
60 + </div>
61 + </object>
62 + </div>
63 + {% elif view.kind == 'binary' %}
50 64 {% if view.isImage %}
51 65 <div class="blob-image"><img src="{{ rawUrl }}" alt="{{ fileName }}"></div>
52 66 {% else %}
53 67