@open330/kiwimu 0.4.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/kiwimu CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env bun
2
- await import("../src/index.ts");
2
+ import "../src/index.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open330/kiwimu",
3
- "version": "0.4.0",
3
+ "version": "0.7.1",
4
4
  "description": "Turn textbooks, PDFs, and web content into your own interlinked learning wiki powered by LLM",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "bin/**/*",
11
11
  "src/**/*",
12
+ "personas/**/*",
12
13
  "assets/**/*",
13
14
  "README.md",
14
15
  "LICENSE"
@@ -48,6 +49,7 @@
48
49
  "mammoth": "^1.12.0",
49
50
  "marked": "^15.0.0",
50
51
  "pdf-parse": "1.1.1",
52
+ "sanitize-html": "^2.17.2",
51
53
  "smol-toml": "^1.3.1",
52
54
  "turndown": "^7.2.0"
53
55
  },
@@ -58,6 +60,7 @@
58
60
  "devDependencies": {
59
61
  "@types/bun": "latest",
60
62
  "@types/pdf-parse": "^1.1.5",
63
+ "@types/sanitize-html": "^2.16.1",
61
64
  "@types/turndown": "^5.0.5"
62
65
  }
63
66
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "나무위키",
3
+ "description": "나무위키 특유의 문체와 스타일로 문서를 작성합니다",
4
+ "system_prompt": "당신은 나무위키 스타일의 위키 편집자입니다. 다음 특징을 반드시 지켜주세요:\n\n1. **문체**: 해요체(~입니다/~합니다)를 기본으로 하되, 가끔 반말(~이다/~한다)을 섞어 사용\n2. **유머**: 적절한 곳에 ~~취소선 드립~~, (괄호 안의 부연설명), [1] 각주 스타일의 코멘트를 삽입\n3. **강조**: 중요한 키워드는 **굵게** 처리하고, 핵심 개념은 반복 강조\n4. **서술 톤**: 백과사전적이면서도 친근한 톤. \"~라고 한다\", \"~라고 카더라\" 등의 표현 활용\n5. **구조**: 목차가 잘 정리된 체계적 구조. 소제목을 적극 활용\n6. **부가 정보**: \"여담으로~\", \"참고로~\", \"사실~\" 등의 표현으로 부가 정보 추가\n7. **링크**: 관련 개념에 적극적으로 [[위키 링크]]를 사용\n\n절대 딱딱한 교과서 문체로 쓰지 마세요. 읽는 사람이 재미있게 학습할 수 있도록 작성해주세요.",
5
+ "content_style": "Write in Korean 나무위키 style:\n- Use 해요체 with occasional 반말 mix\n- Add ~~strikethrough humor~~ and (parenthetical asides)\n- Bold **key terms** generously\n- Use phrases like \"~라고 한다\", \"여담으로~\", \"참고로~\"\n- Be encyclopedic yet friendly and entertaining\n- Structure with clear subsections\n- Use [[wiki links]] actively for related concepts"
6
+ }
@@ -1,10 +1,11 @@
1
1
  import { mkdirSync, rmSync, cpSync, existsSync } from "fs";
2
2
  import { join, dirname } from "path";
3
3
  import { marked } from "marked";
4
+ import sanitizeHtml from "sanitize-html";
4
5
  import type { KiwiConfig } from "../config";
5
6
  import type { Store } from "../store";
6
7
  import { buildGraphData } from "../pipeline/graph";
7
- import { renderPage, renderIndex, renderGraph } from "./templates";
8
+ import { renderPage, renderIndex, renderGraph, renderQuizPage } from "./templates";
8
9
 
9
10
  // Fix internal wiki links: /wiki/slug → /wiki/slug.html
10
11
  function fixWikiLinks(html: string): string {
@@ -68,14 +69,28 @@ export async function buildSite(store: Store, config: KiwiConfig, projectRoot: s
68
69
  const sourcePages = store.listSourcePages();
69
70
  const conceptPages = store.listConceptPages();
70
71
  const wikiName = config.project.name;
72
+ const backlinksMap = store.getAllBacklinksGrouped();
71
73
 
72
74
  for (const page of pages) {
73
75
  let htmlContent = await marked(page.content);
76
+ htmlContent = sanitizeHtml(htmlContent, {
77
+ allowedTags: sanitizeHtml.defaults.allowedTags.concat([
78
+ 'img', 'details', 'summary', 'kbd', 'del', 's', 'sup', 'sub',
79
+ 'span', 'div', 'section', 'figure', 'figcaption', 'mark'
80
+ ]),
81
+ allowedAttributes: {
82
+ ...sanitizeHtml.defaults.allowedAttributes,
83
+ '*': ['id', 'class', 'style'],
84
+ 'img': ['src', 'alt', 'title', 'width', 'height'],
85
+ 'a': ['href', 'title', 'target', 'rel'],
86
+ },
87
+ allowedSchemes: ['http', 'https', 'mailto'],
88
+ });
74
89
  htmlContent = fixWikiLinks(htmlContent);
75
90
 
76
91
  const { body, externalRefs } = extractExternalRefs(htmlContent);
77
92
  const toc = generateToc(page.content);
78
- const backlinks = store.getBacklinks(page.id).map((bl) => ({
93
+ const backlinks = (backlinksMap.get(page.id) || []).map((bl) => ({
79
94
  slug: bl.slug,
80
95
  title: bl.title,
81
96
  pageType: bl.page_type,
@@ -116,6 +131,38 @@ export async function buildSite(store: Store, config: KiwiConfig, projectRoot: s
116
131
  })
117
132
  );
118
133
 
134
+ // Quiz page
135
+ const quizzes = store.getAllQuizzes();
136
+ await Bun.write(
137
+ join(outputDir, "quiz.html"),
138
+ renderQuizPage({
139
+ wikiName,
140
+ quizzes: quizzes.map((q) => ({
141
+ id: q.id,
142
+ question: q.question,
143
+ answer: q.answer,
144
+ quiz_type: q.quiz_type,
145
+ page_title: q.page_title,
146
+ page_slug: q.page_slug,
147
+ })),
148
+ sourcePages: sourcePages.map((p) => ({ slug: p.slug, title: p.title })),
149
+ conceptPages: conceptPages.map((p) => ({ slug: p.slug, title: p.title })),
150
+ })
151
+ );
152
+
153
+ // Random page redirect
154
+ mkdirSync(join(wikiDir), { recursive: true });
155
+ await Bun.write(
156
+ join(wikiDir, "random.html"),
157
+ `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>임의 문서</title></head><body><script>
158
+ fetch('/search-index.json').then(r=>r.json()).then(pages=>{
159
+ const p = pages[Math.floor(Math.random()*pages.length)];
160
+ if(p) location.href='/wiki/'+p.slug+'.html';
161
+ else location.href='/';
162
+ });
163
+ </script></body></html>`
164
+ );
165
+
119
166
  const searchData = pages.map((p) => ({
120
167
  slug: p.slug,
121
168
  title: p.title,
@@ -5,6 +5,7 @@ document.addEventListener("DOMContentLoaded", async () => {
5
5
  if (!input || !dropdown) return;
6
6
 
7
7
  let searchData = [];
8
+ let selectedIndex = -1;
8
9
  try {
9
10
  const resp = await fetch("/search-index.json");
10
11
  searchData = await resp.json();
@@ -12,6 +13,12 @@ document.addEventListener("DOMContentLoaded", async () => {
12
13
  return;
13
14
  }
14
15
 
16
+ function escapeHtml(text) {
17
+ const div = document.createElement('div');
18
+ div.textContent = text;
19
+ return div.innerHTML;
20
+ }
21
+
15
22
  function fuzzyMatch(query, text) {
16
23
  query = query.toLowerCase();
17
24
  text = text.toLowerCase();
@@ -31,6 +38,7 @@ document.addEventListener("DOMContentLoaded", async () => {
31
38
  }
32
39
 
33
40
  input.addEventListener("input", () => {
41
+ selectedIndex = -1;
34
42
  const results = search(input.value);
35
43
  if (results.length === 0) {
36
44
  dropdown.classList.remove("active");
@@ -39,8 +47,8 @@ document.addEventListener("DOMContentLoaded", async () => {
39
47
  }
40
48
  dropdown.innerHTML = results.map(r =>
41
49
  `<a href="/wiki/${r.slug}.html">
42
- <strong>${r.title}</strong>
43
- <div style="font-size:12px;color:#6c757d;margin-top:2px;">${r.preview.slice(0, 80)}...</div>
50
+ <strong>${escapeHtml(r.title)}</strong>
51
+ <div style="font-size:12px;color:#6c757d;margin-top:2px;">${escapeHtml(r.preview.slice(0, 80))}...</div>
44
52
  </a>`
45
53
  ).join("");
46
54
  dropdown.classList.add("active");
@@ -61,6 +69,29 @@ document.addEventListener("DOMContentLoaded", async () => {
61
69
  if (e.key === "Escape") {
62
70
  dropdown.classList.remove("active");
63
71
  input.blur();
72
+ } else if (e.key === "ArrowDown") {
73
+ e.preventDefault();
74
+ const items = dropdown.querySelectorAll("a");
75
+ selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
76
+ items.forEach((a, i) => a.classList.toggle("selected", i === selectedIndex));
77
+ items[selectedIndex]?.scrollIntoView({ block: "nearest" });
78
+ } else if (e.key === "ArrowUp") {
79
+ e.preventDefault();
80
+ const items = dropdown.querySelectorAll("a");
81
+ selectedIndex = Math.max(selectedIndex - 1, 0);
82
+ items.forEach((a, i) => a.classList.toggle("selected", i === selectedIndex));
83
+ } else if (e.key === "Enter" && selectedIndex >= 0) {
84
+ e.preventDefault();
85
+ dropdown.querySelectorAll("a")[selectedIndex]?.click();
86
+ }
87
+ });
88
+
89
+ // Global "/" shortcut to focus search
90
+ document.addEventListener('keydown', (e) => {
91
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
92
+ if (e.key === '/') {
93
+ e.preventDefault();
94
+ input.focus();
64
95
  }
65
96
  });
66
97
  });
@@ -6,31 +6,41 @@
6
6
  --bg-hover: #eaeaea;
7
7
  --text: #1a1a1a;
8
8
  --text-muted: #6b6b6b;
9
- --link: #0067a3;
10
- --link-hover: #004b7a;
9
+ --link: #0275d8; /* NamuWiki blue link */
10
+ --link-hover: #0050a0; /* Darker blue for hover */
11
11
  --border: #dcdcdc;
12
- --accent: #00a495;
12
+ --accent: #00a495; /* NamuWiki green */
13
13
  --accent-dark: #008c7e;
14
- --accent-light: #e0f5f3;
14
+ --accent-light: #e0f5f3; /* Light green for backgrounds */
15
15
  --namu-green: #00a495;
16
16
  --namu-green-dark: #007a6e;
17
17
  --namu-heading-bg: #00a495;
18
18
  --sidebar-width: 250px;
19
19
  --topbar-height: 44px;
20
- --radius: 4px;
20
+ /* --radius variable removed as all corners are sharp */
21
21
  }
22
22
 
23
23
  * {
24
24
  margin: 0;
25
25
  padding: 0;
26
26
  box-sizing: border-box;
27
+ box-shadow: none; /* Enforce no shadows */
28
+ }
29
+
30
+ /* Targeted border-radius reset for kiwimu elements only */
31
+ .topbar, .sidebar, .content, .page-card, .toc-box,
32
+ .search-dropdown, .backlinks li a, .stat-card, .quick-link,
33
+ .file-drop, .add-form input, .add-form button,
34
+ .config-card, .badge, .page-type-badge, .page-body table,
35
+ .page-body blockquote, .page-body pre {
36
+ border-radius: 0;
27
37
  }
28
38
 
29
39
  body {
30
40
  font-family: "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Malgun Gothic", sans-serif;
31
41
  color: var(--text);
32
42
  background: var(--bg);
33
- line-height: 1.7;
43
+ line-height: 1.65; /* Updated line-height */
34
44
  font-size: 15px;
35
45
  }
36
46
 
@@ -57,7 +67,7 @@ a:hover {
57
67
  padding: 0 16px;
58
68
  z-index: 100;
59
69
  gap: 12px;
60
- box-shadow: 0 1px 3px rgba(0,0,0,0.15);
70
+ /* box-shadow: none; -> Handled by global reset */
61
71
  }
62
72
 
63
73
  .topbar-brand {
@@ -92,7 +102,7 @@ a:hover {
92
102
  width: 100%;
93
103
  padding: 5px 12px;
94
104
  border: none;
95
- border-radius: 3px;
105
+ /* border-radius: 3px; -> Handled by global reset */
96
106
  font-size: 14px;
97
107
  outline: none;
98
108
  background: rgba(255,255,255,0.9);
@@ -101,7 +111,7 @@ a:hover {
101
111
 
102
112
  .topbar-search input:focus {
103
113
  background: #ffffff;
104
- box-shadow: 0 0 0 2px rgba(255,255,255,0.4);
114
+ /* box-shadow: none; -> Handled by global reset */
105
115
  }
106
116
 
107
117
  .search-dropdown {
@@ -112,8 +122,8 @@ a:hover {
112
122
  right: 0;
113
123
  background: var(--bg);
114
124
  border: 1px solid var(--border);
115
- border-radius: var(--radius);
116
- box-shadow: 0 4px 12px rgba(0,0,0,0.12);
125
+ /* border-radius: var(--radius); -> Handled by global reset */
126
+ /* box-shadow: none; -> Handled by global reset */
117
127
  max-height: 300px;
118
128
  overflow-y: auto;
119
129
  z-index: 200;
@@ -135,6 +145,10 @@ a:hover {
135
145
  text-decoration: none;
136
146
  }
137
147
 
148
+ .search-dropdown a.selected {
149
+ background: var(--accent-light);
150
+ }
151
+
138
152
  .search-dropdown .search-highlight {
139
153
  color: var(--namu-green);
140
154
  font-weight: 600;
@@ -149,7 +163,7 @@ a:hover {
149
163
  .btn-graph {
150
164
  padding: 4px 10px;
151
165
  border: 1px solid rgba(255,255,255,0.3);
152
- border-radius: 3px;
166
+ /* border-radius: 3px; -> Handled by global reset */
153
167
  font-size: 13px;
154
168
  color: #ffffff;
155
169
  white-space: nowrap;
@@ -238,7 +252,7 @@ a:hover {
238
252
  }
239
253
 
240
254
  .page-body {
241
- line-height: 1.8;
255
+ /* line-height: 1.8; -> Removed to inherit from body's 1.65 */
242
256
  }
243
257
 
244
258
  .page-body h2 {
@@ -248,7 +262,7 @@ a:hover {
248
262
  padding: 4px 10px;
249
263
  background: var(--namu-green);
250
264
  color: #ffffff;
251
- border-radius: 2px;
265
+ /* border-radius: 2px; -> Handled by global reset */
252
266
  }
253
267
 
254
268
  .page-body h3 {
@@ -257,7 +271,7 @@ a:hover {
257
271
  margin: 22px 0 8px;
258
272
  padding: 3px 8px;
259
273
  border-left: 4px solid var(--namu-green);
260
- background: var(--accent-light);
274
+ background: none; /* Removed background */
261
275
  color: var(--text);
262
276
  }
263
277
 
@@ -293,14 +307,14 @@ a:hover {
293
307
  padding: 8px 14px;
294
308
  margin: 10px 0;
295
309
  background: #f0faf9;
296
- border-radius: 0 var(--radius) var(--radius) 0;
310
+ /* border-radius: 0 var(--radius) var(--radius) 0; -> Handled by global reset */
297
311
  font-size: 14px;
298
312
  }
299
313
 
300
314
  .page-body code {
301
315
  background: #f0f0f0;
302
316
  padding: 2px 5px;
303
- border-radius: 2px;
317
+ /* border-radius: 2px; -> Handled by global reset */
304
318
  font-size: 13px;
305
319
  font-family: "JetBrains Mono", "D2Coding", monospace;
306
320
  }
@@ -309,7 +323,7 @@ a:hover {
309
323
  background: #2b2b2b;
310
324
  color: #e0e0e0;
311
325
  padding: 14px;
312
- border-radius: var(--radius);
326
+ /* border-radius: var(--radius); -> Handled by global reset */
313
327
  overflow-x: auto;
314
328
  margin: 10px 0;
315
329
  }
@@ -322,7 +336,7 @@ a:hover {
322
336
 
323
337
  .page-body img {
324
338
  max-width: 100%;
325
- border-radius: var(--radius);
339
+ /* border-radius: var(--radius); -> Handled by global reset */
326
340
  margin: 10px 0;
327
341
  }
328
342
 
@@ -351,11 +365,11 @@ a:hover {
351
365
  font-weight: 800;
352
366
  }
353
367
 
354
- /* TOC - namuwiki style */
368
+ /* TOC - namuwiki style with counters */
355
369
  .toc-box {
356
370
  background: #f5f5f5;
357
371
  border: 1px solid var(--border);
358
- border-radius: var(--radius);
372
+ /* border-radius: var(--radius); -> Handled by global reset */
359
373
  padding: 10px 14px;
360
374
  margin-bottom: 20px;
361
375
  }
@@ -367,29 +381,81 @@ a:hover {
367
381
  color: var(--namu-green-dark);
368
382
  }
369
383
 
370
- .toc-box .toc ul {
384
+ .toc-box .toc {
385
+ counter-reset: h2; /* Reset counter for top-level headings */
386
+ }
387
+
388
+ .toc-box .toc ul { /* Use ul as the base, then li for items */
371
389
  list-style: none;
372
390
  margin: 6px 0 0 0;
373
391
  padding-left: 0;
374
392
  }
375
393
 
376
- .toc-box .toc ul ul {
377
- padding-left: 16px;
394
+ .toc-box .toc ul ul { /* Nested lists */
395
+ padding-left: 18px; /* Indent for sub-levels */
378
396
  }
379
397
 
380
398
  .toc-box .toc li {
381
- margin: 1px 0;
399
+ margin: 2px 0; /* Slightly more space */
400
+ position: relative; /* For counter positioning */
382
401
  }
383
402
 
384
- .toc-box .toc a {
403
+ .toc-box .toc li a {
385
404
  font-size: 13px;
386
405
  color: var(--link);
406
+ display: block; /* Make link clickable over full line */
407
+ padding-left: 20px; /* Space for counter */
387
408
  }
388
409
 
389
- .toc-box .toc a:hover {
410
+ .toc-box .toc li a:hover {
390
411
  color: var(--link-hover);
391
412
  }
392
413
 
414
+ /* Counter for h2 equivalent */
415
+ .toc-box .toc > ul > li {
416
+ counter-increment: h2;
417
+ counter-reset: h3; /* Reset h3 counter for each new h2 */
418
+ }
419
+ .toc-box .toc > ul > li > a::before {
420
+ content: counter(h2) ". ";
421
+ position: absolute;
422
+ left: 0;
423
+ color: var(--text-muted);
424
+ font-weight: 400;
425
+ width: 20px; /* Fixed width for alignment */
426
+ text-align: right;
427
+ }
428
+
429
+ /* Counter for h3 equivalent */
430
+ .toc-box .toc > ul > li > ul > li {
431
+ counter-increment: h3;
432
+ counter-reset: h4; /* Reset h4 counter for each new h3 */
433
+ }
434
+ .toc-box .toc > ul > li > ul > li > a::before {
435
+ content: counter(h2) "." counter(h3) ". ";
436
+ position: absolute;
437
+ left: 0;
438
+ color: var(--text-muted);
439
+ font-weight: 400;
440
+ width: 20px;
441
+ text-align: right;
442
+ }
443
+
444
+ /* Counter for h4 equivalent */
445
+ .toc-box .toc > ul > li > ul > li > ul > li {
446
+ counter-increment: h4;
447
+ }
448
+ .toc-box .toc > ul > li > ul > li > ul > li > a::before {
449
+ content: counter(h2) "." counter(h3) "." counter(h4) ". ";
450
+ position: absolute;
451
+ left: 0;
452
+ color: var(--text-muted);
453
+ font-weight: 400;
454
+ width: 20px;
455
+ text-align: right;
456
+ }
457
+
458
+
393
459
  /* Backlinks */
394
460
  .backlinks {
395
461
  margin-top: 36px;
@@ -416,7 +482,7 @@ a:hover {
416
482
  padding: 3px 10px;
417
483
  background: var(--accent-light);
418
484
  border: 1px solid var(--namu-green);
419
- border-radius: 3px;
485
+ /* border-radius: 3px; -> Handled by global reset */
420
486
  font-size: 13px;
421
487
  color: var(--namu-green-dark);
422
488
  }
@@ -463,7 +529,7 @@ a:hover {
463
529
  padding: 4px 10px;
464
530
  background: var(--namu-green);
465
531
  color: #ffffff;
466
- border-radius: 2px;
532
+ /* border-radius: 2px; -> Handled by global reset */
467
533
  }
468
534
 
469
535
  .page-cards {
@@ -473,12 +539,22 @@ a:hover {
473
539
  margin-bottom: 28px;
474
540
  }
475
541
 
542
+ .empty-state {
543
+ grid-column: 1 / -1;
544
+ padding: 32px;
545
+ text-align: center;
546
+ color: var(--text-muted);
547
+ font-size: 14px;
548
+ border: 2px dashed var(--border);
549
+ margin: 8px 0;
550
+ }
551
+
476
552
  .page-card {
477
553
  display: block;
478
554
  padding: 10px 14px;
479
555
  background: var(--bg);
480
556
  border: 1px solid var(--border);
481
- border-radius: var(--radius);
557
+ /* border-radius: var(--radius); -> Handled by global reset */
482
558
  color: var(--text);
483
559
  transition: all 0.15s;
484
560
  }
@@ -488,7 +564,7 @@ a:hover {
488
564
  border-color: var(--namu-green);
489
565
  text-decoration: none;
490
566
  transform: translateY(-1px);
491
- box-shadow: 0 2px 6px rgba(0,164,149,0.15);
567
+ /* box-shadow: none; -> Handled by global reset */
492
568
  }
493
569
 
494
570
  .card-title {
@@ -506,7 +582,7 @@ a:hover {
506
582
  padding: 10px 18px;
507
583
  background: var(--accent-light);
508
584
  border: 1px solid var(--namu-green);
509
- border-radius: var(--radius);
585
+ /* border-radius: var(--radius); -> Handled by global reset */
510
586
  font-size: 14px;
511
587
  color: var(--namu-green-dark);
512
588
  font-weight: 600;
@@ -539,7 +615,7 @@ a:hover {
539
615
  width: 100%;
540
616
  height: calc(100vh - 200px);
541
617
  border: 1px solid var(--border);
542
- border-radius: var(--radius);
618
+ /* border-radius: var(--radius); -> Handled by global reset */
543
619
  background: #fafafa;
544
620
  }
545
621
 
@@ -577,7 +653,7 @@ h4 .headerlink {
577
653
  font-size: 11px;
578
654
  font-weight: 700;
579
655
  padding: 2px 8px;
580
- border-radius: 2px;
656
+ /* border-radius: 2px; -> Handled by global reset */
581
657
  margin-bottom: 6px;
582
658
  letter-spacing: 0.03em;
583
659
  }
@@ -656,7 +732,7 @@ h4 .headerlink {
656
732
  padding: 14px;
657
733
  background: #f0f7ff;
658
734
  border: 1px solid #bdd7ee;
659
- border-radius: var(--radius);
735
+ /* border-radius: var(--radius); -> Handled by global reset */
660
736
  }
661
737
 
662
738
  .external-refs h3 {
@@ -678,7 +754,7 @@ h4 .headerlink {
678
754
 
679
755
  .external-refs a {
680
756
  font-size: 13px;
681
- color: #1565c0;
757
+ color: #1565c0; /* Specific blue for external refs */
682
758
  }
683
759
 
684
760
  /* Backlink type indicators */
@@ -715,7 +791,7 @@ h4 .headerlink {
715
791
  padding: 5px 14px;
716
792
  border: 1px solid var(--border);
717
793
  background: var(--bg-alt);
718
- border-radius: var(--radius);
794
+ /* border-radius: var(--radius); -> Handled by global reset */
719
795
  font-size: 13px;
720
796
  cursor: pointer;
721
797
  transition: all 0.15s;
@@ -744,7 +820,7 @@ h4 .headerlink {
744
820
  justify-content: center;
745
821
  padding: 18px;
746
822
  border: 2px dashed var(--border);
747
- border-radius: var(--radius);
823
+ /* border-radius: var(--radius); -> Handled by global reset */
748
824
  cursor: pointer;
749
825
  transition: all 0.15s;
750
826
  text-align: center;
@@ -778,7 +854,7 @@ h4 .headerlink {
778
854
  flex: 1;
779
855
  padding: 8px 12px;
780
856
  border: 1px solid var(--border);
781
- border-radius: var(--radius);
857
+ /* border-radius: var(--radius); -> Handled by global reset */
782
858
  font-size: 14px;
783
859
  outline: none;
784
860
  transition: border-color 0.2s;
@@ -786,7 +862,7 @@ h4 .headerlink {
786
862
 
787
863
  .add-form input:focus {
788
864
  border-color: var(--namu-green);
789
- box-shadow: 0 0 0 2px var(--accent-light);
865
+ /* box-shadow: none; -> Handled by global reset */
790
866
  }
791
867
 
792
868
  .add-form button {
@@ -794,7 +870,7 @@ h4 .headerlink {
794
870
  background: var(--namu-green);
795
871
  color: white;
796
872
  border: none;
797
- border-radius: var(--radius);
873
+ /* border-radius: var(--radius); -> Handled by global reset */
798
874
  font-size: 14px;
799
875
  font-weight: 600;
800
876
  cursor: pointer;
@@ -813,7 +889,7 @@ h4 .headerlink {
813
889
 
814
890
  .add-status {
815
891
  padding: 8px 12px;
816
- border-radius: var(--radius);
892
+ /* border-radius: var(--radius); -> Handled by global reset */
817
893
  font-size: 14px;
818
894
  }
819
895
 
@@ -849,7 +925,7 @@ h4 .headerlink {
849
925
  padding: 10px 14px;
850
926
  background: var(--bg-alt);
851
927
  border: 1px solid var(--border);
852
- border-radius: var(--radius);
928
+ /* border-radius: var(--radius); -> Handled by global reset */
853
929
  text-align: center;
854
930
  }
855
931
 
@@ -879,7 +955,7 @@ h4 .headerlink {
879
955
  display: inline-block;
880
956
  width: 12px;
881
957
  height: 12px;
882
- border-radius: 50%;
958
+ border-radius: 50%; /* Keep for circular dot */
883
959
  vertical-align: middle;
884
960
  margin-right: 4px;
885
961
  }
@@ -892,11 +968,49 @@ h4 .headerlink {
892
968
  background: var(--namu-green);
893
969
  }
894
970
 
971
+ /* Hamburger menu button */
972
+ .topbar-menu-btn {
973
+ display: none;
974
+ background: none;
975
+ border: none;
976
+ color: white;
977
+ font-size: 22px;
978
+ cursor: pointer;
979
+ padding: 0 8px;
980
+ }
981
+
982
+ /* Sidebar overlay */
983
+ .sidebar-overlay {
984
+ display: none;
985
+ }
986
+
895
987
  /* Responsive */
896
988
  @media (max-width: 768px) {
989
+ .topbar-menu-btn { display: flex; align-items: center; }
990
+
897
991
  .sidebar {
992
+ position: fixed;
993
+ left: -260px;
994
+ top: 46px;
995
+ bottom: 0;
996
+ width: 260px;
997
+ z-index: 99;
998
+ transition: left 0.25s ease;
999
+ background: var(--bg);
1000
+ display: block !important;
1001
+ }
1002
+ .sidebar.open { left: 0; }
1003
+
1004
+ .sidebar-overlay {
898
1005
  display: none;
1006
+ position: fixed;
1007
+ inset: 0;
1008
+ top: 46px;
1009
+ background: rgba(0,0,0,0.4);
1010
+ z-index: 98;
899
1011
  }
1012
+ .sidebar-overlay.active { display: block; }
1013
+
900
1014
  .content {
901
1015
  margin-left: 0;
902
1016
  padding: 16px 14px;
@@ -905,3 +1019,26 @@ h4 .headerlink {
905
1019
  max-width: 180px;
906
1020
  }
907
1021
  }
1022
+
1023
+ /* Dark mode */
1024
+ @media (prefers-color-scheme: dark) {
1025
+ :root {
1026
+ --bg: #1a1a2e;
1027
+ --bg-alt: #16213e;
1028
+ --bg-hover: #0f3460;
1029
+ --text: #e0e0e0;
1030
+ --text-muted: #8e8e8e;
1031
+ --text-dark: #ffffff;
1032
+ --border: #2a2a4a;
1033
+ --accent-light: #1a3a3a;
1034
+ --namu-green: #00b4a6;
1035
+ --namu-green-dark: #008c7e;
1036
+ }
1037
+
1038
+ .topbar { background: #004d40; }
1039
+ .page-body h2 { background: #006050; }
1040
+ .page-card { background: var(--bg-alt); }
1041
+ .toc-box { background: var(--bg-alt); }
1042
+ .sidebar { background: var(--bg); border-right-color: var(--border); }
1043
+ img { opacity: 0.9; }
1044
+ }