camelmind 0.1.0

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.
Files changed (73) hide show
  1. package/dist/index.js +142 -0
  2. package/package.json +51 -0
  3. package/template/_env.example +16 -0
  4. package/template/_gitignore +32 -0
  5. package/template/_package.json +39 -0
  6. package/template/app/[...slug]/page.tsx +150 -0
  7. package/template/app/api/auth/callback/route.ts +47 -0
  8. package/template/app/api/auth/login/route.ts +39 -0
  9. package/template/app/api/auth/logout/route.ts +8 -0
  10. package/template/app/api/auth/signin/route.ts +64 -0
  11. package/template/app/api/download/route.ts +67 -0
  12. package/template/app/api/raw/route.ts +43 -0
  13. package/template/app/api/search/route.ts +138 -0
  14. package/template/app/favicon.ico +0 -0
  15. package/template/app/globals.css +524 -0
  16. package/template/app/home/page.tsx +136 -0
  17. package/template/app/icon.svg +29 -0
  18. package/template/app/layout.tsx +44 -0
  19. package/template/app/login/LoginForm.tsx +105 -0
  20. package/template/app/login/page.tsx +9 -0
  21. package/template/app/page.tsx +5 -0
  22. package/template/camelmind.config.ts +33 -0
  23. package/template/components/Breadcrumbs/Breadcrumbs.tsx +41 -0
  24. package/template/components/Code/CodeBlock.tsx +55 -0
  25. package/template/components/DocActions/DocActions.tsx +62 -0
  26. package/template/components/Nav/MobileDrawer.tsx +221 -0
  27. package/template/components/Nav/ThemeToggle.tsx +62 -0
  28. package/template/components/Nav/TopNav.tsx +173 -0
  29. package/template/components/Nav/VersionSelector.tsx +107 -0
  30. package/template/components/PageNav/PageNav.tsx +68 -0
  31. package/template/components/Search/SearchModal.tsx +278 -0
  32. package/template/components/SectionCards/SectionCards.tsx +33 -0
  33. package/template/components/Sidebar/Sidebar.tsx +196 -0
  34. package/template/components/Toc/Toc.tsx +57 -0
  35. package/template/components/ZoomImages/ZoomImages.tsx +24 -0
  36. package/template/components/mdx/Callout.tsx +43 -0
  37. package/template/components/mdx/Details.tsx +65 -0
  38. package/template/components/mdx/Icon.tsx +25 -0
  39. package/template/components/mdx/Steps.tsx +33 -0
  40. package/template/components/mdx/Tabs.tsx +69 -0
  41. package/template/components/mdx/index.tsx +34 -0
  42. package/template/content/getting-started/installation.mdx +51 -0
  43. package/template/content/getting-started/overview.mdx +39 -0
  44. package/template/content/guides/writing-docs.mdx +72 -0
  45. package/template/eslint.config.mjs +18 -0
  46. package/template/lib/auth-providers/dev-mock.ts +45 -0
  47. package/template/lib/auth-providers/oidc.ts +95 -0
  48. package/template/lib/auth-roles.ts +28 -0
  49. package/template/lib/auth.ts +76 -0
  50. package/template/lib/config-types.ts +30 -0
  51. package/template/lib/config.ts +29 -0
  52. package/template/lib/mdx.ts +53 -0
  53. package/template/lib/nav-types.ts +31 -0
  54. package/template/lib/nav.ts +125 -0
  55. package/template/lib/versions.ts +61 -0
  56. package/template/nav/nav.yml +20 -0
  57. package/template/next.config.ts +35 -0
  58. package/template/postcss.config.mjs +7 -0
  59. package/template/proxy.ts +50 -0
  60. package/template/public/2f-logo.png +0 -0
  61. package/template/public/favicon.png +0 -0
  62. package/template/public/file.svg +1 -0
  63. package/template/public/globe.svg +1 -0
  64. package/template/public/images/gwa-app-central-main.png +0 -0
  65. package/template/public/next.svg +1 -0
  66. package/template/public/window.svg +1 -0
  67. package/template/scripts/build-offline.sh +189 -0
  68. package/template/scripts/build-pdf.sh +41 -0
  69. package/template/scripts/build-search-index.ts +90 -0
  70. package/template/scripts/generate-master-pdf.ts +260 -0
  71. package/template/scripts/generate-pdfs.ts +185 -0
  72. package/template/tsconfig.json +34 -0
  73. package/template/versions.yml +5 -0
@@ -0,0 +1,43 @@
1
+ import { NextRequest, NextResponse } from "next/server"
2
+ import fs from "fs"
3
+ import path from "path"
4
+ import { getSession } from "@/lib/auth"
5
+ import { isAuthEnabled } from "@/lib/config"
6
+
7
+ export async function GET(req: NextRequest) {
8
+ const session = await getSession()
9
+ if (isAuthEnabled() && !session) {
10
+ return new NextResponse("Unauthorized", { status: 401 })
11
+ }
12
+
13
+ const file = req.nextUrl.searchParams.get("file")
14
+ const download = req.nextUrl.searchParams.get("download") === "1"
15
+
16
+ if (!file) {
17
+ return new NextResponse("Missing file param", { status: 400 })
18
+ }
19
+
20
+ // Only allow reading from the content directory
21
+ const resolved = path.resolve(process.cwd(), file)
22
+ const contentRoot = path.resolve(process.cwd(), "content")
23
+ if (!resolved.startsWith(contentRoot)) {
24
+ return new NextResponse("Forbidden", { status: 403 })
25
+ }
26
+
27
+ if (!fs.existsSync(resolved)) {
28
+ return new NextResponse("Not found", { status: 404 })
29
+ }
30
+
31
+ const raw = fs.readFileSync(resolved, "utf-8")
32
+ const filename = path.basename(resolved).replace(/\.mdx?$/, ".md")
33
+
34
+ const headers: Record<string, string> = {
35
+ "Content-Type": "text/plain; charset=utf-8",
36
+ }
37
+
38
+ if (download) {
39
+ headers["Content-Disposition"] = `attachment; filename="${filename}"`
40
+ }
41
+
42
+ return new NextResponse(raw, { headers })
43
+ }
@@ -0,0 +1,138 @@
1
+ import { NextRequest, NextResponse } from "next/server"
2
+ import { loadNav, getAllSlugs, getNavEntryBySlug, getSectionForSlug } from "@/lib/nav"
3
+ import { loadMdxFile } from "@/lib/mdx"
4
+ import { getSession, hasAccess } from "@/lib/auth"
5
+ import { isAuthEnabled } from "@/lib/config"
6
+ import type { NavGroup } from "@/lib/nav-types"
7
+
8
+ type SearchIndex = {
9
+ slug: string
10
+ title: string
11
+ description?: string
12
+ body: string
13
+ group?: string // top-level nav group (Getting Started, Platform & Security…)
14
+ section?: string // section label (Authorization Path, Entrance Criteria…)
15
+ roles: string[]
16
+ }
17
+
18
+ let _index: SearchIndex[] | null = null
19
+
20
+ function buildIndex(): SearchIndex[] {
21
+ if (_index) return _index
22
+
23
+ const nav = loadNav()
24
+ const slugs = getAllSlugs()
25
+ const index: SearchIndex[] = []
26
+
27
+ const groupMap = new Map<string, string>()
28
+ for (const item of nav.nav) {
29
+ if ("dropdown" in item) {
30
+ const group = item as NavGroup
31
+ for (const entry of group.items) {
32
+ groupMap.set(entry.slug, group.label)
33
+ if (entry.section) {
34
+ for (const child of entry.section) {
35
+ groupMap.set(child.slug, group.label)
36
+ if (child.children) {
37
+ for (const grandchild of child.children) {
38
+ groupMap.set(grandchild.slug, group.label)
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ for (const slug of slugs) {
48
+ const entry = getNavEntryBySlug(slug)
49
+ if (!entry || !entry.file) continue
50
+
51
+ try {
52
+ const { frontmatter, source } = loadMdxFile(entry.file)
53
+ const body = source
54
+ .replace(/<[^>]+>/g, " ")
55
+ .replace(/[#*`>|]/g, " ")
56
+ .replace(/\s+/g, " ")
57
+ .trim()
58
+
59
+ const sectionEntry = getSectionForSlug(slug)
60
+
61
+ index.push({
62
+ slug,
63
+ title: frontmatter.title,
64
+ description: frontmatter.description,
65
+ body,
66
+ group: groupMap.get(slug),
67
+ section: sectionEntry?.label,
68
+ roles: entry.roles ?? [],
69
+ })
70
+ } catch {
71
+ // skip missing files
72
+ }
73
+ }
74
+
75
+ _index = index
76
+ return index
77
+ }
78
+
79
+ export async function GET(req: NextRequest) {
80
+ const session = await getSession()
81
+ const authEnabled = isAuthEnabled()
82
+
83
+ if (authEnabled && !session) {
84
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
85
+ }
86
+
87
+ const userRoles = session?.roles ?? []
88
+ const q = req.nextUrl.searchParams.get("q")?.toLowerCase().trim() ?? ""
89
+ const filterGroup = req.nextUrl.searchParams.get("group") ?? ""
90
+ const filterRole = req.nextUrl.searchParams.get("role") ?? ""
91
+
92
+ const index = buildIndex()
93
+ const accessible = authEnabled
94
+ ? index.filter((doc) => hasAccess(doc.roles, userRoles))
95
+ : index
96
+ if (!q) {
97
+ const groups = [...new Set(accessible.map((d) => d.group).filter(Boolean))]
98
+ return NextResponse.json({ results: [], groups })
99
+ }
100
+
101
+ const terms = q.split(/\s+/).filter(Boolean)
102
+
103
+ const scored = accessible
104
+ .filter((doc) => {
105
+ if (filterGroup && doc.group !== filterGroup) return false
106
+ if (filterRole && !doc.roles.includes(filterRole) && doc.roles.length > 0) return false
107
+ return true
108
+ })
109
+ .map((doc) => {
110
+ const titleLower = doc.title.toLowerCase()
111
+ const descLower = (doc.description ?? "").toLowerCase()
112
+ const bodyLower = doc.body.toLowerCase()
113
+
114
+ let score = 0
115
+ for (const term of terms) {
116
+ if (titleLower.includes(term)) score += 10
117
+ if (descLower.includes(term)) score += 5
118
+ if (bodyLower.includes(term)) score += 1
119
+ }
120
+
121
+ let excerpt = doc.description ?? ""
122
+ if (!excerpt) {
123
+ const idx = bodyLower.indexOf(terms[0])
124
+ if (idx >= 0) {
125
+ excerpt = doc.body.slice(Math.max(0, idx - 30), idx + 100).trim()
126
+ }
127
+ }
128
+
129
+ return { ...doc, score, excerpt }
130
+ })
131
+ .filter((d) => d.score > 0)
132
+ .sort((a, b) => b.score - a.score)
133
+ .slice(0, 10)
134
+ .map(({ body: _body, score: _score, ...rest }) => rest)
135
+
136
+ const groups = [...new Set(accessible.map((d) => d.group).filter(Boolean))]
137
+ return NextResponse.json({ results: scored, groups })
138
+ }
Binary file
@@ -0,0 +1,524 @@
1
+ @import "tailwindcss";
2
+ @plugin "@tailwindcss/typography";
3
+
4
+ /* Tailwind v4 dark mode — toggled by .dark class on <html> */
5
+ @custom-variant dark (&:where(.dark, .dark *));
6
+
7
+ /* ── CamelMind design tokens (Sahara theme) ── */
8
+ :root {
9
+ --cm-camel-gold: #D8A15B;
10
+ --cm-sandstone: #F4D6A7;
11
+ --cm-oasis-teal: #4BA8B7;
12
+ --cm-caravan-brown: #6A4B33;
13
+ --cm-night-dune: #1E2833;
14
+ --cm-parchment: #FFF6E9;
15
+ --cm-dune-beige: #E7D7BF;
16
+ --cm-palm-green: #8FA65C;
17
+ --cm-sunset-gold: #E8B34E;
18
+ --cm-shadow-brown: #4B3424;
19
+ --cm-border-sand: #E6DCCB;
20
+
21
+ --cm-primary: var(--cm-oasis-teal);
22
+ --cm-success: #5B9B62;
23
+ --cm-warning: #D9A441;
24
+ --cm-danger: #C76B56;
25
+
26
+ --cm-bg-primary: var(--cm-parchment);
27
+ --cm-bg-secondary: #FCF8F1;
28
+ --cm-bg-tertiary: #F7F1E7;
29
+ --cm-text-primary: var(--cm-caravan-brown);
30
+ --cm-text-secondary: #7A624F;
31
+ --cm-text-muted: #9B8876;
32
+ --cm-border: var(--cm-border-sand);
33
+ --cm-link: var(--cm-oasis-teal);
34
+ --cm-link-hover: #388A97;
35
+
36
+ --cm-btn-primary-bg: var(--cm-oasis-teal);
37
+ --cm-btn-primary-text: #FFFFFF;
38
+
39
+ --cm-note-bg: #EAF7FA;
40
+ --cm-note-border: #4BA8B7;
41
+ --cm-tip-bg: #EEF7EC;
42
+ --cm-tip-border: #5B9B62;
43
+ --cm-warning-bg: #FFF6E2;
44
+ --cm-warning-border: #D9A441;
45
+ --cm-danger-bg: #FFF0EC;
46
+ --cm-danger-border: #C76B56;
47
+
48
+ --cm-radius-md: 10px;
49
+ --cm-font-heading: var(--font-alegreya), Georgia, serif;
50
+ --cm-font-body: var(--font-inter), system-ui, sans-serif;
51
+ --cm-font-mono: var(--font-jetbrains-mono), monospace;
52
+
53
+ --cm-active: var(--cm-oasis-teal);
54
+ --cm-active-border: var(--cm-oasis-teal);
55
+ }
56
+
57
+ .dark {
58
+ --cm-bg-primary: #1E2833;
59
+ --cm-bg-secondary: #26323F;
60
+ --cm-bg-tertiary: #2D3B4A;
61
+ --cm-text-primary: #F8F2E8;
62
+ --cm-text-secondary: #D7C8B4;
63
+ --cm-text-muted: #B8A996;
64
+ --cm-border: #3B4A5A;
65
+ --cm-link: #63C1CF;
66
+ --cm-link-hover: #7AD2DE;
67
+ --cm-active: #63C1CF;
68
+ --cm-active-border: #63C1CF;
69
+ }
70
+
71
+ .cm-primary-button {
72
+ background: var(--cm-btn-primary-bg);
73
+ color: var(--cm-btn-primary-text);
74
+ border-radius: var(--cm-radius-md);
75
+ }
76
+
77
+ /* ── Callout / Admonition styles ── */
78
+
79
+ .callout {
80
+ margin-bottom: 1.25rem;
81
+ border-radius: 8px;
82
+ font-size: 1rem;
83
+ background-color: #fdfdfd;
84
+ border: 1.5px solid #bbbbbb;
85
+ overflow: hidden;
86
+ }
87
+
88
+ .callout-header {
89
+ border-radius: 8px 8px 0 0;
90
+ font-weight: 700;
91
+ font-size: 0.85rem;
92
+ padding: 0.4rem 0.75rem;
93
+ text-transform: uppercase;
94
+ letter-spacing: 0.04em;
95
+ color: #1f1f1f;
96
+ }
97
+
98
+ .callout-body {
99
+ padding: 0.75rem;
100
+ font-size: 0.95rem;
101
+ color: #333;
102
+ }
103
+
104
+ .callout.note { border-color: var(--cm-note-border); }
105
+ .callout.note .callout-header { background-color: var(--cm-note-bg); color: var(--cm-text-primary); }
106
+
107
+ .callout.tip { border-color: var(--cm-tip-border); }
108
+ .callout.tip .callout-header { background-color: var(--cm-tip-bg); color: var(--cm-text-primary); }
109
+
110
+ .callout.warning { border-color: var(--cm-warning-border); }
111
+ .callout.warning .callout-header { background-color: var(--cm-warning-bg); color: var(--cm-text-primary); }
112
+
113
+ .callout.important { border-color: var(--cm-oasis-teal); }
114
+ .callout.important .callout-header { background-color: var(--cm-note-bg); color: var(--cm-text-primary); }
115
+
116
+ .callout.danger { border-color: var(--cm-danger-border); }
117
+ .callout.danger .callout-header { background-color: var(--cm-danger-bg); color: var(--cm-text-primary); }
118
+
119
+ .callout.success { border-color: var(--cm-tip-border); }
120
+ .callout.success .callout-header { background-color: var(--cm-tip-bg); color: var(--cm-text-primary); }
121
+
122
+ body {
123
+ font-family: var(--cm-font-body);
124
+ }
125
+
126
+ /* ── Prose refinements ── */
127
+
128
+ /* Tighter max-width for better line length */
129
+ .prose {
130
+ max-width: 680px;
131
+ }
132
+
133
+ /* Alternating table rows — no cell borders */
134
+ .prose table {
135
+ border-collapse: collapse;
136
+ width: 100%;
137
+ }
138
+ .prose thead th {
139
+ background-color: #f4f4f4;
140
+ border-bottom: 2px solid #e0e0e0;
141
+ padding: 0.55rem 0.85rem;
142
+ text-align: left;
143
+ font-size: 0.85rem;
144
+ font-weight: 600;
145
+ color: #333;
146
+ }
147
+ .prose tbody td {
148
+ padding: 0.55rem 0.85rem;
149
+ border: none;
150
+ font-size: 0.9rem;
151
+ vertical-align: top;
152
+ }
153
+ .prose tbody tr:nth-child(even) td {
154
+ background-color: #f9f9f9;
155
+ }
156
+ .prose tbody tr:nth-child(odd) td {
157
+ background-color: #ffffff;
158
+ }
159
+
160
+ /* Remove underlines from heading anchor links */
161
+ .prose h1 a,
162
+ .prose h2 a,
163
+ .prose h3 a,
164
+ .prose h4 a,
165
+ .prose h5 a,
166
+ .prose h6 a {
167
+ text-decoration: none;
168
+ }
169
+
170
+ /* Strip background and restore color for code inside pre (code blocks) */
171
+ .prose pre code {
172
+ background: none;
173
+ padding: 0;
174
+ border-radius: 0;
175
+ font-size: inherit;
176
+ color: #e2e8f0;
177
+ }
178
+
179
+ /* Inline code — covers both backtick syntax and raw <code> tags in MDX */
180
+ .prose code {
181
+ background-color: #f3f4f6;
182
+ color: #1f2937;
183
+ padding: 0.15rem 0.375rem;
184
+ border-radius: 0.25rem;
185
+ font-size: 0.875rem;
186
+ font-weight: normal;
187
+ font-family: ui-monospace, monospace;
188
+ }
189
+
190
+ /* Remove backtick pseudo-elements added by @tailwindcss/typography */
191
+ @layer utilities {
192
+ .prose code::before,
193
+ .prose code::after {
194
+ content: none;
195
+ }
196
+ }
197
+
198
+ /* Softer code block background */
199
+ .prose pre, pre {
200
+ background-color: #1a1d23 !important;
201
+ border-radius: 8px;
202
+ }
203
+
204
+ /* Timeline container */
205
+ .timeline {
206
+ display: flex;
207
+ flex-direction: column; /* stack vertically for better responsiveness */
208
+ justify-content: flex-start;
209
+ align-items: flex-start;
210
+ padding-top: 1.5rem;
211
+ }
212
+
213
+ /* Wrapper for all timeline steps */
214
+ .timeline-content {
215
+ width: 100%;
216
+ position: relative;
217
+ padding-left: 1.5rem;
218
+ }
219
+
220
+ /* Individual timeline item */
221
+ .timeline-item {
222
+ margin-bottom: 2.5rem;
223
+ position: relative;
224
+ padding-left: 2rem;
225
+ max-width: 100%;
226
+ }
227
+
228
+ /* Numbered circle — used by raw HTML divs in MDX */
229
+ .timeline-item::before {
230
+ content: attr(data-step);
231
+ position: absolute;
232
+ left: -2rem;
233
+ top: 0;
234
+ background-color: #636363;
235
+ color: white;
236
+ width: 30px;
237
+ height: 30px;
238
+ display: flex;
239
+ justify-content: center;
240
+ align-items: center;
241
+ border-radius: 50%;
242
+ font-weight: bold;
243
+ font-size: 16px;
244
+ z-index: 1;
245
+ }
246
+
247
+ /* When a real <a> renders the circle (Step component), suppress ::before */
248
+ .timeline-item:has(.timeline-step-anchor:not(:empty))::before {
249
+ display: none;
250
+ }
251
+
252
+ /* Anchor link that IS the circle — same geometry as ::before */
253
+ .timeline-step-anchor {
254
+ position: absolute;
255
+ left: -2rem;
256
+ top: 0;
257
+ background-color: #636363;
258
+ color: white;
259
+ width: 30px;
260
+ height: 30px;
261
+ display: flex;
262
+ justify-content: center;
263
+ align-items: center;
264
+ border-radius: 50%;
265
+ font-weight: bold;
266
+ font-size: 16px;
267
+ z-index: 1;
268
+ text-decoration: none;
269
+ transition: background-color 0.15s;
270
+ }
271
+
272
+ .timeline-step-anchor:hover {
273
+ background-color: #444;
274
+ }
275
+
276
+ /* Dotted vertical line */
277
+ .timeline-item:not(.last-item)::after {
278
+ content: '';
279
+ position: absolute;
280
+ top: 0;
281
+ left: -1.25rem;
282
+ width: 2px;
283
+ height: calc(100% + 40px);
284
+ background-color: transparent;
285
+ border-left: 2px dotted #dadde1;
286
+ }
287
+
288
+ /* Timeline heading spacing */
289
+ .timeline-item h3 {
290
+ margin-bottom: 0.5rem;
291
+ }
292
+
293
+ /* Timeline images */
294
+ .timeline-image img {
295
+ max-width: 100%;
296
+ max-height: 300px;
297
+ border-radius: 4px;
298
+ margin-top: 0.5rem;
299
+ }
300
+
301
+
302
+ /* Image zoom */
303
+ .prose img {
304
+ cursor: zoom-in;
305
+ transition: transform 0.3s ease-in-out;
306
+ position: relative;
307
+ z-index: 1;
308
+ border-radius: 6px;
309
+ }
310
+
311
+ .prose img.zoomed {
312
+ transform: scale(1.7);
313
+ cursor: zoom-out;
314
+ z-index: 9999;
315
+ position: relative;
316
+ }
317
+
318
+
319
+ /* ── Dark mode overrides ── */
320
+
321
+ .dark .callout {
322
+ background-color: #1e2027;
323
+ border-color: #374151;
324
+ }
325
+
326
+ .dark .callout-header {
327
+ color: #f3f4f6;
328
+ }
329
+
330
+ .dark .callout-body {
331
+ color: #d1d5db;
332
+ }
333
+
334
+ .dark .callout.note { border-color: #3b5ea6; }
335
+ .dark .callout.note .callout-header { background-color: #1e3a5f; }
336
+
337
+ .dark .callout.tip { border-color: #3b4ea6; }
338
+ .dark .callout.tip .callout-header { background-color: #1e2f6b; }
339
+
340
+ .dark .callout.warning { border-color: #7c5a2a; }
341
+ .dark .callout.warning .callout-header { background-color: #4a3010; }
342
+
343
+ .dark .callout.important { border-color: #1a7a70; }
344
+ .dark .callout.important .callout-header { background-color: #0d3d39; }
345
+
346
+ .dark .callout.danger { border-color: #7c2a2a; }
347
+ .dark .callout.danger .callout-header { background-color: #4a1010; }
348
+
349
+ .dark .callout.success { border-color: #2a6b2a; }
350
+ .dark .callout.success .callout-header { background-color: #103a10; }
351
+
352
+ /* Dark prose — tables, code, headings */
353
+ .dark .prose {
354
+ color: #d1d5db;
355
+ }
356
+
357
+ .dark .prose h1,
358
+ .dark .prose h2,
359
+ .dark .prose h3,
360
+ .dark .prose h4 {
361
+ color: #f3f4f6;
362
+ }
363
+
364
+ .dark .prose strong {
365
+ color: #f3f4f6;
366
+ }
367
+
368
+ .dark .prose a {
369
+ color: #93c5fd;
370
+ }
371
+
372
+ .dark .prose h1 a,
373
+ .dark .prose h2 a,
374
+ .dark .prose h3 a,
375
+ .dark .prose h4 a,
376
+ .dark .prose h5 a,
377
+ .dark .prose h6 a {
378
+ color: inherit;
379
+ }
380
+
381
+ .dark .prose thead th {
382
+ background-color: #1f2937;
383
+ border-bottom-color: #374151;
384
+ color: #d1d5db;
385
+ }
386
+
387
+ .dark .prose tbody tr:nth-child(even) td {
388
+ background-color: #1a1f2e;
389
+ }
390
+
391
+ .dark .prose tbody tr:nth-child(odd) td {
392
+ background-color: #111827;
393
+ }
394
+
395
+ .dark .prose code:not(pre code) {
396
+ background-color: #1f2937;
397
+ color: #e5e7eb;
398
+ }
399
+
400
+ .dark .prose pre code {
401
+ background: none !important;
402
+ color: #e2e8f0;
403
+ }
404
+
405
+ /* Prose link styles override the circle's color — win specificity back */
406
+ .prose .timeline-step-anchor,
407
+ .dark .prose .timeline-step-anchor {
408
+ color: white;
409
+ }
410
+
411
+ /* Dark timeline */
412
+ .dark .timeline-item::before {
413
+ background-color: #4b5563;
414
+ }
415
+
416
+ .dark .timeline-step-anchor {
417
+ background-color: #4b5563;
418
+ color: white;
419
+ }
420
+
421
+ .dark .timeline-step-anchor:hover {
422
+ background-color: #6b7280;
423
+ }
424
+
425
+ .dark .timeline-item:not(.last-item)::after {
426
+ border-left-color: #374151;
427
+ }
428
+
429
+ /* ── Print / PDF styles ── */
430
+ @media print {
431
+ /* Hide all chrome — nav, sidebar, ToC, actions, footer */
432
+ nav,
433
+ aside,
434
+ footer,
435
+ [data-print="hide"],
436
+ .print\:hidden {
437
+ display: none !important;
438
+ }
439
+
440
+ /* Break the h-screen / overflow layout so full content prints.
441
+ Target Tailwind utility classes directly — they map 1:1 to CSS selectors. */
442
+ html,
443
+ body,
444
+ .h-screen,
445
+ .h-full {
446
+ height: auto !important;
447
+ min-height: 0 !important;
448
+ }
449
+
450
+ .overflow-hidden,
451
+ .overflow-y-auto,
452
+ .overflow-x-hidden {
453
+ overflow: visible !important;
454
+ }
455
+
456
+ /* flex-1 inside a fixed-height container would clip; release it */
457
+ .flex-1 {
458
+ flex: none !important;
459
+ height: auto !important;
460
+ }
461
+
462
+ /* Full-width article */
463
+ article {
464
+ max-width: 100% !important;
465
+ padding: 0 !important;
466
+ margin: 0 !important;
467
+ }
468
+
469
+ /* Expand all <details> elements — also handled via JS in generate-pdfs.ts */
470
+ details,
471
+ details[open] {
472
+ display: block !important;
473
+ }
474
+ details > :not(summary) {
475
+ display: block !important;
476
+ visibility: visible !important;
477
+ }
478
+
479
+ /* Avoid breaking inside callouts, code blocks, tables, timeline steps */
480
+ .callout,
481
+ pre,
482
+ table,
483
+ figure,
484
+ .timeline-item {
485
+ break-inside: avoid;
486
+ page-break-inside: avoid;
487
+ }
488
+
489
+ /* Keep headings with the content below them */
490
+ h1, h2, h3, h4 {
491
+ break-after: avoid;
492
+ page-break-after: avoid;
493
+ }
494
+
495
+ /* Remove link underlines and interactive cursor */
496
+ a {
497
+ text-decoration: none !important;
498
+ color: inherit !important;
499
+ }
500
+
501
+ /* Disable image zoom cursor */
502
+ .prose img {
503
+ cursor: default !important;
504
+ transform: none !important;
505
+ max-width: 100%;
506
+ }
507
+
508
+ /* Code blocks — keep dark background */
509
+ pre, code {
510
+ -webkit-print-color-adjust: exact !important;
511
+ print-color-adjust: exact !important;
512
+ }
513
+
514
+ /* Callout backgrounds */
515
+ .callout {
516
+ -webkit-print-color-adjust: exact !important;
517
+ print-color-adjust: exact !important;
518
+ }
519
+
520
+ /* Page margins */
521
+ @page {
522
+ margin: 18mm 16mm 18mm 16mm;
523
+ }
524
+ }