SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 71.5% CSS 14.1% Nunjucks 11.8% Shell 2.6%

feat: design v3 'Nightforge' — aurora background, ⌘K palette, spotlight cards

- tokens.css v3: electric indigo→cyan→mint palette + violet, aurora glow
  tokens, surface gradients, glow shadows, dark & light themes
- app.css v3: animated aurora + film grain canvas, mouse-tracking spotlight
  on repo cards, conic-ring hero avatar, animated gradient hero name,
  gradient stat tiles, glowing heatmap, sheen primary buttons, custom
  scrollbars, gradient hairlines (nav/hero/footer), richer 3-part footer
- ⌘K command palette: fuzzy repo jump over /api/v1/repos, full keyboard
  navigation, glass backdrop (new cmdk component in layout + app.js)
- markdown.css: gradient heading underlines, gradient-border blockquotes,
  bordered inline code, faded hr

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

Showing 5 changed files with +615 and −197

modified src/web/assets/css/app.css +292 −108
@@ -5,21 +5,18 @@
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 v2 — layout, depth, motion, every page
8 + * Purpose : Application styles v3 "Nightforge" — aurora, spotlight, ⌘K, 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%; scroll-padding-top: 76px; }
15 +html { -webkit-text-size-adjust: 100%; scroll-padding-top: 80px; }
16 16
17 17 body {
18 18 margin: 0;
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);
19 + background: var(--bg);
23 20 color: var(--text);
24 21 font-family: var(--font-ui);
25 22 font-size: 15px;
@@ -28,8 +25,35 @@ body {
28 25 display: flex;
29 26 flex-direction: column;
30 27 font-feature-settings: 'cv02', 'cv03', 'ss01';
28 + position: relative;
31 29 }
32 30
31 +/* ── Aurora canvas ───────────────────────── */
32 +.aurora {
33 + position: fixed; inset: 0; z-index: -2; overflow: hidden;
34 + pointer-events: none;
35 +}
36 +.aurora::before {
37 + content: ''; position: absolute; inset: -20%;
38 + background:
39 + radial-gradient(42% 34% at 18% 8%, var(--bg-glow-1), transparent 70%),
40 + radial-gradient(36% 30% at 82% 4%, var(--bg-glow-2), transparent 70%),
41 + radial-gradient(30% 26% at 62% 18%, var(--bg-glow-3), transparent 72%),
42 + radial-gradient(34% 30% at 38% 2%, var(--bg-glow-4), transparent 70%);
43 + animation: aurora-drift 26s ease-in-out infinite alternate;
44 +}
45 +@keyframes aurora-drift {
46 + from { transform: translate3d(-2.5%, -1.5%, 0) scale(1); }
47 + 50% { transform: translate3d(1.5%, 1%, 0) scale(1.06); }
48 + to { transform: translate3d(3%, -1%, 0) scale(1.02); }
49 +}
50 +/* film grain — keeps the gradients from banding, adds texture */
51 +.aurora::after {
52 + content: ''; position: absolute; inset: 0; opacity: 0.05;
53 + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
54 +}
55 +html[data-theme='light'] .aurora::after { opacity: 0.035; }
56 +
33 57 a { color: var(--accent); text-decoration: none; transition: color 0.15s ease; }
34 58 a:hover { color: var(--accent-hover); text-decoration: underline; text-underline-offset: 3px; }
35 59 code, pre, kbd { font-family: var(--font-mono); }
@@ -38,8 +62,15 @@ button { font-family: inherit; }
38 62
39 63 :focus-visible { outline: none; box-shadow: var(--ring); border-radius: 6px; }
40 64
65 +/* custom scrollbars */
66 +* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
67 +*::-webkit-scrollbar { width: 10px; height: 10px; }
68 +*::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
69 +*::-webkit-scrollbar-thumb:hover { background: var(--faint); border: 2px solid transparent; background-clip: padding-box; }
70 +*::-webkit-scrollbar-track { background: transparent; }
71 +
41 72 .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; width: 100%; }
42 main.container { flex: 1; padding-top: 28px; padding-bottom: 56px; }
73 +main.container { flex: 1; padding-top: 30px; padding-bottom: 60px; }
43 74
44 75 .sr-only, .skip-link {
45 76 position: absolute; width: 1px; height: 1px; overflow: hidden;
@@ -52,22 +83,37 @@ main.container { flex: 1; padding-top: 28px; padding-bottom: 56px; }
52 83 }
53 84 .muted { color: var(--muted); }
54 85
86 +/* ── Card primitive (shared look) ────────── */
87 +.repo-card, .release-card, .search-results li, .commit-list, .file-table,
88 +.readme-section, .blob-card, .ref-table, .commit-detail-header, .diff-file,
89 +.badge-stat, .clone-box, .error-art {
90 + background: var(--surface-grad);
91 + border: 1px solid var(--border);
92 + box-shadow: inset 0 1px 0 var(--edge-light), var(--shadow-1);
93 +}
94 +
55 95 /* ── Top nav ─────────────────────────────── */
56 96 .topnav {
57 97 position: sticky; top: 0; z-index: 50;
58 98 background: var(--nav-bg);
59 backdrop-filter: blur(14px) saturate(1.4);
60 -webkit-backdrop-filter: blur(14px) saturate(1.4);
99 + backdrop-filter: blur(18px) saturate(1.6);
100 + -webkit-backdrop-filter: blur(18px) saturate(1.6);
61 101 border-bottom: 1px solid var(--border);
62 102 }
63 .topnav-inner { display: flex; align-items: center; gap: 16px; height: 58px; }
103 +.topnav::after {
104 + content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
105 + background: var(--gradient-hot); opacity: 0.35;
106 + mask: linear-gradient(90deg, transparent, black 20%, black 80%, transparent);
107 + -webkit-mask: linear-gradient(90deg, transparent, black 20%, black 80%, transparent);
108 +}
109 +.topnav-inner { display: flex; align-items: center; gap: 16px; height: 60px; }
64 110 .brand {
65 111 display: flex; align-items: center; gap: 10px;
66 112 color: var(--text-bright); font-weight: 700; letter-spacing: -0.01em;
67 113 }
68 114 .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); }
115 +.brand-mark { flex: none; filter: drop-shadow(0 2px 10px var(--accent-glow)); transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); }
116 +.brand:hover .brand-mark { transform: rotate(-8deg) scale(1.1); }
71 117 .global-search { flex: 1; max-width: 440px; position: relative; }
72 118 .global-search input, .repo-filters input, .search-page-form input {
73 119 width: 100%; background: var(--surface); color: var(--text);
@@ -75,19 +121,29 @@ main.container { flex: 1; padding-top: 28px; padding-bottom: 56px; }
75 121 padding: 8px 13px; font-size: 14px; outline: none;
76 122 transition: border-color 0.15s ease, box-shadow 0.15s ease;
77 123 }
124 +.global-search input { padding-right: 74px; }
78 125 .global-search input:focus, .repo-filters input:focus, .search-page-form input:focus {
79 126 border-color: var(--accent); box-shadow: var(--ring);
80 127 }
128 +.kbd-hint {
129 + position: absolute; right: 7px; top: 50%; transform: translateY(-50%);
130 + display: inline-flex; align-items: center; gap: 3px;
131 + background: var(--surface-2); border: 1px solid var(--border);
132 + border-radius: 7px; padding: 2px 7px; cursor: pointer;
133 + color: var(--muted); font-family: var(--font-mono); font-size: 11px;
134 + transition: border-color 0.15s ease, color 0.15s ease;
135 +}
136 +.kbd-hint:hover { border-color: var(--accent); color: var(--accent); }
81 137 .topnav-links { margin-left: auto; display: flex; align-items: center; gap: 12px; }
82 138 .topnav-ext { color: var(--muted); font-size: 14px; font-weight: 500; }
83 139 .topnav-ext:hover { color: var(--text); text-decoration: none; }
84 140 .icon-btn {
85 141 background: var(--surface); color: var(--muted); border: 1px solid var(--border);
86 142 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;
143 + transition: border-color 0.15s ease, color 0.15s ease, transform 0.12s ease, box-shadow 0.15s ease;
88 144 }
89 .icon-btn:hover { border-color: var(--border-strong); color: var(--text); }
90 .icon-btn:active { transform: scale(0.94); }
145 +.icon-btn:hover { border-color: var(--border-strong); color: var(--text); box-shadow: 0 0 16px -6px var(--accent-glow); }
146 +.icon-btn:active { transform: scale(0.92) rotate(-12deg); }
91 147 html[data-theme='dark'] .icon-moon { display: none; }
92 148 html[data-theme='light'] .icon-sun { display: none; }
93 149
@@ -97,29 +153,38 @@ html[data-theme='light'] .icon-sun { display: none; }
97 153 background: var(--surface); color: var(--text);
98 154 border: 1px solid var(--border); border-radius: var(--radius);
99 155 padding: 6px 14px; font-size: 14px; font-weight: 500; cursor: pointer;
100 box-shadow: var(--shadow-1);
156 + box-shadow: inset 0 1px 0 var(--edge-light), var(--shadow-1);
101 157 transition: border-color 0.15s ease, transform 0.12s ease, background 0.15s ease, box-shadow 0.15s ease;
102 158 }
103 159 .btn:hover {
104 160 border-color: var(--border-strong); background: var(--surface-2);
105 161 text-decoration: none; color: var(--text); transform: translateY(-1px);
162 + box-shadow: inset 0 1px 0 var(--edge-light), 0 6px 20px -8px var(--accent-glow);
106 163 }
107 164 .btn:active { transform: translateY(0) scale(0.98); }
108 165 .btn-primary {
166 + position: relative; overflow: hidden;
109 167 background: var(--gradient-brand); border-color: transparent; color: #fff;
110 box-shadow: 0 4px 16px -4px var(--accent-glow);
168 + box-shadow: 0 4px 18px -4px var(--accent-glow);
169 +}
170 +.btn-primary::before {
171 + content: ''; position: absolute; inset: 0;
172 + background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, 0.35) 50%, transparent 70%);
173 + transform: translateX(-120%);
174 + transition: transform 0.55s ease;
111 175 }
112 .btn-primary:hover { background: var(--gradient-brand); color: #fff; filter: brightness(1.08); }
176 +.btn-primary:hover { background: var(--gradient-brand); color: #fff; filter: brightness(1.06); }
177 +.btn-primary:hover::before { transform: translateX(120%); }
113 178 .btn-sm { padding: 4px 11px; font-size: 13px; }
114 .copy-btn.copied { border-color: var(--accent-2); color: var(--accent-2); }
179 +.copy-btn.copied { border-color: var(--accent-2); color: var(--accent-2); box-shadow: 0 0 16px -6px var(--accent-2); }
115 180
116 181 .chip {
117 182 display: inline-flex; align-items: center;
118 183 border-radius: 999px; padding: 2px 11px; font-size: 12px; font-weight: 500;
119 184 background: var(--chip-bg); color: var(--accent); border: 1px solid transparent;
120 transition: border-color 0.15s ease, transform 0.12s ease;
185 + transition: border-color 0.15s ease, transform 0.12s ease, box-shadow 0.15s ease;
121 186 }
122 .chip-topic:hover { border-color: var(--accent); text-decoration: none; transform: translateY(-1px); }
187 +.chip-topic:hover { border-color: var(--accent); text-decoration: none; transform: translateY(-1px); box-shadow: 0 4px 14px -6px var(--accent-glow); }
123 188 .chip-public { color: var(--muted); background: transparent; border-color: var(--border-strong); }
124 189 .chip-license { color: var(--accent-2); background: var(--accent-2-soft); }
125 190 .chip-default { color: var(--accent-2); background: var(--accent-2-soft); }
@@ -134,77 +199,111 @@ html[data-theme='light'] .icon-sun { display: none; }
134 199 .hero {
135 200 position: relative;
136 201 display: flex; align-items: center; justify-content: space-between;
137 flex-wrap: wrap; gap: 22px; padding: 40px 0 30px;
138 border-bottom: 1px solid var(--border); margin-bottom: 30px;
202 + flex-wrap: wrap; gap: 24px; padding: 52px 0 38px;
203 + border-bottom: 1px solid var(--border); margin-bottom: 32px;
139 204 }
140 205 .hero::after {
141 206 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%);
207 + background: var(--gradient-hot); opacity: 0.65;
208 + mask: linear-gradient(90deg, black, transparent 75%);
209 + -webkit-mask: linear-gradient(90deg, black, transparent 75%);
145 210 }
146 .hero-id { display: flex; align-items: center; gap: 20px; }
211 +.hero-id { display: flex; align-items: center; gap: 22px; }
147 212 .hero-avatar {
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;
213 + position: relative;
214 + width: 84px; height: 84px; border-radius: 24px;
215 + background: var(--gradient-hot); background-size: 200% 200%;
216 + animation: gradient-pan 9s ease infinite;
217 + color: #fff; font-weight: 700; font-size: 25px; letter-spacing: 0.02em;
151 218 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);
219 + box-shadow: 0 14px 44px -10px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.3);
220 +}
221 +.hero-avatar::before {
222 + content: ''; position: absolute; inset: -5px; border-radius: 28px; z-index: -1;
223 + background: conic-gradient(from var(--ring-angle, 0deg), var(--accent), var(--accent-3), var(--accent-2), var(--accent));
224 + opacity: 0.55; filter: blur(6px);
225 + animation: ring-spin 7s linear infinite;
226 +}
227 +@property --ring-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
228 +@keyframes ring-spin { to { --ring-angle: 360deg; } }
229 +@keyframes gradient-pan {
230 + 0%, 100% { background-position: 0% 50%; }
231 + 50% { background-position: 100% 50%; }
153 232 }
154 233 .hero-name {
155 margin: 0; font-size: 28px; font-weight: 700; letter-spacing: -0.02em;
156 background: var(--gradient-text);
234 + margin: 0; font-size: 34px; font-weight: 700; letter-spacing: -0.025em; line-height: 1.15;
235 + background: var(--gradient-text); background-size: 180% 100%;
157 236 -webkit-background-clip: text; background-clip: text;
158 237 -webkit-text-fill-color: transparent; color: transparent;
238 + animation: gradient-pan 8s ease infinite;
159 239 }
160 .hero-tagline { margin: 4px 0 0; color: var(--muted); }
240 +.hero-tagline { margin: 6px 0 0; color: var(--muted); font-size: 15px; }
161 241 .hero-badges { display: flex; gap: 10px; flex-wrap: wrap; }
162 242 .badge-stat {
163 background: var(--surface); border: 1px solid var(--border);
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;
243 + border-radius: var(--radius);
244 + padding: 12px 18px; font-size: 13px; color: var(--muted);
245 + font-variant-numeric: tabular-nums;
246 + display: flex; flex-direction: column; gap: 1px; min-width: 96px;
247 + transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
248 +}
249 +.badge-stat:hover { transform: translateY(-2px); border-color: var(--border-strong); box-shadow: inset 0 1px 0 var(--edge-light), var(--shadow-card); }
250 +.badge-stat strong {
251 + font-size: 22px; font-weight: 700; letter-spacing: -0.02em;
252 + background: var(--gradient-brand);
253 + -webkit-background-clip: text; background-clip: text;
254 + -webkit-text-fill-color: transparent; color: transparent;
166 255 }
167 .badge-stat strong { color: var(--text-bright); margin-right: 5px; font-size: 16px; }
168 256
169 257 .section-title {
170 258 font-size: 17px; font-weight: 600; letter-spacing: -0.01em;
171 margin: 0 0 14px; display: flex; align-items: center; gap: 8px;
259 + margin: 0 0 14px; display: flex; align-items: center; gap: 10px;
260 +}
261 +.section-title::before {
262 + content: ''; width: 4px; height: 17px; border-radius: 3px; flex: none;
263 + background: var(--gradient-brand); box-shadow: 0 0 12px -2px var(--accent-glow);
172 264 }
173 265
174 266 /* ── Repo cards / home ───────────────────── */
175 267 .pinned-grid {
176 268 display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
177 gap: 14px; margin-bottom: 34px;
269 + gap: 14px; margin-bottom: 36px;
178 270 }
179 271 .home-columns { display: grid; grid-template-columns: 1fr 300px; gap: 36px; align-items: start; }
180 272 .repo-list { display: flex; flex-direction: column; gap: 12px; }
181 273 .repo-card {
182 274 position: relative;
183 background: var(--surface); border: 1px solid var(--border);
184 border-radius: var(--radius-lg); padding: 17px 19px;
185 box-shadow: var(--shadow-1);
275 + border-radius: var(--radius-lg); padding: 18px 20px;
186 276 transition: border-color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
187 277 overflow: hidden;
188 278 }
279 +/* mouse spotlight — --mx/--my set by app.js */
280 +.repo-card::after {
281 + content: ''; position: absolute; inset: 0; pointer-events: none;
282 + background: radial-gradient(340px circle at var(--mx, 50%) var(--my, -40%), var(--accent-soft), transparent 65%);
283 + opacity: 0; transition: opacity 0.25s ease;
284 +}
285 +.repo-card:hover::after { opacity: 1; }
189 286 .repo-card::before {
190 287 content: ''; position: absolute; inset: 0 0 auto 0; height: 2px;
191 background: var(--gradient-brand); opacity: 0;
288 + background: var(--gradient-hot); opacity: 0;
192 289 transition: opacity 0.18s ease;
193 290 }
194 291 .repo-card:hover {
195 292 border-color: var(--border-strong); transform: translateY(-2px);
196 box-shadow: var(--shadow-card);
293 + box-shadow: inset 0 1px 0 var(--edge-light), var(--shadow-card);
197 294 }
198 295 .repo-card:hover::before { opacity: 1; }
199 296 .repo-card-name { margin: 0 0 4px; font-size: 16px; display: flex; align-items: center; gap: 8px; }
200 297 .repo-card-name a { color: var(--text-bright); font-weight: 600; }
201 298 .repo-card-name a:hover { color: var(--accent); text-decoration: none; }
299 +.repo-card-name a::after { content: ''; position: absolute; inset: 0; } /* whole card clickable */
300 +.repo-card .chip-topic, .repo-card .repo-card-meta { position: relative; z-index: 1; }
202 301 .repo-card-desc { margin: 0 0 10px; color: var(--muted); font-size: 14px; }
203 302 .repo-card .chip-row { margin-bottom: 10px; }
204 303 .repo-card-meta { display: flex; gap: 16px; color: var(--muted); font-size: 13px; flex-wrap: wrap; }
205 304 .lang-dot {
206 305 width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex: none;
207 box-shadow: 0 0 6px 0 currentColor; opacity: 0.95;
306 + box-shadow: 0 0 8px 0 currentColor; opacity: 0.95;
208 307 }
209 308 .lang-dot-label { display: inline-flex; align-items: center; gap: 6px; }
210 309
@@ -227,38 +326,50 @@ html[data-theme='light'] .icon-sun { display: none; }
227 326 }
228 327 .activity-feed li:not(:last-child)::before {
229 328 content: ''; position: absolute; left: 7px; top: 22px; bottom: -8px;
230 width: 2px; background: var(--border); border-radius: 1px;
329 + width: 2px; border-radius: 1px;
330 + background: linear-gradient(180deg, var(--accent-2-soft), var(--border));
231 331 }
232 332 .activity-dot {
233 333 width: 8px; height: 8px; border-radius: 50%;
234 334 background: var(--accent-2); flex: none; position: relative; top: -1px;
235 box-shadow: 0 0 8px 0 var(--accent-2);
335 + box-shadow: 0 0 10px 1px var(--accent-2);
336 +}
337 +.activity-feed li:first-child .activity-dot { animation: pulse-dot 2.4s ease infinite; }
338 +@keyframes pulse-dot {
339 + 0%, 100% { box-shadow: 0 0 8px 0 var(--accent-2); }
340 + 50% { box-shadow: 0 0 14px 3px var(--accent-2); }
236 341 }
237 342 .ref-inline { font-size: 12px; color: var(--muted); }
238 343
239 344 /* ── Heatmap ─────────────────────────────── */
240 .heatmap-section { margin-top: 44px; }
345 +.heatmap-section { margin-top: 46px; }
241 346 .heatmap-scroll { overflow-x: auto; padding-bottom: 6px; }
242 347 .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; }
348 +.heatmap-cell { transition: transform 0.1s ease; transform-origin: center; transform-box: fill-box; rx: 2.5px; }
349 +.heatmap-cell:hover { transform: scale(1.4); stroke: var(--accent); stroke-width: 1; }
245 350 .heatmap-cell[data-level='0'] { fill: var(--heat-0); }
246 351 .heatmap-cell[data-level='1'] { fill: var(--heat-1); }
247 352 .heatmap-cell[data-level='2'] { fill: var(--heat-2); }
248 353 .heatmap-cell[data-level='3'] { fill: var(--heat-3); }
249 .heatmap-cell[data-level='4'] { fill: var(--heat-4); }
354 +.heatmap-cell[data-level='4'] { fill: var(--heat-4); filter: drop-shadow(0 0 3px var(--heat-glow)); }
250 355 .heatmap-legend { display: flex; align-items: center; gap: 4px; color: var(--muted); font-size: 12px; margin-top: 8px; }
251 .heatmap-cell-demo { width: 11px; height: 11px; border-radius: 2px; display: inline-block; }
356 +.heatmap-cell-demo { width: 11px; height: 11px; border-radius: 3px; display: inline-block; }
252 357 .heatmap-cell-demo[data-level='0'] { background: var(--heat-0); }
253 358 .heatmap-cell-demo[data-level='1'] { background: var(--heat-1); }
254 359 .heatmap-cell-demo[data-level='2'] { background: var(--heat-2); }
255 360 .heatmap-cell-demo[data-level='3'] { background: var(--heat-3); }
256 .heatmap-cell-demo[data-level='4'] { background: var(--heat-4); }
361 +.heatmap-cell-demo[data-level='4'] { background: var(--heat-4); box-shadow: 0 0 5px var(--heat-glow); }
257 362
258 363 /* ── Repo header ─────────────────────────── */
259 .repo-header { padding-bottom: 0; margin-bottom: 22px; border-bottom: 1px solid var(--border); }
364 +.repo-header { padding-bottom: 0; margin-bottom: 22px; border-bottom: 1px solid var(--border); position: relative; }
365 +.repo-header::after {
366 + content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
367 + background: var(--gradient-hot); opacity: 0.4;
368 + mask: linear-gradient(90deg, black, transparent 65%);
369 + -webkit-mask: linear-gradient(90deg, black, transparent 65%);
370 +}
260 371 .repo-title {
261 font-size: 21px; margin: 6px 0 6px; display: flex; align-items: center;
372 + font-size: 22px; margin: 6px 0 6px; display: flex; align-items: center;
262 373 gap: 8px; flex-wrap: wrap; font-weight: 500; letter-spacing: -0.01em;
263 374 }
264 375 .repo-owner { color: var(--muted); }
@@ -270,11 +381,12 @@ html[data-theme='light'] .icon-sun { display: none; }
270 381 .repo-meta-row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin-bottom: 10px; }
271 382 .repo-homepage { font-size: 14px; font-weight: 500; }
272 383 .lang-bar {
273 display: flex; height: 8px; border-radius: 4px; overflow: hidden;
384 + display: flex; height: 9px; border-radius: 5px; overflow: hidden;
274 385 background: var(--surface-2); margin: 8px 0; gap: 1px;
386 + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25);
275 387 }
276 388 .lang-bar-seg { display: block; height: 100%; min-width: 2px; transition: filter 0.15s ease; }
277 .lang-bar-seg:hover { filter: brightness(1.25); }
389 +.lang-bar-seg:hover { filter: brightness(1.3) saturate(1.2); }
278 390 .lang-legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 13px; margin-bottom: 12px; }
279 391 .repo-tabs { display: flex; gap: 2px; align-items: center; margin-top: 10px; overflow-x: auto; }
280 392 .repo-tab {
@@ -287,6 +399,7 @@ html[data-theme='light'] .icon-sun { display: none; }
287 399 .repo-tab::after {
288 400 content: ''; position: absolute; left: 10px; right: 10px; bottom: 0; height: 2px;
289 401 background: var(--gradient-brand); border-radius: 2px 2px 0 0;
402 + box-shadow: 0 -1px 12px -2px var(--accent-glow);
290 403 opacity: 0; transform: scaleX(0.5);
291 404 transition: opacity 0.18s ease, transform 0.18s ease;
292 405 }
@@ -299,9 +412,7 @@ html[data-theme='light'] .icon-sun { display: none; }
299 412 .repo-toolbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between; margin-bottom: 16px; }
300 413 .clone-box {
301 414 display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
302 background: var(--surface); border: 1px solid var(--border);
303 415 border-radius: var(--radius-lg); padding: 5px;
304 box-shadow: var(--shadow-1);
305 416 }
306 417 .clone-url {
307 418 background: transparent; border: none; color: var(--muted);
@@ -317,35 +428,35 @@ html[data-theme='light'] .icon-sun { display: none; }
317 428
318 429 /* ── File table ──────────────────────────── */
319 430 .file-table {
320 width: 100%; border-collapse: collapse; border: 1px solid var(--border);
321 border-radius: var(--radius-lg); overflow: hidden; background: var(--surface);
322 font-size: 14px; margin-bottom: 26px; box-shadow: var(--shadow-1);
431 + width: 100%; border-collapse: collapse;
432 + border-radius: var(--radius-lg); overflow: hidden;
433 + font-size: 14px; margin-bottom: 26px;
323 434 }
324 435 .file-table td { padding: 9px 14px; border-top: 1px solid var(--border); }
325 436 .file-table tr:first-child td { border-top: 0; }
326 .file-row { transition: background 0.12s ease; }
327 .file-row:hover { background: var(--surface-2); }
437 +.file-row { transition: background 0.12s ease, box-shadow 0.12s ease; }
438 +.file-row:hover { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--accent); }
328 439 .file-name { white-space: nowrap; width: 32%; }
329 440 .file-name a { color: var(--text); }
330 441 .file-name a:hover { color: var(--accent); text-decoration: none; }
331 442 .file-icon { vertical-align: -3px; margin-right: 8px; color: var(--muted); }
332 .icon-dir { color: var(--accent); }
443 +.icon-dir { color: var(--accent); filter: drop-shadow(0 0 6px var(--accent-glow)); }
333 444 .file-commit { overflow: hidden; text-overflow: ellipsis; max-width: 400px; white-space: nowrap; }
334 445 .file-commit a:hover { color: var(--accent); }
335 446 .file-date { text-align: right; white-space: nowrap; font-size: 13px; font-variant-numeric: tabular-nums; }
336 447
337 448 /* ── README / markdown section ───────────── */
338 449 .readme-section {
339 border: 1px solid var(--border); border-radius: var(--radius-lg);
340 background: var(--surface); margin-bottom: 32px; box-shadow: var(--shadow-1);
450 + border-radius: var(--radius-lg);
451 + margin-bottom: 32px;
341 452 }
342 453 .readme-header {
343 454 display: flex; align-items: center; gap: 8px; padding: 11px 20px;
344 455 border-bottom: 1px solid var(--border); color: var(--muted); font-size: 13px;
345 position: sticky; top: 58px; background: var(--surface); z-index: 10;
456 + position: sticky; top: 60px; background: var(--surface); z-index: 10;
346 457 border-radius: var(--radius-lg) var(--radius-lg) 0 0;
347 458 }
348 .readme-section .markdown-body { padding: 26px 34px 34px; }
459 +.readme-section .markdown-body { padding: 28px 36px 36px; }
349 460 .empty-repo { text-align: center; padding: 52px 20px; }
350 461 .notice {
351 462 background: var(--accent-soft); border: 1px solid var(--accent);
@@ -354,20 +465,20 @@ html[data-theme='light'] .icon-sun { display: none; }
354 465
355 466 /* ── Blob view ───────────────────────────── */
356 467 .blob-card {
357 border: 1px solid var(--border); border-radius: var(--radius-lg);
358 background: var(--surface); overflow: hidden; box-shadow: var(--shadow-1);
468 + border-radius: var(--radius-lg);
469 + overflow: hidden;
359 470 }
360 471 .blob-header {
361 472 display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
362 473 padding: 10px 16px; border-bottom: 1px solid var(--border);
363 position: sticky; top: 58px; background: var(--surface); z-index: 10;
474 + position: sticky; top: 60px; background: var(--surface); z-index: 10;
364 475 }
365 476 .blob-meta { display: flex; gap: 8px; align-items: center; font-size: 13px; }
366 477 .blob-actions { margin-left: auto; display: flex; gap: 6px; flex-wrap: wrap; }
367 478 .blob-image { text-align: center; padding: 32px; background: var(--surface-2); }
368 479 .blob-image img { max-width: 100%; border-radius: var(--radius); box-shadow: var(--shadow-2); }
369 480 .blob-binary { text-align: center; padding: 52px 20px; color: var(--muted); }
370 .blob-markdown { padding: 26px 34px 34px; }
481 +.blob-markdown { padding: 28px 36px 36px; }
371 482
372 483 /* PDF viewer */
373 484 .pdf-viewer { background: var(--surface-2); }
@@ -402,14 +513,13 @@ html[data-theme='light'] .icon-sun { display: none; }
402 513 /* ── Commits ─────────────────────────────── */
403 514 .commit-list {
404 515 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;
516 + border-radius: var(--radius-lg); overflow: hidden;
407 517 }
408 518 .commit-row {
409 519 display: flex; gap: 12px; align-items: flex-start; padding: 13px 16px;
410 border-top: 1px solid var(--border); transition: background 0.12s ease;
520 + border-top: 1px solid var(--border); transition: background 0.12s ease, box-shadow 0.12s ease;
411 521 }
412 .commit-row:hover { background: var(--surface-2); }
522 +.commit-row:hover { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--accent); }
413 523 .commit-row:first-child { border-top: 0; }
414 524 .commit-avatar {
415 525 flex: none; border-radius: 8px; overflow: hidden; background: var(--surface-2);
@@ -430,16 +540,15 @@ html[data-theme='light'] .icon-sun { display: none; }
430 540 font-family: var(--font-mono); font-size: 12px; background: var(--surface-2);
431 541 color: var(--accent); border: 1px solid var(--border); border-radius: 7px;
432 542 padding: 3px 9px; cursor: pointer; flex: none;
433 transition: border-color 0.15s ease, transform 0.12s ease;
543 + transition: border-color 0.15s ease, transform 0.12s ease, box-shadow 0.15s ease;
434 544 }
435 .sha-chip:hover { border-color: var(--accent); transform: translateY(-1px); }
545 +.sha-chip:hover { border-color: var(--accent); transform: translateY(-1px); box-shadow: 0 4px 14px -6px var(--accent-glow); }
436 546 .pagination { display: flex; gap: 10px; justify-content: center; margin: 22px 0; }
437 547
438 548 /* ── Commit detail / diffs ───────────────── */
439 549 .commit-detail-header {
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);
550 + border-radius: var(--radius-lg);
551 + padding: 18px 22px; margin-bottom: 18px;
443 552 }
444 553 .commit-detail-subject { margin: 0 0 8px; font-size: 19px; letter-spacing: -0.01em; }
445 554 .commit-detail-meta { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; font-size: 14px; }
@@ -448,14 +557,13 @@ html[data-theme='light'] .icon-sun { display: none; }
448 557 .diff-del { color: var(--del-text); font-weight: 600; font-variant-numeric: tabular-nums; }
449 558
450 559 .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);
560 + border-radius: var(--radius-lg);
561 + margin-bottom: 16px; overflow: hidden;
454 562 }
455 563 .diff-file-header {
456 564 display: flex; gap: 10px; align-items: center; padding: 10px 14px;
457 565 cursor: pointer; background: var(--surface-2); font-size: 13px; list-style: none;
458 position: sticky; top: 58px; z-index: 5;
566 + position: sticky; top: 60px; z-index: 5;
459 567 transition: background 0.12s ease;
460 568 }
461 569 .diff-file-header:hover { background: var(--surface-3); }
@@ -515,9 +623,9 @@ html[data-theme='light'] .icon-sun { display: none; }
515 623
516 624 /* ── Branch/tag tables ───────────────────── */
517 625 .ref-table {
518 width: 100%; border-collapse: collapse; background: var(--surface);
519 border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden;
520 font-size: 14px; box-shadow: var(--shadow-1);
626 + width: 100%; border-collapse: collapse;
627 + border-radius: var(--radius-lg); overflow: hidden;
628 + font-size: 14px;
521 629 }
522 630 .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 631 .ref-table td { padding: 11px 14px; border-top: 1px solid var(--border); }
@@ -531,12 +639,10 @@ html[data-theme='light'] .icon-sun { display: none; }
531 639 .release-list { display: flex; flex-direction: column; gap: 18px; }
532 640 .release-card {
533 641 position: relative;
534 background: var(--surface); border: 1px solid var(--border);
535 642 border-radius: var(--radius-lg); padding: 18px 22px;
536 box-shadow: var(--shadow-1);
537 643 transition: border-color 0.18s ease, box-shadow 0.18s ease;
538 644 }
539 .release-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-card); }
645 +.release-card:hover { border-color: var(--border-strong); box-shadow: inset 0 1px 0 var(--edge-light), var(--shadow-card); }
540 646 .release-header { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
541 647 .release-tag { margin: 0; font-size: 17px; font-family: var(--font-mono); }
542 648 .release-subject { color: var(--muted); }
@@ -545,33 +651,95 @@ html[data-theme='light'] .icon-sun { display: none; }
545 651 display: flex; align-items: center; gap: 10px;
546 652 background: var(--surface-2); border: 1px solid var(--border);
547 653 border-radius: var(--radius); padding: 9px 14px; font-size: 14px;
548 transition: border-color 0.15s ease;
654 + transition: border-color 0.15s ease, box-shadow 0.15s ease;
549 655 }
550 .release-assets li:hover { border-color: var(--accent); }
656 +.release-assets li:hover { border-color: var(--accent); box-shadow: 0 4px 18px -8px var(--accent-glow); }
551 657 .release-assets li svg { color: var(--accent-2); flex: none; }
552 658
553 659 /* ── Search page ─────────────────────────── */
554 660 .search-page-form { display: flex; gap: 8px; margin-bottom: 28px; max-width: 560px; }
555 661 .search-results { list-style: none; margin: 0 0 32px; padding: 0; display: flex; flex-direction: column; gap: 14px; }
556 662 .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;
663 + border-radius: var(--radius-lg); padding: 15px 19px;
664 + transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
560 665 }
561 .search-results li:hover { border-color: var(--border-strong); transform: translateY(-1px); }
666 +.search-results li:hover { border-color: var(--border-strong); transform: translateY(-1px); box-shadow: inset 0 1px 0 var(--edge-light), var(--shadow-card); }
562 667 .search-hit-name { font-weight: 600; font-size: 15px; }
563 668 .search-snippet { color: var(--muted); font-size: 13px; margin: 6px 0 0; }
564 669
670 +/* ── ⌘K command palette ──────────────────── */
671 +.cmdk[hidden] { display: none; }
672 +.cmdk { position: fixed; inset: 0; z-index: 90; }
673 +.cmdk-backdrop {
674 + position: absolute; inset: 0; background: var(--backdrop);
675 + backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
676 + animation: fade-in 0.16s ease both;
677 +}
678 +.cmdk-panel {
679 + position: relative; margin: 12vh auto 0; width: min(620px, calc(100vw - 32px));
680 + background: var(--surface-grad); border: 1px solid var(--border-strong);
681 + border-radius: var(--radius-lg); overflow: hidden;
682 + box-shadow: inset 0 1px 0 var(--edge-light), var(--shadow-pop), 0 0 80px -30px var(--accent-glow);
683 + animation: pop-in 0.18s cubic-bezier(0.2, 1.2, 0.4, 1) both;
684 +}
685 +@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
686 +@keyframes pop-in {
687 + from { opacity: 0; transform: translateY(10px) scale(0.985); }
688 + to { opacity: 1; transform: none; }
689 +}
690 +.cmdk-input-row {
691 + display: flex; align-items: center; gap: 10px; padding: 14px 16px;
692 + border-bottom: 1px solid var(--border);
693 +}
694 +.cmdk-input-row svg { color: var(--muted); flex: none; }
695 +.cmdk-input-row input {
696 + flex: 1; background: transparent; border: 0; outline: none;
697 + color: var(--text-bright); font-size: 16px; font-family: var(--font-ui);
698 +}
699 +.cmdk-input-row input::placeholder { color: var(--faint); }
700 +.cmdk-input-row kbd {
701 + background: var(--surface-2); border: 1px solid var(--border); border-radius: 6px;
702 + padding: 2px 7px; font-size: 11px; color: var(--muted);
703 +}
704 +.cmdk-list { list-style: none; margin: 0; padding: 8px; max-height: 46vh; overflow-y: auto; }
705 +.cmdk-item {
706 + display: flex; align-items: center; gap: 12px; padding: 10px 12px;
707 + border-radius: 10px; cursor: pointer; font-size: 14px;
708 + color: var(--text); position: relative;
709 +}
710 +.cmdk-item:hover, .cmdk-item[aria-selected='true'] { background: var(--accent-soft); }
711 +.cmdk-item[aria-selected='true']::before {
712 + content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 3px;
713 + border-radius: 2px; background: var(--gradient-brand);
714 + box-shadow: 0 0 10px -1px var(--accent-glow);
715 +}
716 +.cmdk-item-name { font-weight: 600; color: var(--text-bright); white-space: nowrap; }
717 +.cmdk-item-desc { color: var(--muted); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
718 +.cmdk-item .lang-dot { width: 8px; height: 8px; }
719 +.cmdk-item-hint { margin-left: auto; color: var(--faint); font-size: 12px; font-family: var(--font-mono); flex: none; }
720 +.cmdk-empty { padding: 26px 12px; text-align: center; color: var(--muted); font-size: 14px; }
721 +.cmdk-footer {
722 + display: flex; gap: 14px; padding: 9px 16px; border-top: 1px solid var(--border);
723 + color: var(--faint); font-size: 12px;
724 +}
725 +.cmdk-footer kbd {
726 + background: var(--surface-2); border: 1px solid var(--border); border-radius: 5px;
727 + padding: 0 5px; font-size: 11px; margin-right: 4px;
728 +}
729 +body.cmdk-open { overflow: hidden; }
730 +
565 731 /* ── Error pages ─────────────────────────── */
566 732 .error-page { text-align: center; padding: 80px 20px; }
567 733 .error-code {
568 font-size: 104px; font-weight: 700; font-family: var(--font-mono); line-height: 1;
569 background: var(--gradient-brand);
734 + font-size: 110px; font-weight: 700; font-family: var(--font-mono); line-height: 1;
735 + background: var(--gradient-hot); background-size: 200% 200%;
736 + animation: gradient-pan 6s ease infinite;
570 737 -webkit-background-clip: text; background-clip: text;
571 738 -webkit-text-fill-color: transparent; color: transparent;
739 + filter: drop-shadow(0 6px 30px var(--accent-glow));
572 740 }
573 741 .error-message { font-size: 18px; color: var(--muted); margin: 16px 0 8px; }
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; }
742 +.error-art { display: inline-block; text-align: left; color: var(--muted); border-radius: var(--radius); padding: 14px 18px; font-size: 13px; margin: 16px 0 24px; }
575 743
576 744 /* ── Mermaid blocks ──────────────────────── */
577 745 .mermaid-block { margin: 16px 0; }
@@ -581,18 +749,31 @@ html[data-theme='light'] .icon-sun { display: none; }
581 749
582 750 /* ── Footer ──────────────────────────────── */
583 751 .footer {
752 + position: relative;
584 753 border-top: 1px solid var(--border); color: var(--muted);
585 padding: 22px 0; font-size: 13px; text-align: center;
754 + padding: 26px 0; font-size: 13px;
755 +}
756 +.footer::before {
757 + content: ''; position: absolute; left: 0; right: 0; top: -1px; height: 1px;
758 + background: var(--gradient-hot); opacity: 0.3;
759 + mask: linear-gradient(90deg, transparent, black 30%, black 70%, transparent);
760 + -webkit-mask: linear-gradient(90deg, transparent, black 30%, black 70%, transparent);
586 761 }
762 +.footer-inner { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
763 +.footer-brand { display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--text); }
764 +.footer-links { display: flex; gap: 16px; }
765 +.footer a { color: var(--muted); }
766 +.footer a:hover { color: var(--accent); }
587 767
588 768 /* ── Page entrance ───────────────────────── */
589 769 @keyframes rise-in {
590 from { opacity: 0; transform: translateY(6px); }
770 + from { opacity: 0; transform: translateY(8px); }
591 771 to { opacity: 1; transform: none; }
592 772 }
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; }
773 +main.container > * { animation: rise-in 0.4s cubic-bezier(0.2, 0.9, 0.3, 1) both; }
774 +main.container > *:nth-child(2) { animation-delay: 0.05s; }
775 +main.container > *:nth-child(3) { animation-delay: 0.1s; }
776 +main.container > *:nth-child(4) { animation-delay: 0.14s; }
596 777
597 778 /* ── Responsive ──────────────────────────── */
598 779 @media (max-width: 960px) {
@@ -602,11 +783,14 @@ main.container > *:nth-child(3) { animation-delay: 0.08s; }
602 783 }
603 784 @media (max-width: 640px) {
604 785 .global-search { display: none; }
605 .hero { flex-direction: column; align-items: flex-start; }
786 + .hero { flex-direction: column; align-items: flex-start; padding-top: 36px; }
787 + .hero-name { font-size: 27px; }
788 + .hero-avatar { width: 68px; height: 68px; border-radius: 19px; font-size: 21px; }
606 789 .pinned-grid { grid-template-columns: 1fr; }
607 790 .clone-url { width: 100%; max-width: none; }
608 791 .repo-toolbar { flex-direction: column; align-items: stretch; }
609 792 .file-date { display: none; }
610 793 .readme-section .markdown-body, .blob-markdown { padding: 16px; }
611 794 .pdf-frame { height: 70vh; min-height: 420px; }
795 + .footer-inner { flex-direction: column; align-items: flex-start; gap: 8px; }
612 796 }
modified src/web/assets/css/markdown.css +17 −8
@@ -5,7 +5,7 @@
5 5 * Author : Simon-Pierre Boucher
6 6 * Contact : contact@spboucher.ai
7 7 * File : src/web/assets/css/markdown.css
8 * Purpose : GitHub-grade .markdown-body typography (badges included)
8 + * Purpose : GitHub-grade .markdown-body typography v3 (badges included)
9 9 * License : MIT © Simon-Pierre Boucher
10 10 * ─────────────────────────────────────────────
11 11 */
@@ -27,8 +27,13 @@
27 27 line-height: 1.25;
28 28 position: relative;
29 29 }
30 .markdown-body h1 { font-size: 2em; padding-bottom: 0.3em; border-bottom: 1px solid var(--border); }
31 .markdown-body h2 { font-size: 1.5em; padding-bottom: 0.3em; border-bottom: 1px solid var(--border); }
30 +.markdown-body h1, .markdown-body h2 {
31 + padding-bottom: 0.3em;
32 + border-bottom: 1px solid var(--border);
33 + background: linear-gradient(90deg, var(--accent), var(--accent-2)) left bottom / 48px 1px no-repeat;
34 +}
35 +.markdown-body h1 { font-size: 2em; letter-spacing: -0.02em; }
36 +.markdown-body h2 { font-size: 1.5em; letter-spacing: -0.01em; }
32 37 .markdown-body h3 { font-size: 1.25em; }
33 38 .markdown-body h4 { font-size: 1em; }
34 39 .markdown-body h5 { font-size: 0.875em; }
@@ -55,9 +60,12 @@
55 60 }
56 61
57 62 .markdown-body blockquote {
58 padding: 0 1em;
63 + padding: 0.5em 1em;
59 64 color: var(--muted);
60 border-left: 0.25em solid var(--accent-2);
65 + border-left: 3px solid transparent;
66 + border-image: linear-gradient(180deg, var(--accent), var(--accent-2)) 1;
67 + background: linear-gradient(90deg, var(--accent-soft), transparent 70%);
68 + border-radius: 0 8px 8px 0;
61 69 margin-left: 0;
62 70 margin-right: 0;
63 71 }
@@ -73,12 +81,13 @@
73 81
74 82 .markdown-body code {
75 83 background: var(--surface-2);
84 + border: 1px solid var(--border);
76 85 border-radius: 6px;
77 padding: 0.2em 0.4em;
86 + padding: 0.15em 0.4em;
78 87 font-size: 85%;
79 88 font-family: var(--font-mono);
80 89 }
81 .markdown-body pre code { background: transparent; padding: 0; font-size: 100%; }
90 +.markdown-body pre code { background: transparent; border: 0; padding: 0; font-size: 100%; }
82 91
83 92 .markdown-body .code-block {
84 93 background: var(--code-bg);
@@ -156,7 +165,7 @@
156 165 .markdown-body hr {
157 166 height: 1px;
158 167 border: 0;
159 background: var(--border);
168 + background: linear-gradient(90deg, transparent, var(--border-strong) 20%, var(--border-strong) 80%, transparent);
160 169 margin: 24px 0;
161 170 }
162 171
modified src/web/assets/css/tokens.css +132 −78
@@ -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 v2 — colors, gradients, depth, themes
8 + * Purpose : Design tokens v3 "Nightforge" — electric palette, aurora, depth
9 9 * License : MIT © Simon-Pierre Boucher
10 10 * ─────────────────────────────────────────────
11 11 */
@@ -55,100 +55,154 @@
55 55
56 56 :root,
57 57 html[data-theme='dark'] {
58 --bg: #0b0e14;
59 --bg-glow-1: rgba(79, 140, 255, 0.055);
60 --bg-glow-2: rgba(34, 211, 170, 0.04);
61 --surface: #11151c;
62 --surface-2: #161b24;
63 --surface-3: #1b2130;
64 --border: #1f2530;
65 --border-strong: #2a3242;
66 --text: #e6e9ef;
67 --text-bright: #f4f6fa;
68 --muted: #8b93a3;
69 --faint: #5c6474;
70 --accent: #4f8cff;
71 --accent-hover: #6ea1ff;
72 --accent-soft: rgba(79, 140, 255, 0.14);
73 --accent-glow: rgba(79, 140, 255, 0.35);
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%);
78 --danger: #ff5d5d;
79 --success: #3fb950;
80 --warning: #d29922;
81 --add-bg: rgba(63, 185, 80, 0.14);
82 --add-bg-strong: rgba(63, 185, 80, 0.28);
83 --add-text: #56d364;
84 --del-bg: rgba(248, 81, 73, 0.13);
85 --del-bg-strong: rgba(248, 81, 73, 0.28);
86 --del-text: #ff7b72;
87 --hunk-bg: rgba(79, 140, 255, 0.07);
88 --code-bg: #0d1117;
89 --code-head: #131922;
90 --chip-bg: rgba(79, 140, 255, 0.12);
91 --heat-0: #161b24;
92 --heat-1: #0e4429;
93 --heat-2: #006d32;
94 --heat-3: #26a641;
95 --heat-4: #39d353;
96 --radius: 10px;
97 --radius-lg: 14px;
58 + /* canvas */
59 + --bg: #05070c;
60 + --bg-glow-1: rgba(124, 154, 255, 0.09);
61 + --bg-glow-2: rgba(52, 227, 176, 0.055);
62 + --bg-glow-3: rgba(192, 132, 252, 0.07);
63 + --bg-glow-4: rgba(56, 207, 253, 0.06);
64 +
65 + /* surfaces & lines */
66 + --surface: #0d1119;
67 + --surface-2: #131826;
68 + --surface-3: #1a2033;
69 + --surface-grad: linear-gradient(180deg, #10151f 0%, #0c1018 100%);
70 + --border: #1d2434;
71 + --border-strong: #2b3550;
72 + --edge-light: rgba(255, 255, 255, 0.055); /* 1px inner top highlight */
73 +
74 + /* text */
75 + --text: #e7eaf3;
76 + --text-bright: #f6f8fd;
77 + --muted: #8d95ab;
78 + --faint: #5b6478;
79 +
80 + /* accents */
81 + --accent: #7c9aff;
82 + --accent-hover: #9ab2ff;
83 + --accent-soft: rgba(124, 154, 255, 0.15);
84 + --accent-glow: rgba(124, 154, 255, 0.4);
85 + --accent-2: #34e3b0;
86 + --accent-2-soft: rgba(52, 227, 176, 0.13);
87 + --accent-3: #c084fc;
88 + --accent-3-soft: rgba(192, 132, 252, 0.13);
89 +
90 + /* gradients */
91 + --gradient-brand: linear-gradient(120deg, #7c9aff 0%, #38cffd 52%, #34e3b0 100%);
92 + --gradient-hot: linear-gradient(115deg, #c084fc 0%, #7c9aff 30%, #38cffd 62%, #34e3b0 100%);
93 + --gradient-text: linear-gradient(100deg, #f6f8fd 0%, #c9d6ff 45%, #7c9aff 80%, #38cffd 100%);
94 + --gradient-border:
95 + linear-gradient(140deg, rgba(124, 154, 255, 0.85), rgba(56, 207, 253, 0.55) 45%, rgba(52, 227, 176, 0.75));
96 +
97 + /* status */
98 + --danger: #ff6b6b;
99 + --success: #3fd06a;
100 + --warning: #e8b339;
101 +
102 + /* diff */
103 + --add-bg: rgba(63, 208, 106, 0.13);
104 + --add-bg-strong: rgba(63, 208, 106, 0.26);
105 + --add-text: #5be381;
106 + --del-bg: rgba(255, 99, 99, 0.12);
107 + --del-bg-strong: rgba(255, 99, 99, 0.26);
108 + --del-text: #ff8585;
109 + --hunk-bg: rgba(124, 154, 255, 0.08);
110 +
111 + /* code */
112 + --code-bg: #090c13;
113 + --code-head: #10141f;
114 + --chip-bg: rgba(124, 154, 255, 0.13);
115 +
116 + /* heatmap */
117 + --heat-0: #12172386;
118 + --heat-1: #133a54;
119 + --heat-2: #14639c;
120 + --heat-3: #38cffd;
121 + --heat-4: #9be9ff;
122 + --heat-glow: rgba(56, 207, 253, 0.55);
123 +
124 + /* geometry & type */
125 + --radius: 12px;
126 + --radius-lg: 16px;
98 127 --font-ui: 'Inter', -apple-system, 'Segoe UI', sans-serif;
99 128 --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
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);
129 +
130 + /* depth */
131 + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
132 + --shadow-2: 0 8px 28px -10px rgba(0, 0, 0, 0.6);
133 + --shadow-card: 0 18px 50px -20px rgba(0, 0, 0, 0.75), 0 0 40px -18px var(--accent-glow);
134 + --shadow-pop: 0 24px 80px -24px rgba(0, 0, 0, 0.85);
135 + --ring: 0 0 0 3px rgba(124, 154, 255, 0.35);
136 + --nav-bg: rgba(5, 7, 12, 0.72);
137 + --backdrop: rgba(3, 4, 8, 0.66);
105 138 color-scheme: dark;
106 139 }
107 140
108 141 html[data-theme='light'] {
109 --bg: #f6f8fb;
110 --bg-glow-1: rgba(47, 111, 224, 0.05);
111 --bg-glow-2: rgba(13, 158, 128, 0.04);
142 + --bg: #f5f7fd;
143 + --bg-glow-1: rgba(76, 111, 255, 0.075);
144 + --bg-glow-2: rgba(12, 166, 120, 0.05);
145 + --bg-glow-3: rgba(147, 51, 234, 0.05);
146 + --bg-glow-4: rgba(2, 148, 212, 0.055);
147 +
112 148 --surface: #ffffff;
113 --surface-2: #f1f4f8;
114 --surface-3: #e9edf4;
115 --border: #dbe1ea;
116 --border-strong: #c3ccd9;
117 --text: #1c2330;
118 --text-bright: #0d1320;
119 --muted: #57606e;
120 --faint: #8a93a3;
121 --accent: #2f6fe0;
122 --accent-hover: #1e5ecf;
123 --accent-soft: rgba(47, 111, 224, 0.1);
124 --accent-glow: rgba(47, 111, 224, 0.25);
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%);
149 + --surface-2: #f0f3fa;
150 + --surface-3: #e7ebf6;
151 + --surface-grad: linear-gradient(180deg, #ffffff 0%, #fafbfe 100%);
152 + --border: #dde3f0;
153 + --border-strong: #c2cbdf;
154 + --edge-light: rgba(255, 255, 255, 0.85);
155 +
156 + --text: #1b2233;
157 + --text-bright: #0c1224;
158 + --muted: #566078;
159 + --faint: #8a93a9;
160 +
161 + --accent: #4c6fff;
162 + --accent-hover: #3557e8;
163 + --accent-soft: rgba(76, 111, 255, 0.11);
164 + --accent-glow: rgba(76, 111, 255, 0.3);
165 + --accent-2: #0ca678;
166 + --accent-2-soft: rgba(12, 166, 120, 0.11);
167 + --accent-3: #9333ea;
168 + --accent-3-soft: rgba(147, 51, 234, 0.1);
169 +
170 + --gradient-brand: linear-gradient(120deg, #4c6fff 0%, #0294d4 52%, #0ca678 100%);
171 + --gradient-hot: linear-gradient(115deg, #9333ea 0%, #4c6fff 30%, #0294d4 62%, #0ca678 100%);
172 + --gradient-text: linear-gradient(100deg, #0c1224 0%, #23346c 45%, #4c6fff 85%);
173 + --gradient-border:
174 + linear-gradient(140deg, rgba(76, 111, 255, 0.7), rgba(2, 148, 212, 0.45) 45%, rgba(12, 166, 120, 0.6));
175 +
129 176 --danger: #d33636;
130 177 --success: #1a7f37;
131 178 --warning: #9a6700;
179 +
132 180 --add-bg: rgba(26, 127, 55, 0.1);
133 181 --add-bg-strong: rgba(26, 127, 55, 0.22);
134 182 --add-text: #1a7f37;
135 183 --del-bg: rgba(207, 34, 46, 0.09);
136 184 --del-bg-strong: rgba(207, 34, 46, 0.2);
137 185 --del-text: #cf222e;
138 --hunk-bg: rgba(47, 111, 224, 0.06);
139 --code-bg: #f6f8fa;
140 --code-head: #eef1f5;
141 --chip-bg: rgba(47, 111, 224, 0.1);
142 --heat-0: #ebedf0;
143 --heat-1: #9be9a8;
144 --heat-2: #40c463;
145 --heat-3: #30a14e;
146 --heat-4: #216e39;
186 + --hunk-bg: rgba(76, 111, 255, 0.07);
187 +
188 + --code-bg: #f7f9fd;
189 + --code-head: #eef1f8;
190 + --chip-bg: rgba(76, 111, 255, 0.1);
191 +
192 + --heat-0: #e8ecf4;
193 + --heat-1: #bfe3ff;
194 + --heat-2: #6cc0f7;
195 + --heat-3: #2492e0;
196 + --heat-4: #0b5cad;
197 + --heat-glow: rgba(36, 146, 224, 0.4);
198 +
147 199 --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);
200 + --shadow-2: 0 8px 28px -12px rgba(16, 24, 40, 0.18);
201 + --shadow-card: 0 18px 50px -24px rgba(16, 24, 40, 0.28), 0 0 36px -20px var(--accent-glow);
202 + --shadow-pop: 0 24px 80px -28px rgba(16, 24, 40, 0.35);
203 + --ring: 0 0 0 3px rgba(76, 111, 255, 0.28);
204 + --nav-bg: rgba(245, 247, 253, 0.78);
205 + --backdrop: rgba(18, 24, 40, 0.4);
152 206 color-scheme: light;
153 207 }
154 208
modified src/web/assets/js/app.js +139 −1
@@ -5,7 +5,7 @@
5 5 * Author : Simon-Pierre Boucher
6 6 * Contact : contact@spboucher.ai
7 7 * File : src/web/assets/js/app.js
8 * Purpose : Vanilla JS layer — theme, copy, filters, anchors, mermaid
8 + * Purpose : Vanilla JS layer — theme, copy, filters, anchors, mermaid, ⌘K, spotlight
9 9 * License : MIT © Simon-Pierre Boucher
10 10 * ─────────────────────────────────────────────
11 11 */
@@ -170,6 +170,144 @@
170 170 });
171 171 }
172 172
173 + /* ── Card spotlight (mouse-tracking glow) ─── */
174 + document.addEventListener('pointermove', function (event) {
175 + var card = event.target.closest ? event.target.closest('.repo-card') : null;
176 + if (!card) return;
177 + var rect = card.getBoundingClientRect();
178 + card.style.setProperty('--mx', (event.clientX - rect.left) + 'px');
179 + card.style.setProperty('--my', (event.clientY - rect.top) + 'px');
180 + }, { passive: true });
181 +
182 + /* ── ⌘K command palette ───────────────────── */
183 + var cmdk = document.getElementById('cmdk');
184 + if (cmdk) {
185 + var cmdkInput = document.getElementById('cmdk-input');
186 + var cmdkList = document.getElementById('cmdk-list');
187 + var cmdkRepos = null;
188 + var cmdkItems = [];
189 + var cmdkIndex = 0;
190 +
191 + // Subsequence fuzzy match; lower score = better. Returns -1 on no match.
192 + var fuzzyScore = function (query, text) {
193 + if (!query) return 0;
194 + var q = query.toLowerCase();
195 + var t = text.toLowerCase();
196 + var direct = t.indexOf(q);
197 + if (direct !== -1) return direct;
198 + var ti = 0;
199 + var gaps = 0;
200 + for (var qi = 0; qi < q.length; qi += 1) {
201 + var found = t.indexOf(q[qi], ti);
202 + if (found === -1) return -1;
203 + gaps += found - ti;
204 + ti = found + 1;
205 + }
206 + return 100 + gaps;
207 + };
208 +
209 + var cmdkRender = function () {
210 + var query = cmdkInput.value.trim();
211 + var scored = [];
212 + (cmdkRepos || []).forEach(function (repo) {
213 + var haystack = repo.name + ' ' + (repo.description || '');
214 + var score = fuzzyScore(query, haystack);
215 + if (score !== -1) scored.push({ repo: repo, score: score });
216 + });
217 + scored.sort(function (a, b) { return a.score - b.score || a.repo.name.localeCompare(b.repo.name); });
218 + scored = scored.slice(0, 12);
219 + cmdkList.innerHTML = '';
220 + cmdkItems = [];
221 + if (cmdkRepos === null) {
222 + cmdkList.innerHTML = '<li class="cmdk-empty">Loading repositories…</li>';
223 + return;
224 + }
225 + if (!scored.length) {
226 + var empty = document.createElement('li');
227 + empty.className = 'cmdk-empty';
228 + empty.textContent = query ? 'No repository matches “' + query + '”' : 'No repositories yet.';
229 + cmdkList.appendChild(empty);
230 + return;
231 + }
232 + scored.forEach(function (entry, index) {
233 + var li = document.createElement('li');
234 + li.className = 'cmdk-item';
235 + li.setAttribute('role', 'option');
236 + li.setAttribute('data-href', '/' + entry.repo.name);
237 + var name = document.createElement('span');
238 + name.className = 'cmdk-item-name';
239 + name.textContent = entry.repo.name;
240 + li.appendChild(name);
241 + if (entry.repo.description) {
242 + var desc = document.createElement('span');
243 + desc.className = 'cmdk-item-desc';
244 + desc.textContent = entry.repo.description;
245 + li.appendChild(desc);
246 + }
247 + if (entry.repo.topLanguage) {
248 + var hint = document.createElement('span');
249 + hint.className = 'cmdk-item-hint';
250 + hint.textContent = entry.repo.topLanguage;
251 + li.appendChild(hint);
252 + }
253 + li.addEventListener('click', function () { location.href = li.getAttribute('data-href'); });
254 + li.addEventListener('pointerenter', function () { cmdkSelect(index); });
255 + cmdkList.appendChild(li);
256 + cmdkItems.push(li);
257 + });
258 + cmdkSelect(0);
259 + };
260 +
261 + var cmdkSelect = function (index) {
262 + cmdkIndex = Math.max(0, Math.min(index, cmdkItems.length - 1));
263 + cmdkItems.forEach(function (item, i) {
264 + item.setAttribute('aria-selected', i === cmdkIndex ? 'true' : 'false');
265 + });
266 + var current = cmdkItems[cmdkIndex];
267 + if (current) current.scrollIntoView({ block: 'nearest' });
268 + };
269 +
270 + var cmdkOpen = function () {
271 + cmdk.hidden = false;
272 + document.body.classList.add('cmdk-open');
273 + cmdkInput.value = '';
274 + cmdkRender();
275 + cmdkInput.focus();
276 + if (cmdkRepos === null) {
277 + fetch('/api/v1/repos')
278 + .then(function (res) { return res.json(); })
279 + .then(function (data) { cmdkRepos = data.repos || []; cmdkRender(); })
280 + .catch(function () { cmdkRepos = []; cmdkRender(); });
281 + }
282 + };
283 + var cmdkClose = function () {
284 + cmdk.hidden = true;
285 + document.body.classList.remove('cmdk-open');
286 + };
287 +
288 + var opener = document.getElementById('cmdk-open');
289 + if (opener) opener.addEventListener('click', cmdkOpen);
290 + cmdk.addEventListener('click', function (event) {
291 + if (event.target.closest('[data-cmdk-close]')) cmdkClose();
292 + });
293 + cmdkInput.addEventListener('input', cmdkRender);
294 + document.addEventListener('keydown', function (event) {
295 + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
296 + event.preventDefault();
297 + if (cmdk.hidden) cmdkOpen(); else cmdkClose();
298 + return;
299 + }
300 + if (cmdk.hidden) return;
301 + if (event.key === 'Escape') { event.preventDefault(); cmdkClose(); }
302 + else if (event.key === 'ArrowDown') { event.preventDefault(); cmdkSelect(cmdkIndex + 1); }
303 + else if (event.key === 'ArrowUp') { event.preventDefault(); cmdkSelect(cmdkIndex - 1); }
304 + else if (event.key === 'Enter') {
305 + var current = cmdkItems[cmdkIndex];
306 + if (current) { event.preventDefault(); location.href = current.getAttribute('data-href'); }
307 + }
308 + });
309 + }
310 +
173 311 /* ── Mermaid: lazy-load only when present ─── */
174 312 var mermaidBlocks = document.querySelectorAll('.mermaid-block');
175 313 if (mermaidBlocks.length > 0) {
modified src/web/views/layout.njk +35 −2
@@ -40,6 +40,7 @@
40 40 <script src="/assets/js/theme-boot.js"></script>
41 41 </head>
42 42 <body>
43 + <div class="aurora" aria-hidden="true"></div>
43 44 <a class="skip-link" href="#main">Skip to content</a>
44 45 <header class="topnav">
45 46 <div class="container topnav-inner">
@@ -53,6 +54,7 @@
53 54 <form class="global-search" action="/search" method="get" role="search">
54 55 <input type="search" name="q" id="global-search" placeholder="Search repositories… ( / )"
55 56 value="{{ q | default('') }}" autocomplete="off" aria-label="Search repositories">
57 + <button type="button" class="kbd-hint" id="cmdk-open" aria-label="Open command palette" title="Jump to repository">⌘K</button>
56 58 </form>
57 59 <nav class="topnav-links" aria-label="Site">
58 60 <button id="theme-toggle" class="icon-btn" type="button" aria-label="Toggle color theme" title="Toggle theme">
@@ -67,10 +69,41 @@
67 69 {% block content %}{% endblock %}
68 70 </main>
69 71 <footer class="footer">
70 <div class="container">
71 © {{ owner.name }} · <a href="mailto:{{ owner.email }}">{{ owner.email }}</a> · Powered by SPB Git
72 + <div class="container footer-inner">
73 + <span class="footer-brand">
74 + <svg width="18" height="18" viewBox="0 0 28 28" aria-hidden="true">
75 + <rect width="28" height="28" rx="7" fill="var(--accent)"/>
76 + <path d="M8 18.5c1 1.4 2.7 2.2 4.6 2.2 2.6 0 4.4-1.3 4.4-3.3 0-1.8-1.2-2.7-3.6-3.3l-1.6-.4c-1.4-.4-2-.9-2-1.8 0-1.1 1-1.8 2.5-1.8 1.4 0 2.5.6 3.2 1.7l1.9-1.3c-1-1.6-2.8-2.5-5-2.5C10 8 8.2 9.4 8.2 11.4c0 1.9 1.2 2.8 3.4 3.3l1.7.4c1.5.4 2.1.9 2.1 1.8 0 1.1-1.1 1.8-2.7 1.8-1.6 0-2.9-.7-3.7-2z" fill="#fff"/>
77 + </svg>
78 + SPB Git
79 + </span>
80 + <span>© {{ owner.name }} · <a href="mailto:{{ owner.email }}">{{ owner.email }}</a></span>
81 + <span class="footer-links">
82 + <a href="/feed.atom">Atom feed</a>
83 + <a href="/api/v1/repos">API</a>
84 + <a href="https://spboucher.ai" rel="noopener">spboucher.ai</a>
85 + </span>
72 86 </div>
73 87 </footer>
88 + <div class="cmdk" id="cmdk" hidden>
89 + <div class="cmdk-backdrop" data-cmdk-close></div>
90 + <div class="cmdk-panel" role="dialog" aria-modal="true" aria-label="Command palette">
91 + <div class="cmdk-input-row">
92 + <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
93 + <circle cx="7" cy="7" r="5"/><path d="m14.5 14.5-3.8-3.8" stroke-linecap="round"/>
94 + </svg>
95 + <input id="cmdk-input" type="text" placeholder="Jump to a repository…" autocomplete="off"
96 + role="combobox" aria-expanded="true" aria-controls="cmdk-list" aria-label="Jump to a repository">
97 + <kbd>esc</kbd>
98 + </div>
99 + <ul class="cmdk-list" id="cmdk-list" role="listbox" aria-label="Repositories"></ul>
100 + <div class="cmdk-footer">
101 + <span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
102 + <span><kbd>↵</kbd> open</span>
103 + <span><kbd>esc</kbd> close</span>
104 + </div>
105 + </div>
106 + </div>
74 107 <script src="/assets/js/app.js" defer></script>
75 108 </body>
76 109 </html>
77 110