PageSourceSearch

https://firnum.netlify.app/

html firnum.netlify.app collected 2026-10-03 10:44:04 UTC 89,427 bytes, 2,387 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en" data-theme="dark">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width, initial-scale=1.0">
6<title>Barrows &amp; Borderlands — Game of Games</title>
7<meta name="description" content="Barrows & Borderlands is a Game of Games — a guild-based TTRPG campaign set in the weird science-fantasy world of Firnum. Explore guilds, lore, house rules, and more.">
8
9<!-- Open Graph — controls Discord / social media link previews -->
10<meta property="og:type" content="website">
11<meta property="og:title" content="Barrows & Borderlands — Game of Games">
12<meta property="og:description" content="A guild-based TTRPG campaign set in the weird science-fantasy world of Firnum. Explore 60+ guilds, deep lore, house rules, and more.">
13<meta property="og:url" content="https://firnum.netlify.app">
14<meta property="og:image" content="https://firnum.netlify.app/images/item-cards-1.jpeg">
15<meta property="og:image:width" content="1280">
16<meta property="og:image:height" content="960">
17<meta name="twitter:card" content="summary_large_image">
18<meta name="theme-color" content="#1a1a2e">
19
20<link rel="icon" href="images/favicon.ico" type="image/x-icon">
21<script src="config.js"></script>
21
22<link rel="preconnect" href="https://fonts.googleapis.com">
23<link id="googleFontsLink" href="" rel="stylesheet">
24<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
24
25<style id="themeStyle"></style>
26<style id="fontStyle"></style>
27<style>
28/* ============================================
29   BARROWS & BORDERLANDS WIKI
30   Structural CSS — edit colors/fonts in config.js
31   ============================================ */
32
33* { margin: 0; padding: 0; box-sizing: border-box; }
34
35html, body { overflow-x: hidden; }
36body { line-height: 1.7; min-height: 100vh; }
37
38.content { overflow-wrap: break-word; word-wrap: break-word; }
39.main-content { max-width: 100vw; overflow-x: hidden; }
40
41/* --- Header --- */
42.site-header {
43  position: fixed;
44  top: 0; left: 0; right: 0;
45  display: flex;
46  align-items: center;
47  padding: 0 16px;
48  z-index: 100;
49  gap: 12px;
50}
51
52.site-header::after {
53  content: '';
54  position: absolute;
55  bottom: -1px; left: 0; right: 0;
56  height: 1px;
57  background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
58}
59
60.menu-toggle {
61  display: none;
62  background: none;
63  border: 1px solid var(--border);
64  color: var(--gold);
65  font-size: 1.4em;
66  cursor: pointer;
67  padding: 4px 8px;
68  border-radius: 4px;
69  flex-shrink: 0;
70}
71
72.site-title {
73  font-weight: 700;
74  cursor: pointer;
75  letter-spacing: 2px;
76  text-transform: uppercase;
77  white-space: nowrap;
78  flex-shrink: 0;
79}
80
81.site-subtitle {
82  font-size: 0.72em;
83  letter-spacing: 1px;
84  opacity: 0.55;
85  white-space: nowrap;
86  display: none;
87  flex-shrink: 0;
88}
89
90@media (min-width: 1000px) { .site-subtitle { display: block; } }
91
92.header-spacer { flex: 1; }
93
94.search-container {
95  max-width: 340px;
96  width: 100%;
97  position: relative;
98  flex-shrink: 1;
99}
100
101.search-input {
102  width: 100%;
103  padding: 7px 12px;
104  border-radius: 6px;
105  font-size: 0.92em;
106  outline: none;
107  transition: border-color 0.2s;
108}
109
110/* Store button */
111.store-btn {
112  background: none;
113  cursor: pointer;
114  padding: 6px 12px;
115  border-radius: 6px;
116  font-size: 0.85em;
117  font-weight: 700;
118  letter-spacing: 1px;
119  text-decoration: none;
120  white-space: nowrap;
121  flex-shrink: 0;
122  transition: all 0.2s;
123  display: flex;
124  align-items: center;
125  gap: 4px;
126}
127
128.theme-toggle {
129  background: none;
130  cursor: pointer;
131  padding: 6px 10px;
132  border-radius: 6px;
133  font-size: 1.1em;
134  transition: all 0.2s;
135  flex-shrink: 0;
136}
137
138/* --- Layout --- */
139.layout {
140  display: flex;
141  min-height: calc(100vh - var(--header-height));
142}
143
144/* --- Sidebar --- */
145.sidebar {
146  width: var(--sidebar-width);
147  padding: 16px 0 40px;
148  position: fixed;
149  top: var(--header-height);
150  bottom: 0;
151  overflow-y: auto;
152  overflow-x: hidden;
153  z-index: 50;
154  transition: transform 0.3s ease;
155  border-right: 1px solid var(--border);
156}
157
158.sidebar::-webkit-scrollbar { width: 5px; }
159.sidebar::-webkit-scrollbar-track { background: transparent; }
160.sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
161
162/* Sidebar search — mobile only */
163.sidebar-search {
164  display: none; /* hidden on desktop */
165  padding: 12px 14px 6px;
166  position: relative;
167}
168.sidebar-search input {
169  width: 100%;
170  padding: 10px 12px;
171  border-radius: 6px;
172  font-size: 16px; /* prevents iOS zoom */
173  outline: none;
174  border: 1px solid var(--sidebar-border, var(--border));
175  background: var(--sidebar-bg, var(--bg-secondary));
176  color: var(--sidebar-text, var(--text-primary));
177  font-family: inherit;
178}
179.sidebar-search input::placeholder {
180  color: var(--sidebar-text-muted, var(--text-muted));
181}
182.sidebar-search .search-results {
183  position: absolute;
184  left: 14px;
185  right: 14px;
186  top: 100%;
187  border-radius: 6px;
188  max-height: 250px;
189  overflow-y: auto;
190  display: none;
191  box-shadow: 0 8px 32px rgba(0,0,0,0.4);
192  z-index: 200;
193  background: var(--bg-card);
194}
195.sidebar-search .search-results.visible { display: block; }
196
197.sidebar-section { padding: 0 14px; margin-bottom: 6px; }
198
199.sidebar-section-title {
200  font-size: 0.65em;
201  font-weight: 700;
202  text-transform: uppercase;
203  letter-spacing: 2.5px;
204  padding: 10px 0 5px;
205}
206
207.sidebar-link {
208  display: flex;
209  align-items: center;
210  padding: 6px 10px;
211  text-decoration: none;
212  font-size: 0.9em;
213  border-radius: 4px;
214  cursor: pointer;
215  transition: all 0.15s;
216  gap: 7px;
217}
218
219.sidebar-link.active {
220  border-left: 2px solid var(--gold);
221  margin-left: -2px;
222}
223
224.sidebar-link .icon {
225  font-size: 0.88em;
226  width: 18px;
227  text-align: center;
228  flex-shrink: 0;
229}
230
231/* SVG icon — used by the icon() helper */
232.nav-icon {
233  flex-shrink: 0;
234  display: block;
235  opacity: 0.75;
236  transition: opacity 0.15s;
237}
238
239.sidebar-link:hover .nav-icon,
240.sidebar-link.active .nav-icon,
241.tools-header:hover .nav-icon,
242.tools-header.open .nav-icon,
243.guild-type-header:hover .nav-icon,
244.lore-node-header:hover .nav-icon,
245.lore-node-header.active-lore .nav-icon { opacity: 1; }
246
247/* Chevron arrow for collapsibles */
248.tools-header .nav-icon:last-child,
249.guild-type-header .nav-icon:last-child,
250.lore-node-header .nav-icon:last-child {
251  margin-left: auto;
252  transition: transform 0.2s, opacity 0.15s;
253}
254
255.tools-header.open .nav-icon:last-child,
256.guild-type-header.open .nav-icon:last-child,
257.lore-node-header.open .nav-icon:last-child { transform: rotate(90deg); }
258
259/* Lore tree */
260.lore-tree-node { margin-bottom: 2px; }
261
262.lore-node-header {
263  display: flex;
264  align-items: center;
265  padding: 6px 10px;
266  font-size: 0.9em;
267  cursor: pointer;
268  border-radius: 4px;
269  transition: all 0.15s;
270  gap: 7px;
271  user-select: none;
272}
273
274.lore-node-header.has-children:hover { background: var(--bg-hover); color: var(--text-primary); }
275.lore-node-header.leaf { cursor: pointer; }
276.lore-node-header.leaf:hover { background: var(--bg-hover); color: var(--text-primary); }
277.lore-node-header.active-lore { background: var(--bg-hover); color: var(--gold); border-left: 2px solid var(--gold); margin-left: -2px; }
278
279.lore-node-children { display: none; }
280.lore-node-children.open { display: block; }
281
282/* indent levels */
283.lore-depth-1 { padding-left: 18px; }
284.lore-depth-2 { padding-left: 36px; }
285.lore-depth-3 { padding-left: 54px; }
286
287/* Campaign Tools dropdown */
288.tools-group { margin-bottom: 4px; }
289
290.tools-header {
291  display: flex;
292  align-items: center;
293  padding: 6px 10px;
294  font-size: 0.9em;
295  cursor: pointer;
296  border-radius: 4px;
297  transition: all 0.15s;
298  gap: 7px;
299  user-select: none;
300}
301
302.tools-children { display: none; padding-left: 18px; }
303.tools-children.open { display: block; }
304.tools-children .sidebar-link { font-size: 0.86em; padding: 5px 10px; }
305
306/* Guild type collapsibles */
307.guild-type-group { margin-bottom: 3px; }
308
309/* ── World group (Firnum / Thernn) ── */
310.guild-world-group { margin-bottom: 4px; }
311.guild-world-header {
312  display: flex; align-items: center; gap: 8px;
313  padding: 8px 14px 8px 10px; border-radius: 6px;
314  cursor: pointer; font-size: 0.82rem; font-weight: 700;
315  letter-spacing: 0.08em; text-transform: uppercase;
316  transition: background 0.15s, color 0.15s;
317  color: var(--sidebar-gold, var(--gold));
318  user-select: none;
319}
320.guild-world-header:hover { background: var(--sidebar-hover, var(--bg-hover)); }
321.guild-world-header .nav-icon:last-child { margin-left: auto; transition: transform 0.2s; opacity: 0.5; }
322.guild-world-header.open .nav-icon:last-child { transform: rotate(90deg); }
323.guild-world-children { display: none; padding-left: 6px; }
324.guild-world-children.open { display: block; }
325
326.guild-type-header {
327  display: flex;
328  align-items: center;
329  padding: 6px 10px;
330  font-size: 0.9em;
331  cursor: pointer;
332  border-radius: 4px;
333  transition: all 0.15s;
334  gap: 7px;
335  user-select: none;
336}
337
338.guild-type-children { display: none; padding-left: 18px; }
339.guild-type-children.open { display: block; }
340.guild-type-children .sidebar-link { font-size: 0.86em; padding: 4px 10px; }
341
342.guild-type-label {
343  display: flex;
344  align-items: center;
345  gap: 7px;
346  flex: 1;
347  cursor: pointer;
348  min-width: 0;
349}
350.guild-type-chevron {
351  flex-shrink: 0;
352  cursor: pointer;
353  opacity: 0.5;
354  padding: 2px 4px;
355  border-radius: 3px;
356  transition: opacity 0.15s;
357}
358.guild-type-chevron:hover { opacity: 1; }
359
360.guild-stars {
361  font-size: 0.75em;
362  margin-left: auto;
363  letter-spacing: 1px;
364  flex-shrink: 0;
365}
366
367/* --- Main Content --- */
368.main-content {
369  margin-top: var(--header-height);
370  flex: 1;
371  margin-left: var(--sidebar-width);
372  padding: 32px 48px;
373  max-width: var(--content-max-width);
374  min-height: calc(100vh - var(--header-height));
375  transition: margin-left 0.3s ease;
376}
377
378/* --- Full-screen embed (map, character creator, tools) --- */
379.fullscreen-embed {
380  position: fixed;
381  top: var(--header-height);
382  left: var(--sidebar-width);
383  right: 0;
384  bottom: 0;
385  z-index: 10;
386  display: none;
387  transition: left 0.3s ease;
388}
389
390.fullscreen-embed iframe {
391  width: 100%;
392  height: 100%;
393  border: none;
394  display: block;
395}
396
397/* --- Inline modal panel (forms, attendance) --- */
398.inline-panel-overlay {
399  display: none;
400  position: fixed;
401  inset: 0;
402  background: rgba(0,0,0,0.7);
403  z-index: 300;
404  align-items: center;
405  justify-content: center;
406}
407
408.inline-panel-overlay.open {
409  display: flex;
410}
411
412.inline-panel {
413  position: relative;
414  width: min(860px, 92vw);
415  height: min(700px, 88vh);
416  border-radius: 8px;
417  overflow: hidden;
418  display: flex;
419  flex-direction: column;
420  border: 1px solid var(--border);
421  box-shadow: 0 20px 60px rgba(0,0,0,0.6);
422}
423
424.inline-panel-header {
425  display: flex;
426  align-items: center;
427  padding: 10px 16px;
428  border-bottom: 1px solid var(--border);
429  gap: 12px;
430  flex-shrink: 0;
431}
432
433.inline-panel-title {
434  font-size: 0.9em;
435  font-weight: 700;
436  letter-spacing: 1px;
437  flex: 1;
438}
439
440.inline-panel-close {
441  background: none;
442  border: 1px solid var(--border);
443  color: inherit;
444  cursor: pointer;
445  padding: 4px 10px;
446  border-radius: 4px;
447  font-size: 1em;
448  transition: all 0.2s;
449}
450
451.inline-panel-close:hover { border-color: var(--gold); color: var(--gold); }
452
453.inline-panel iframe {
454  flex: 1;
455  width: 100%;
456  border: none;
457}
458
459
460/* --- Markdown Rendering --- */
461.content h1 {
462  font-size: 2em;
463  font-weight: 700;
464  letter-spacing: 1px;
465  margin-bottom: 8px;
466  line-height: 1.2;
467}
468
469.content h2 {
470  font-size: 1.4em;
471  font-weight: 600;
472  margin: 32px 0 12px;
473  padding-bottom: 6px;
474  border-bottom: 1px solid var(--border);
475}
476
477.content h3 { font-size: 1.15em; margin: 24px 0 8px; }
478
479.content h4 {
480  font-size: 1.05em;
481  border-bottom: 2px solid var(--border-accent);
482  padding-bottom: 6px;
483  margin: 28px 0 12px;
484  letter-spacing: 1px;
485}
486
487.content p { margin: 10px 0; }
488.content strong { font-weight: 700; }
489
490.content hr {
491  border: none;
492  border-top: 1px solid var(--border);
493  margin: 28px 0;
494  position: relative;
495}
496
497.content hr::after {
498  content: var(--divider-symbol);
499  position: absolute;
500  top: -0.65em;
501  left: 50%;
502  transform: translateX(-50%);
503  background: var(--bg-primary);
504  padding: 0 12px;
505  font-size: 0.9em;
506}
507
508.content blockquote {
509  border-left: 4px solid var(--gold-dim);
510  background: rgba(0,0,0,0.08);
511  padding: 12px 20px;
512  margin: 16px 0;
513  font-style: italic;
514  border-radius: 0 6px 6px 0;
515}
516
517.content a { text-decoration: none; border-bottom: 1px solid transparent; transition: all 0.2s; }
518.content a:hover { border-bottom-color: var(--gold); }
519
520/* Apply / Action Buttons */
521.content a.embed-btn,
522.content a[href*="forms/d/e"],
523.content a[href*="APPLY"],
524.content a[href*="discord.gg"],
525.content a[href*="ko-fi.com"],
526.content a[href*="JOIN"] {
527  display: inline-block;
528  background: var(--text-primary);
529  color: var(--bg-primary) !important;
530  padding: 10px 24px;
531  border-radius: 4px;
532  font-weight: 700;
533  font-size: 0.9em;
534  text-decoration: none !important;
535  text-transform: uppercase;
536  letter-spacing: 2px;
537  border: 1px solid var(--text-primary) !important;
538  border-bottom: 1px solid var(--text-primary) !important;
539  transition: all 0.2s ease;
540  margin: 8px 0;
541}
542
543.content a.embed-btn:hover,
544.content a[href*="forms/d/e"]:hover,
545.content a[href*="APPLY"]:hover,
546.content a[href*="discord.gg"]:hover,
547.content a[href*="ko-fi.com"]:hover {
548  background: var(--text-secondary);
549  border-color: var(--text-secondary) !important;
550  transform: translateY(-1px);
551}
552
553/* Tables */
554.content table {
555  width: 100%;
556  border-collapse: collapse;
557  margin: 16px 0;
558  font-size: 0.92em;
559  background: var(--bg-card);
560  border: 1px solid var(--border);
561  border-radius: 8px;
562  overflow: hidden;
563}
564
565.content thead th {
566  font-weight: 600;
567  font-size: 0.85em;
568  text-transform: uppercase;
569  letter-spacing: 0.5px;
570  padding: 10px 12px;
571  text-align: left;
572  border-bottom: 2px solid var(--border-accent);
573}
574
575.content tbody td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
576.content tbody tr:last-child td { border-bottom: none; }
577.content tbody tr:hover { background: var(--bg-hover); }
578
579/* Lists */
580.content ul { padding-left: 0; list-style: none; }
581.content ul li { padding: 4px 0; padding-left: 20px; position: relative; }
582.content ul li::before {
583  content: '◆';
584  position: absolute;
585  left: 0;
586  font-size: 0.5em;
587  top: 10px;
588}
589
590.content ol { padding-left: 24px; }
591.content ol li { padding: 3px 0; }
592
593.content code {
594  font-family: var(--mono-font);
595  padding: 2px 6px;
596  border-radius: 3px;
597  font-size: 0.88em;
598}
599
600.content img {
601  max-width: 100%;
602  max-height: 420px;
603  width: auto;
604  height: auto;
605  object-fit: contain;
606  border-radius: 8px;
607  border: 1px solid var(--border);
608  margin: 12px 0;
609  box-shadow: 0 2px 12px rgba(0,0,0,0.15);
610  display: block;
611}
612
613/* When two images sit side-by-side in one paragraph */
614.content p > img + img {
615  display: inline-block;
616  max-height: 320px;
617  max-width: 48%;
618  margin: 12px 1% 12px 0;
619  vertical-align: top;
620}
621.content p > img:first-child:not(:only-child) {
622  display: inline-block;
623  max-height: 320px;
624  max-width: 48%;
625  margin: 12px 1% 12px 0;
626  vertical-align: top;
627}
628
629/* --- Discord event cards --- */
630.discord-event-card {
631  background: var(--bg-card);
632  border: 1px solid var(--border);
633  border-left: 3px solid var(--gold-dim);
634  border-radius: 6px;
635  padding: 16px 20px;
636  margin: 12px 0;
637  display: flex;
638  align-items: center;
639  gap: 20px;
640  text-decoration: none !important;
641  color: inherit !important;
642  transition: all 0.2s;
643  cursor: pointer;
644  border-bottom: 1px solid var(--border) !important;
645}
646
647.discord-event-card:hover {
648  border-color: var(--gold-dim) !important;
649  border-left-color: var(--gold) !important;
650  background: var(--bg-hover);
651  transform: translateX(4px);
652}
653
654.discord-event-icon { font-size: 1.8em; flex-shrink: 0; }
655
656.discord-event-info { flex: 1; }
657
658.discord-event-title { font-weight: 700; font-size: 1.05em; margin-bottom: 2px; }
659
660.discord-event-meta { font-size: 0.85em; opacity: 0.65; }
661
662.discord-event-arrow { font-size: 1.2em; opacity: 0.4; flex-shrink: 0; }
663
664/* --- Character Roster --- */
665.roster-table-wrap {
666  overflow-x: auto;
667  margin: 20px 0;
668  /* Break out of content max-width so the full table is visible */
669  width: calc(100vw - var(--sidebar-width) - 96px);
670  max-width: 1400px;
671}
672
673.roster-table {
674  width: 100%;
675  border-collapse: collapse;
676  font-size: 0.9em;
677}
678
679.roster-table th {
680  text-align: left;
681  padding: 10px 12px;
682  font-size: 0.75em;
683  letter-spacing: 1.5px;
684  text-transform: uppercase;
685  border-bottom: 2px solid var(--border);
686  white-space: nowrap;
687}
688
689.roster-table td {
690  padding: 10px 12px;
691  border-bottom: 1px solid var(--border);
692  vertical-align: middle;
693}
694
695.roster-table tr:hover td {
696  background: rgba(201, 168, 76, 0.04);
697}
698
699.roster-status {
700  display: inline-flex;
701  align-items: center;
702  gap: 4px;
703  padding: 2px 8px;
704  border-radius: 10px;
705  font-size: 0.85em;
706  font-weight: 600;
707  white-space: nowrap;
708}
709
710.roster-status.active   { background: rgba(46,204,113,0.15); color: #2ecc71; }
711.roster-status.training { background: rgba(52,152,219,0.15); color: #3498db; }
712.roster-status.traveling { background: rgba(230,126,34,0.15); color: #e67e22; }
713
714.roster-lock-info {
715  font-size: 0.8em;
716  opacity: 0.6;
717  margin-top: 2px;
718}
719
720.roster-count {
721  font-size: 0.85em;
722  opacity: 0.5;
723  margin-bottom: 16px;
724}
725
726.roster-empty {
727  text-align: center;
728  padding: 40px 20px;
729  opacity: 0.5;
730  font-style: italic;
731}
732
733/* --- Session index list --- */
734.session-list { margin: 16px 0; }
735
736.session-item {
737  background: var(--bg-card);
738  border: 1px solid var(--border);
739  border-radius: 6px;
740  padding: 14px 18px;
741  margin-bottom: 8px;
742  cursor: pointer;
743  transition: all 0.15s;
744  display: flex;
745  align-items: center;
746  gap: 16px;
747}
748
749.session-item:hover { border-color: var(--gold-dim); background: var(--bg-hover); }
750.session-item .session-name { font-weight: 600; font-size: 0.95em; }
751.session-item .session-file { font-size: 0.8em; opacity: 0.5; margin-left: auto; font-family: var(--mono-font); }
752
753/* --- Guild directory grid --- */
754.guild-directory-grid {
755  display: grid;
756  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
757  gap: 12px;
758  margin: 16px 0;
759}
760
761.guild-type-card {
762  background: var(--bg-card);
763  border: 1px solid var(--border);
764  border-radius: 8px;
765  padding: 16px 20px;
766  cursor: pointer;
767  transition: all 0.2s;
768}
769
770.guild-type-card:hover {
771  border-color: var(--gold-dim);
772  background: var(--bg-hover);
773  transform: translateY(-2px);
774  box-shadow: 0 4px 20px rgba(0,0,0,0.3);
775}
776
777.guild-type-card .card-title { font-weight: 600; font-size: 0.95em; margin-bottom: 4px; }
778.guild-type-card .card-desc { font-size: 0.85em; line-height: 1.4; }
779.guild-type-card .card-count { font-size: 0.8em; margin-top: 8px; font-family: var(--mono-font); }
780
781/* --- Footer --- */
782.site-footer {
783  margin-left: var(--sidebar-width);
784  padding: 24px 48px;
785  border-top: 1px solid var(--border);
786  font-size: 0.85em;
787  display: flex;
788  gap: 24px;
789  flex-wrap: wrap;
790  transition: margin-left 0.3s ease;
791}
792
793.site-footer a { text-decoration: none; transition: color 0.2s; }
794
795/* --- Search Results --- */
796.search-results {
797  position: absolute;
798  top: calc(100% + 4px);
799  left: 0; right: 0;
800  border-radius: 6px;
801  max-height: 300px;
802  overflow-y: auto;
803  display: none;
804  box-shadow: 0 8px 32px rgba(0,0,0,0.4);
805  z-index: 200;
806}
807
808.search-results.visible { display: block; }
809
810.search-result-item {
811  padding: 10px 14px;
812  cursor: pointer;
813  border-bottom: 1px solid var(--border);
814  transition: background 0.15s;
815}
816
817.search-result-item:last-child { border-bottom: none; }
818.search-result-title { font-size: 0.9em; }
819.search-result-type { font-size: 0.8em; }
820
821/* Loading */
822.loading { text-align: center; padding: 60px 20px; font-style: italic; }
823
824/* Sidebar overlay for mobile */
825.sidebar-overlay {
826  display: none;
827  position: fixed;
828  inset: 0;
829  background: rgba(0,0,0,0.5);
830  z-index: 40;
831}
832
833/* --- Desktop sidebar collapse --- */
834.sidebar-collapse-btn {
835  display: none; /* hidden on mobile, shown on desktop via media query */
836  position: absolute;
837  top: 10px;
838  right: -14px;
839  width: 28px;
840  height: 28px;
841  border-radius: 50%;
842  border: 1px solid var(--border);
843  background: var(--sidebar-bg, var(--bg-secondary));
844  color: var(--sidebar-text, var(--text-secondary));
845  cursor: pointer;
846  align-items: center;
847  justify-content: center;
848  font-size: 14px;
849  z-index: 51;
850  transition: all 0.2s;
851  padding: 0;
852  line-height: 1;
853}
854.sidebar-collapse-btn:hover {
855  background: var(--sidebar-hover, var(--bg-hover));
856  color: var(--sidebar-gold, var(--gold));
857  border-color: var(--sidebar-gold, var(--gold));
858}
859
860@media (min-width: 769px) {
861  .sidebar-collapse-btn {
862    display: flex;
863    position: fixed;
864    top: calc(var(--header-height) + 12px);
865    left: calc(var(--sidebar-width) - 14px);
866    right: auto;
867    transition: left 0.3s ease;
868  }
869
870  /* Collapsed state on desktop */
871  body.sidebar-collapsed .sidebar {
872    transform: translateX(-100%);
873    border-right: none !important;
874  }
875  body.sidebar-collapsed .sidebar-collapse-btn {
876    left: 6px;
877  }
878  body.sidebar-collapsed .main-content { margin-left: 0 !important; }
879  body.sidebar-collapsed .site-footer { margin-left: 0 !important; }
880  body.sidebar-collapsed .fullscreen-embed { left: 0 !important; }
881}
882
883/* --- Mobile --- */
884@media (max-width: 768px) {
885  .menu-toggle { display: block; }
886  .sidebar { transform: translateX(-100%); width: 280px; position: fixed; }
887  .sidebar.open { transform: translateX(0); }
888  .sidebar.open ~ .sidebar-overlay { display: block; }
889  body { font-size: 17px !important; }
890  .main-content { margin-left: 0 !important; padding: 20px 14px; max-width: 100vw !important; }
891  .site-footer { margin-left: 0; padding: 16px 14px; }
892  .search-container { display: none !important; } /* hidden in header on mobile — moved to sidebar */
893  .sidebar-search { display: block !important; } /* show search in sidebar on mobile */
894  .content h1 { font-size: 1.4em; }
895  .content h2 { font-size: 1.2em; margin: 24px 0 10px; }
896  .guild-directory-grid { grid-template-columns: 1fr; }
897  .fullscreen-embed { left: 0; }
898  .store-btn span.store-label { display: none; }
899
900  /* Better touch targets */
901  .sidebar-link { padding: 10px 12px; font-size: 0.95em; min-height: 44px; }
902  .tools-header { padding: 10px 12px; min-height: 44px; }
903  .guild-type-header { padding: 10px 12px; min-height: 44px; }
904  .lore-node-header { padding: 10px 12px; min-height: 44px; }
905  .guild-world-header { padding: 10px 14px; min-height: 44px; }
906
907  /* Tables scroll horizontally */
908  .content table { display: block; overflow-x: auto; white-space: nowrap; }
909  .roster-table-wrap { width: calc(100vw - 28px); }
910
911  /* Side-by-side images stack */
912  .content p > img + img,
913  .content p > img:first-child:not(:only-child) {
914    max-width: 100%;
915    max-height: none;
916    display: block;
917    margin: 8px 0;
918  }
919
920  /* Discord event cards stack */
921  .discord-event-card { flex-direction: column; align-items: flex-start; gap: 8px; padding: 14px 16px; }
922  .discord-event-arrow { display: none; }
923
924  /* Header tighter */
925  .site-header { padding: 0 10px; gap: 8px; }
926}
927
928/* Small phones */
929@media (max-width: 480px) {
930  .main-content { padding: 16px 10px; }
931  .content h1 { font-size: 1.25em; }
932  body { font-size: 16px !important; }
933  .guild-type-card { padding: 12px 14px; }
934  .site-footer { flex-direction: column; gap: 12px; padding: 14px 10px; }
935}
936</style>
937</head>
938<body>
939
940<!-- ============================================================
941     SVG ICON SPRITE
942     To add a custom icon: copy one <symbol> block, give it a new id,
943     and draw your SVG path inside it. Then reference it with icon: "your-id"
944     in config.js. See EDITING-GUIDE.md for a full walkthrough.
945     ============================================================ -->
946<svg xmlns="http://www.w3.org/2000/svg" style="display:none" id="iconSprite">
947  <!-- Navigation -->
948  <symbol id="icon-home" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
949    <path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H5a1 1 0 01-1-1V9.5z"/><polyline points="9 21 9 12 15 12 15 21"/>
950  </symbol>
951  <symbol id="icon-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
952    <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
953  </symbol>
954  <symbol id="icon-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
955    <path d="M12 2L3 7v6c0 5 4 9 9 10 5-1 9-5 9-10V7L12 2z"/>
956  </symbol>
957  <symbol id="icon-map" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
958    <polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/>
959  </symbol>
960  <symbol id="icon-egg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
961    <path d="M12 2C8 2 4 7 4 13a8 8 0 0016 0c0-6-4-11-8-11z"/>
962  </symbol>
963  <symbol id="icon-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
964    <path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/>
965  </symbol>
966  <symbol id="icon-scroll" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
967    <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>
968  </symbol>
969  <!-- Campaign Tools -->
970  <symbol id="icon-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
971    <path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>
972  </symbol>
973  <symbol id="icon-flask" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
974    <path d="M9 3h6m-6 0v6L5 17a2 2 0 001.8 3h10.4A2 2 0 0019 17L15 9V3m-6 0h6"/>
975  </symbol>
976  <symbol id="icon-sword" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
977    <polyline points="14.5 17.5 3 6 3 3 6 3 17.5 14.5"/><line x1="13" y1="19" x2="19" y2="13"/><line x1="16" y1="16" x2="20" y2="20"/><line x1="19" y1="21" x2="21" y2="19"/>
978  </symbol>
979  <symbol id="icon-shop" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
980    <path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/>
981  </symbol>
982  <symbol id="icon-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
983    <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
984  </symbol>
985  <symbol id="icon-chest" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
986    <rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v2"/><line x1="12" y1="12" x2="12" y2="12"/>
987    <path d="M2 13h5m10 0h5M7 13a2 2 0 004 0"/>
988  </symbol>
989  <symbol id="icon-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
990    <path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/>
991  </symbol>
992  <symbol id="icon-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
993    <rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 7l-10 6L2 7"/>
994  </symbol>
995  <symbol id="icon-dungeon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
996    <rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v4m6-4v4M3 9h4m14 0h-4M3 15h4m14 0h-4M9 21v-4m6 4v-4"/>
997  </symbol>
998  <!-- Guild types -->
999  <symbol id="icon-scales" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1000    <line x1="12" y1="3" x2="12" y2="21"/><path d="M3 9l4-4 4 4M17 9l-4-4-4 4"/><path d="M5 21h14"/><path d="M5 9c0 3.866 0 7 7 7s7-3.134 7-7"/>
1001  </symbol>
1002  <symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1003    <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
1004  </symbol>
1005  <symbol id="icon-dagger" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1006    <line x1="12" y1="2" x2="12" y2="18"/><path d="M8 18h8"/><path d="M10 14l-3 6m10-6l3 6"/><path d="M9 6l3-4 3 4"/>
1007  </symbol>
1008  <symbol id="icon-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1009    <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
1010  </symbol>
1011  <symbol id="icon-bow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1012    <path d="M18 3C9 3 4 8 4 12s5 9 14 9"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="16" y1="8" x2="20" y2="12"/><line x1="16" y1="16" x2="20" y2="12"/>
1013  </symbol>
1014  <symbol id="icon-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1015    <circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>
1016  </symbol>
1017  <!-- Lore tree -->
1018  <symbol id="icon-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1019    <circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>
1020  </symbol>
1021  <symbol id="icon-mountain" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1022    <polygon points="3 20 10 7 14 13 17 10 21 20 3 20"/>
1023  </symbol>
1024  <symbol id="icon-tree" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1025    <path d="M12 2L4 14h16L12 2z"/><path d="M12 14v8"/><path d="M8 22h8"/>
1026  </symbol>
1027  <symbol id="icon-city" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1028    <line x1="3" y1="22" x2="21" y2="22"/><rect x="5" y="8" width="6" height="14"/><rect x="13" y="12" width="6" height="10"/><path d="M5 8V5l3-3 3 3v3"/><path d="M13 12V9l3-3 3 3v3"/>
1029  </symbol>
1030  <symbol id="icon-desert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1031    <path d="M2 20h20M2 20c4-4 6-8 10-8s6 4 10 8"/><circle cx="17" cy="7" r="3"/><line x1="17" y1="4" x2="17" y2="2"/>
1032  </symbol>
1033  <symbol id="icon-wave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1034    <path d="M2 12c2-4 4-4 6 0s4 4 6 0 4-4 6 0"/><path d="M2 18c2-4 4-4 6 0s4 4 6 0 4-4 6 0"/>
1035  </symbol>
1036  <symbol id="icon-horizon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1037    <line x1="2" y1="12" x2="22" y2="12"/><path d="M5 12V8m14 4V8M2 16h20"/><path d="M9 8a3 3 0 106 0"/>
1038  </symbol>
1039  <symbol id="icon-cosmos" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1040    <circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/><line x1="4.93" y1="4.93" x2="9.17" y2="9.17"/><line x1="14.83" y1="14.83" x2="19.07" y2="19.07"/><line x1="14.83" y1="9.17" x2="19.07" y2="4.93"/><line x1="4.93" y1="19.07" x2="9.17" y2="14.83"/>
1041  </symbol>
1042  <!-- UI chrome -->
1043  <symbol id="icon-tools" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1044    <path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>
1045  </symbol>
1046  <symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1047    <polyline points="9 18 15 12 9 6"/>
1048  </symbol>
1049  <symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1050    <line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
1051  </symbol>
1052  <symbol id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1053    <path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
1054  </symbol>
1055  <symbol id="icon-sun-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1056    <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
1057  </symbol>
1058  <symbol id="icon-store" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1059    <circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 001.95-1.57L23 6H6"/>
1060  </symbol>
1061  <symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1062    <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
1063  </symbol>
1064  <symbol id="icon-coffee" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1065    <path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/>
1066  </symbol>
1067  <symbol id="icon-bifrost" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1068    <!-- The great arc of the bridge -->
1069    <path d="M2 20 Q12 2 22 20"/>
1070    <!-- Two inner arcs giving it depth/layers -->
1071    <path d="M4 20 Q12 5 20 20" stroke-width="1.2"/>
1072    <path d="M6 20 Q12 8 18 20" stroke-width="0.9"/>
1073    <!-- Left pillar/post -->
1074    <line x1="4" y1="20" x2="4" y2="17"/>
1075    <!-- Right pillar/post -->
1076    <line x1="20" y1="20" x2="20" y2="17"/>
1077    <!-- Ground line -->
1078    <line x1="2" y1="20" x2="22" y2="20"/>
1079  </symbol>
1080</svg>
1081
1082<!-- ============================================================
1083     HEADER
1084     ============================================================ -->
1085<header class="site-header" id="siteHeader">
1086  <button class="menu-toggle" onclick="toggleSidebar()" aria-label="Toggle menu">
1087    <svg width="18" height="18"><use href="#icon-menu"/></svg>
1088  </button>
1089  <div class="site-title" id="siteTitle" onclick="navigateTo('overview')"></div>
1090  <div class="site-subtitle" id="siteSubtitle"></div>
1091  <div class="header-spacer"></div>
1092  <div class="search-container">
1093    <input type="text" class="search-input" id="searchInput" placeholder="Search the wiki…"
1094      oninput="handleSearch(this.value)"
1095      onfocus="handleSearch(this.value)"
1096      onblur="setTimeout(()=>closeSearch(),200)">
1097    <div class="search-results" id="searchResults"></div>
1098  </div>
1099  <a id="storeBtnEl" class="store-btn" href="" target="_blank" title="Visit the Store" style="display:none">
1100    <svg width="15" height="15"><use href="#icon-store"/></svg>
1101    <span class="store-label">Store</span>
1102  </a>
1103  <button class="theme-toggle" id="themeToggleBtn" onclick="toggleTheme()" title="Toggle light/dark mode">
1104    <svg width="16" height="16" id="themeIcon"><use href="#icon-moon"/></svg>
1105  </button>
1106</header>
1107
1108<!-- ============================================================
1109     LAYOUT
1110     ============================================================ -->
1111<div class="layout">
1112  <nav class="sidebar" id="sidebar"></nav>
1113  <button class="sidebar-collapse-btn" id="sidebarCollapseBtn" onclick="toggleDesktopSidebar()" title="Toggle sidebar">
1114    <svg width="14" height="14"><use href="#icon-chevron-right"/></svg>
1115  </button>
1116  <div class="sidebar-overlay" onclick="toggleSidebar()"></div>
1117  <main class="main-content" id="mainContent">
1118    <div class="content" id="content">
1119      <div class="loading">Loading the wiki…</div>
1120    </div>
1121  </main>
1122</div>
1123
1124<!-- Full-screen embeds (map, character creator, tools) -->
1125<div class="fullscreen-embed" id="fullscreenEmbed">
1126  <iframe id="fullscreenIframe" src="" title="Embedded tool" allowfullscreen></iframe>
1127</div>
1128
1129<!-- Inline modal panel (attendance form, etc.) -->
1130<div class="inline-panel-overlay" id="inlinePanelOverlay" onclick="closeInlinePanel(event)">
1131  <div class="inline-panel" id="inlinePanel">
1132    <div class="inline-panel-header" id="inlinePanelHeader">
1133      <span class="inline-panel-title" id="inlinePanelTitle"></span>
1134      <button class="inline-panel-close" onclick="closeInlinePanelDirect()">✕ Close</button>
1135    </div>
1136    <iframe id="inlinePanelIframe" src="" title="Inline panel"></iframe>
1137  </div>
1138</div>
1139
1140<footer class="site-footer" id="siteFooter"></footer>
1141
1142<!-- Fantasy Calendar script (loaded once) -->
1143<script id="fantasyCalScript"></script>
1143
1144
1145<script>
1146// ============================================================
1147//  WIKI ENGINE — reads everything from config.js
1148// ============================================================
1149
1150const C = WIKI_CONFIG;
1151
1152// ----------------------------------------------------------
1153//  SVG ICON HELPER
1154//  Renders a named icon from the sprite sheet.
1155//  Usage: icon('home') → <svg><use href="#icon-home"/></svg>
1156//  If the icon id doesn't exist in the sprite, nothing shows.
1157// ----------------------------------------------------------
1158function icon(name, size = 16) {
1159  return `<svg width="${size}" height="${size}" class="nav-icon" aria-hidden="true"><use href="#icon-${name}"/></svg>`;
1160}
1161
1162// ----------------------------------------------------------
1163//  APPLY CONFIG
1164// ----------------------------------------------------------
1165function applyConfig() {
1166  document.getElementById('googleFontsLink').href = C.fonts.googleFontsUrl;
1167
1168  document.getElementById('fontStyle').textContent = `
1169    body { font-family: '${C.fonts.bodyFont}', Georgia, serif; font-size: ${C.fonts.bodySize}; }
1170    .site-title, .content h1, .content h2, .content h3, .content h4,
1171    .sidebar-section-title, .guild-type-header, .tools-header,
1172    .card-title, .search-result-title, .inline-panel-title,
1173    .discord-event-title { font-family: '${C.fonts.headingFont}', serif; }
1174    .content h1 { font-family: '${C.fonts.titleFont || C.fonts.headingFont}', serif; font-size: 2.4em; letter-spacing: 0.02em; }
1175    .content code, .session-item .session-file { font-family: '${C.fonts.monoFont}', monospace; }
1176  `;
1177
1178  document.getElementById('siteTitle').textContent = C.site.title;
1179  document.getElementById('siteSubtitle').textContent = C.site.subtitle;
1180  document.title = C.site.title;
1181
1182  applyThemeVars(C.site.defaultTheme);
1183
1184  // Store button
1185  if (C.storeUrl) {
1186    const btn = document.getElementById('storeBtnEl');
1187    btn.href = C.storeUrl;
1188    btn.style.display = 'flex';
1189  }
1190
1191  // Footer
1192  document.getElementById('siteFooter').innerHTML = C.footerLinks.map(l =>
1193    `<a href="${l.url}" target="_blank">${l.label}</a>`
1194  ).join('');
1195
1196  // Restore saved theme
1197  try {
1198    const saved = localStorage.getItem('bnb-theme');
1199    if (saved) applyThemeVars(saved);
1200  } catch(e) {}
1201}
1202
1203function applyThemeVars(theme) {
1204  const t = theme === 'light' ? C.lightTheme : C.darkTheme;
1205  document.documentElement.setAttribute('data-theme', theme);
1206
1207  document.getElementById('themeStyle').textContent = `
1208    :root {
1209      --bg-primary:        ${t.bgPrimary};
1210      --bg-secondary:      ${t.bgSecondary};
1211      --bg-card:           ${t.bgCard};
1212      --bg-hover:          ${t.bgHover};
1213      --border:            ${t.border};
1214      --border-accent:     ${t.borderAccent};
1215      --text-primary:      ${t.textPrimary};
1216      --text-secondary:    ${t.textSecondary};
1217      --text-muted:        ${t.textMuted};
1218      --gold:              ${t.gold};
1219      --gold-dim:          ${t.goldDim};
1220      --gold-glow:         ${t.goldGlow};
1221      --sidebar-width:     ${C.layout.sidebarWidth};
1222      --header-height:     ${C.layout.headerHeight};
1223      --content-max-width: ${C.layout.contentMaxWidth};
1224      --mono-font:         '${C.fonts.monoFont}', monospace;
1225      --divider-symbol:    '${C.dividerSymbol}';
1226      --brown:             ${t.redAccent};
1227    }
1228    body { background: ${t.bgPrimary}; color: ${t.textPrimary}; }
1229    .site-header { background: ${t.sidebarBg || t.bgSecondary}; border-bottom: 1px solid ${t.sidebarBorder || t.border}; height: ${C.layout.headerHeight}; }
1230    .site-title { color: ${t.sidebarGold || t.gold}; text-shadow: none; }
1231    .site-subtitle { color: ${t.sidebarTextMuted || t.textMuted}; }
1232    .sidebar { background: ${t.sidebarBg || t.bgSecondary}; border-right: 1px solid ${t.sidebarBorder || t.border}; }
1233    .sidebar-section-title { color: ${t.sidebarTextMuted || t.textMuted}; }
1234    .sidebar-link { color: ${t.sidebarText || t.textSecondary}; }
1235    .sidebar-link:hover { background: ${t.sidebarHover || t.bgHover}; color: ${t.sidebarGold || t.gold}; }
1236    .sidebar-link.active { background: ${t.sidebarActiveGlow || t.goldGlow}; color: ${t.sidebarGold || t.gold}; }
1237    .guild-type-header, .tools-header, .lore-node-header { color: ${t.sidebarText || t.textSecondary}; }
1238    .guild-type-header:hover, .tools-header:hover { background: ${t.sidebarHover || t.bgHover}; color: ${t.sidebarGold || t.gold}; }
1239    .guild-type-header .arrow, .tools-header .arrow, .lore-node-header .arrow { color: ${t.sidebarTextMuted || t.textMuted}; }
1240    .lore-node-header.active-lore { color: ${t.sidebarGold || t.gold}; }
1241    .lore-node-header.leaf:hover, .lore-node-header.has-children:hover { background: ${t.sidebarHover || t.bgHover}; color: ${t.sidebarGold || t.gold}; }
1242    .guild-stars { color: ${t.sidebarGold || t.gold}; }
1243    .main-content { margin-top: ${C.layout.headerHeight}; }
1244    .search-input { background: ${t.bgPrimary}; border: 1px solid ${t.border}; color: ${t.textPrimary}; }
1245    .search-input:focus { border-color: ${t.goldDim}; }
1246    .search-input::placeholder { color: ${t.textMuted}; }
1247    .search-results { background: ${t.bgCard}; border: 1px solid ${t.border}; }
1248    .search-result-item:hover { background: ${t.bgHover}; }
1249    .search-result-title { color: ${t.gold}; }
1250    .search-result-type { color: ${t.textMuted}; }
1251    .theme-toggle { border: 1px solid ${t.border}; color: ${t.textSecondary}; }
1252    .theme-toggle:hover { border-color: ${t.goldDim}; color: ${t.gold}; }
1253    .store-btn { border: 1px solid ${t.goldDim}; color: ${t.gold}; font-family: '${C.fonts.headingFont}', serif; }
1254    .store-btn:hover { background: ${t.goldGlow}; box-shadow: none; }
1255    .content h1 { color: ${t.gold}; text-shadow: none; }
1256    .content h2 { color: ${t.textPrimary}; }
1257    .content h3, .content h4 { color: ${t.gold}; }
1258    .content strong { color: ${t.gold}; }
1259    .content a { color: ${t.gold}; }
1260    .content blockquote { color: ${t.textSecondary}; }
1261    .content code { background: ${t.goldGlow}; color: ${t.gold}; }
1262    .content ul li::before { color: ${t.goldDim}; }
1263    .content thead th { background: ${t.goldGlow}; color: ${t.gold}; }
1264    .content hr::after { background: ${t.bgPrimary}; color: ${t.goldDim}; }
1265    .guild-type-card, .session-item { background: ${t.bgCard}; border-color: ${t.border}; color: ${t.textPrimary}; }
1266    .guild-type-card:hover, .session-item:hover { border-color: ${t.goldDim}; background: ${t.bgHover}; }
1267    .guild-type-card .card-title { color: ${t.gold}; }
1268    .guild-type-card .card-desc, .guild-type-card .card-count { color: ${t.textMuted}; }
1269    .session-item .session-name { color: ${t.textPrimary}; }
1270    .session-item .session-file { color: ${t.textMuted}; }
1271    .discord-event-card { background: ${t.bgCard}; border-color: ${t.border}; border-left-color: ${t.goldDim}; color: ${t.textPrimary}; }
1272    .discord-event-card:hover { border-left-color: ${t.gold}; background: ${t.bgHover}; }
1273    .discord-event-title { color: ${t.textPrimary}; }
1274    .discord-event-meta { color: ${t.textSecondary}; }
1275    .inline-panel { background: ${t.bgSecondary}; }
1276    .inline-panel-header { background: ${t.bgSecondary}; color: ${t.textPrimary}; }
1277    .site-footer { border-top-color: ${t.border}; color: ${t.textMuted}; }
1278    .site-footer a { color: ${t.textSecondary}; }
1279    .site-footer a:hover { color: ${t.gold}; }
1280    .loading { color: ${t.textMuted}; }
1281    .roster-table th { color: ${t.textMuted}; border-bottom-color: ${t.border}; }
1282    .roster-table td { border-bottom-color: ${t.border}; color: ${t.textPrimary}; }
1283  `;
1284
1285  const btn = document.getElementById('themeIcon');
1286  if (btn) btn.innerHTML = `<use href="#icon-${theme === 'dark' ? 'sun-sm' : 'moon'}"/>`;
1287}
1288
1289function toggleTheme() {
1290  const current = document.documentElement.getAttribute('data-theme');
1291  const next = current === 'dark' ? 'light' : 'dark';
1292  applyThemeVars(next);
1293  try { localStorage.setItem('bnb-theme', next); } catch(e) {}
1294}
1295
1296
1297// ----------------------------------------------------------
1298//  DISCORD EVENTS
1299// ----------------------------------------------------------
1300async function loadDiscordEvents(container) {
1301  if (!C.discordEvents || !C.discordEvents.enabled) return;
1302
1303  const endpoint = C.discordEvents.endpoint || '/api/discord-events';
1304  const max = C.discordEvents.maxEvents || 8;
1305
1306  // Insert placeholder block at top of content
1307  const old = document.getElementById('discordEventsBlock');
1308  if (old) old.remove();
1309
1310  const block = document.createElement('div');
1311  block.id = 'discordEventsBlock';
1312  block.innerHTML = `
1313    <h2 style="display:flex;align-items:center;gap:8px">
1314      <svg width="18" height="18" style="flex-shrink:0"><use href="#icon-calendar"/></svg>
1315      Upcoming Sessions
1316    </h2>
1317    <div id="discordEventsList"><div class="loading" style="padding:20px 0;text-align:left">Loading events from Discord…</div></div>`;
1318  container.insertBefore(block, container.firstChild);
1319
1320  try {
1321    const resp = await fetch(endpoint);
1322    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
1323    const { events } = await resp.json();
1324    const listEl = document.getElementById('discordEventsList');
1325    if (!listEl) return;
1326
1327    if (!events || events.length === 0) {
1328      listEl.innerHTML = `<p style="opacity:0.6;font-style:italic">No upcoming sessions scheduled right now. Check the Discord Events tab or <a href="https://discord.gg/barrowsandborderlands" target="_blank">join the server</a> to get notified.</p>`;
1329      return;
1330    }
1331
1332    listEl.innerHTML = events.slice(0, max).map(e => {
1333      const start = new Date(e.start);
1334      const dateStr = start.toLocaleDateString('en-US', { weekday:'long', month:'long', day:'numeric' });
1335      const timeStr = start.toLocaleTimeString('en-US', { hour:'numeric', minute:'2-digit', timeZoneName:'short' });
1336      const isActive = e.status === 'active';
1337      const badge = isActive ? `<span style="background:var(--green-accent,#3a7a3a);color:#fff;font-size:0.72em;padding:2px 8px;border-radius:12px;font-weight:700;letter-spacing:1px;text-transform:uppercase">LIVE</span>` : '';
1338      const desc = e.description ? `<div style="font-size:0.88em;opacity:0.7;margin-top:4px;max-width:600px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${e.description.slice(0,120)}${e.description.length>120?'…':''}</div>` : '';
1339      const rsvp = e.userCount >
1339 0 ? `<span style="font-size:0.78em;opacity:0.55;margin-left:auto;flex-shrink:0">${e.userCount} attending</span>` : '';
1340
1341      return `<a class="discord-event-card" href="${e.url}" target="_blank">
1342        <div class="discord-event-icon">${isActive ? '🔴' : '⚔️'}</div>
1343        <div class="discord-event-info">
1344          <div class="discord-event-title" style="display:flex;align-items:center;gap:8px">${e.name} ${badge}</div>
1345          <div class="discord-event-meta">${dateStr} · ${timeStr}${e.location ? ' · ' + e.location : ''}</div>
1346          ${desc}
1347        </div>
1348        ${rsvp}
1349        <div class="discord-event-arrow">›</div>
1350      </a>`;
1351    }).join('');
1352
1353  } catch(err) {
1354    const listEl = document.getElementById('discordEventsList');
1355    if (listEl) {
1356      // Show helpful message — likely not yet set up
1357      listEl.innerHTML = `
1358        <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:20px 24px;">
1359          <p style="font-weight:600;margin-bottom:8px">Discord Events not yet connected</p>
1360          <p style="font-size:0.9em;opacity:0.7">To show live Discord events here, complete the one-time bot setup in <code>config.js</code>.</p>
1361          <a href="https://discord.gg/barrowsandborderlands" target="_blank" style="display:inline-block;margin-top:12px">View events on Discord →</a>
1362        </div>`;
1363    }
1364  }
1365}
1366
1367// ----------------------------------------------------------
1368//  CHARACTER ROSTER
1369// ----------------------------------------------------------
1370async function showRosterPage() {
1371  showMainContent();
1372  const content = document.getElementById('content');
1373  content.innerHTML = `
1374    <h1>📜 Character Roster</h1>
1375    <p class="roster-count">Loading characters…</p>
1376    <div class="roster-table-wrap">
1377      <table class="roster-table">
1378        <thead>
1379          <tr>
1380            <th>Character</th>
1381            <th>Player</th>
1382            <th>Race</th>
1383            <th>Class</th>
1384            <th>Lv</th>
1385            <th>XP</th>
1386            <th>Location</th>
1387            <th>Status</th>
1388          </tr>
1389        </thead>
1390        <tbody id="rosterBody">
1391          <tr><td colspan="8" class="roster-empty">Loading…</td></tr>
1392        </tbody>
1393      </table>
1394    </div>`;
1395
1396  try {
1397    const resp = await fetch('/api/character-roster');
1398    if (!resp.ok) throw new Error(`${resp.status}`);
1399    const data = await resp.json();
1400
1401    const tbody = document.getElementById('rosterBody');
1402    const countEl = content.querySelector('.roster-count');
1403
1404    if (!data.characters || data.characters.length === 0) {
1405      tbody.innerHTML = `<tr><td colspan="8" class="roster-empty">No characters registered yet. Use <code>/character add</code> in Discord to get started.</td></tr>`;
1406      if (countEl) countEl.textContent = '0 characters';
1407      return;
1408    }
1409
1410    if (countEl) countEl.textContent = `${data.count} character${data.count !== 1 ? 's' : ''} across the campaign`;
1411
1412    tbody.innerHTML = data.characters.map(c => {
1413      const statusEmoji = c.status === 'training' ? '📚' : c.status === 'traveling' ? '🗺️' : '⚔️';
1414      let statusHtml = `<span class="roster-status ${c.status}">${statusEmoji} ${c.status.charAt(0).toUpperCase() + c.status.slice(1)}</span>`;
1415
1416      if (c.status !== 'active' && c.lockUntil) {
1417        const remaining = new Date(c.lockUntil).getTime() - Date.now();
1418        if (remaining > 0) {
1419          const days  = Math.floor(remaining / 86400000);
1420          const hours = Math.floor((remaining % 86400000) / 3600000);
1421          let timeStr = days > 0 ? `${days}d ${hours}h` : `${hours}h`;
1422          let detail = timeStr + ' left';
1423          if (c.status === 'traveling' && c.destination) detail += ` → ${c.destination}`;
1424          statusHtml += `<div class="roster-lock-info">🔒 ${detail}</div>`;
1425        }
1426      }
1427
1428      return `<tr>
1429        <td><strong>${c.name}</strong></td>
1430        <td>${c.player}</td>
1431        <td>${c.race}</td>
1432        <td>${c.class}</td>
1433        <td>${c.level}</td>
1434        <td>${c.xp.toLocaleString()}</td>
1435        <td>${c.location || '—'}</td>
1436        <td>${statusHtml}</td>
1437      </tr>`;
1438    }).join('');
1439
1440  } catch(e) {
1441    const tbody = document.getElementById('rosterBody');
1442    if (tbody) {
1443      tbody.innerHTML = `<tr><td colspan="8" class="roster-empty">Could not load roster. The character system may not be set up yet.</td></tr>`;
1444    }
1445    console.warn('[Roster]', e.message);
1446  }
1447}
1448
1449// ----------------------------------------------------------
1450//  SIDEBAR BUILDER
1451// ----------------------------------------------------------
1452function buildSidebar() {
1453  const sidebar = document.getElementById('sidebar');
1454  let html = `
1455    <div class="sidebar-search">
1456      <input type="text" id="sidebarSearchInput" placeholder="Search the wiki…"
1457        oninput="handleMobileSearch(this.value)"
1458        onfocus="handleMobileSearch(this.value)"
1459        onblur="setTimeout(()=>closeMobileSearch(),200)">
1460      <div class="search-results" id="sidebarSearchResults"></div>
1461    </div>`;
1462  html += `<div class="sidebar-section"><div class="sidebar-section-title">Wiki</div>`;
1463
1464  // Nav entries with a sidebarFolder are grouped into collapsible folders
1465  const navList = C.nav.filter(s => s.type !== 'guilds' && s.type !== 'lore'); // guilds & lore rendered separately below
1466  let curNavFolder = null;
1467  for (const section of navList) {
1468    const f = section.sidebarFolder;
1469    if (curNavFolder && (!f || f.id !== curNavFolder)) {
1470      html += `</div></div>`;
1471      curNavFolder = null;
1472    }
1473    if (f && f.id !== curNavFolder) {
1474      curNavFolder = f.id;
1475      html += `
1476      <div class="tools-group">
1477        <div class="tools-header" id="tools-header-nf-${f.id}" onclick="toggleNavFolder('${f.id}')">
1478          ${icon(f.icon)}
1479          <span>${f.label}</span>
1480          ${icon('chevron-right', 12)}
1481        </div>
1482        <div class="tools-children" id="tools-children-nf-${f.id}">`;
1483    }
1484    html += `
1485      <div class="sidebar-link" id="nav-${section.id}" onclick="navigateTo('${section.id}')">
1486        ${icon(section.icon)}
1487        <span>${section.label}</span>
1488      </div>`;
1489  }
1490  if (curNavFolder) html += `</div></div>`;
1491
1492  // Campaign Tools collapsible
1493  if (C.campaignTools && C.campaignTools.length > 0) {
1494    html += `
1495      <div class="tools-group">
1496        <div class="tools-header" id="tools-header-main" onclick="toggleTools()">
1497          ${icon('tools')}
1498          <span>Campaign Tools</span>
1499          ${icon('chevron-right', 12)}
1500        </div>
1501        <div class="tools-children" id="tools-children-main">`;
1502
1503    for (let i = 0; i < C.campaignTools.length; i++) {
1504      const tool = C.campaignTools[i];
1505      if (tool.children && tool.children.length > 0) {
1506        // Sub-folder: collapsible group nested inside Campaign Tools
1507        html += `
1508        <div class="tools-group">
1509          <div class="tools-header" id="tools-sub-header-${i}" onclick="toggleToolsSub(${i})">
1510            ${icon(tool.icon)}
1511            <span>${tool.label}</span>
1512            ${icon('chevron-right', 12)}
1513          </div>
1514          <div class="tools-children" id="tools-sub-children-${i}">`;
1515        for (let j = 0; j < tool.children.length; j++) {
1516          const child = tool.children[j];
1517          html += `
1518            <div class="sidebar-link" onclick="openSubTool(${i}, ${j})">
1519              ${icon(child.icon)}
1520              <span>${child.label}</span>
1521            </div>`;
1522        }
1523        html += `</div></div>`;
1524      } else {
1525        html += `
1526        <div class="sidebar-link" onclick="openTool(${i})">
1527          ${icon(tool.icon)}
1528          <span>${tool.label}</span>
1529        </div>`;
1530      }
1531    }
1532
1533    html += `</div></div>`;
1534  }
1535
1536  // Extra tool folders — sibling dropdowns beneath Campaign Tools
1537  if (C.toolFolders && C.toolFolders.length > 0) {
1538    for (let k = 0; k < C.toolFolders.length; k++) {
1539      const folder = C.toolFolders[k];
1540      html += `
1541      <div class="tools-group">
1542        <div class="tools-header" id="tools-header-g${k}" onclick="toggleToolFolder(${k})">
1543          ${icon(folder.icon)}
1544          <span>${folder.label}</span>
1545          ${icon('chevron-right', 12)}
1546        </div>
1547        <div class="tools-children" id="tools-children-g${k}">`;
1548      for (let j = 0; j < (folder.tools || []).length; j++) {
1549        const tool = folder.tools[j];
1550        html += `
1551          <div class="sidebar-link" onclick="openFolderTool(${k}, ${j})">
1552            ${icon(tool.icon)}
1553            <span>${tool.label}</span>
1554          </div>`;
1555      }
1556      html += `</div></div>`;
1557    }
1558  }
1559
1560  html += `</div>`;
1561
1562  // Lore Wiki tree section — built dynamically from _order.txt files
1563  const loreSection = C.nav.find(s => s.type === 'lore');
1564  if (loreSection) {
1565    html += `<div class="sidebar-section" id="loreSidebarSection"><div class="sidebar-section-title">Lore Wiki</div><div id="loreTreeMount"><div style="padding:8px 10px;font-size:0.8em;opacity:0.4">Loading…</div></div></div>`;
1566  }
1567
1568  // Guilds section — split into Firnum and Thernn worlds
1569  const guildsSection = C.nav.find(s => s.type === 'guilds');
1570  if (guildsSection) {
1571    const worlds = [
1572      { key: 'firnum', label: 'Firnum Guilds' },
1573      { key: 'thernn', label: 'Thernn Guilds' },
1574    ];
1575    html += `<div class="sidebar-section">
1576      <div class="sidebar-section-title" style="cursor:pointer" onclick="navigateGuildOverview()">Guilds</div>
1577      <div class="sidebar-link" onclick="navigateGuildOverview()" style="margin-bottom:4px">
1578        ${icon('scroll')}
1579        <span>Guild Overview</span>
1580      </div>`;
1581
1582    for (const world of worlds) {
1583      const worldTypes = C.guildTypes.filter(t => t.world === world.key);
1584      const worldGuildCount = C.guilds.filter(g => worldTypes.some(t => t.id === g.type)).length;
1585      html += `
1586        <div class="guild-world-group">
1587          <div class="guild-world-header" onclick="toggleWorldGroup('${world.key}')" id="world-hdr-${world.key}">
1588            ${icon('shield')}
1589            <span>${world.label}</span>
1590            <span style="font-size:0.72em;opacity:0.45;margin-left:3px">(${worldGuildCount})</span>
1591            ${icon('chevron-right', 10)}
1592          </div>
1593          <div class="guild-world-children" id="world-${world.key}">`;
1594
1595      for (const type of worldTypes) {
1596        const guilds = C.guilds.filter(g => g.type === type.id);
1597        html += `<div class="guild-type-group">
1598          <div class="guild-type-header">
1599            <span class="guild-type-label" onclick="showGuildTypeOverview('${type.id}');toggleGuildType('${type.id}')">
1600              ${icon(type.icon)}
1601              <span>${type.name}</span>
1602              <span style="font-size:0.72em;opacity:0.45;margin-left:3px">(${guilds.length})</span>
1603            </span>
1604            ${guilds.length > 0 ? `<span class="guild-type-chevron" onclick="toggleGuildType('${type.id}')">${icon('chevron-right', 10)}</span>` : ''}
1605          </div>`;
1606
1607        if (guilds.length > 0) {
1608          html += `<div class="guild-type-children" id="type-${type.id}">`;
1609          for (const guild of guilds) {
1610            html += `
1611              <div class="sidebar-link" onclick="navigateGuild('${type.id}','${guild.file}')" data-guild="${guild.file}">
1612                <span>${guild.name}</span>
1613                <span class="guild-stars">${'★'.repeat(guild.stars)}</span>
1614              </div>`;
1615          }
1616          html += `</div>`;
1617        }
1618        html += `</div>`;
1619      }
1620
1621      html += `</div></div>`; // close world-children + world-group
1622    }
1623
1624    html += `</div>`; // close sidebar-section
1625  }
1626
1627  sidebar.innerHTML = html;
1628
1629  // Kick off async lore tree build after sidebar is in DOM
1630  if (loreSection) buildLoreTreeFromFS(loreSection.folder);
1631}
1632
1633// ----------------------------------------------------------
1634//  LORE TREE — FILE SYSTEM DRIVEN
1635//
1636//  Structure:
1637//    lore/
1638//      _order.txt          ← defines order + labels for this folder
1639//      Firnum.md           ← a page
1640//      Firnum/             ← subfolder = children of Firnum
1641//        _order.txt
1642//        Malores.md
1643//        The-Borderlands.md
1644//        The-Borderlands/
1645//          _order.txt
1646//          Decima.md
1647//
1648//  _order.txt format (one entry per line):
1649//    Filename.md | Display Label | icon-name
1650//    FolderName  | (expand)          ← marks a folder as expandable (no .md)
1651//
1652//  If no _order.txt exists, files show alphabetically with auto-labels.
1653//  To add a new page: drop a .md file in the right folder. Done.
1654//  To nest it: put it in a subfolder. Add it to _order.txt for ordering.
1655// ----------------------------------------------------------
1656
1657async function parseOrderFile(folder) {
1658  const text = await loadMarkdown(`${folder}/_order.txt`);
1659  if (!text) return null;
1660  return text.split('\n')
1661    .map(l => l.trim()).filter(Boolean)
1662    .map(l => {
1663      const parts = l.split('|').map(p => p.trim());
1664      return { name: parts[0], label: parts[1] || null, icon: parts[2] || 'scroll' };
1665    });
1666}
1667
1668function slugify(str) {
1669  return str.replace(/\.md$/, '').replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase();
1670}
1671
1672function autoLabel(filename) {
1673  return filename.replace(/\.md$/, '').replace(/-/g, ' ');
1674}
1675
1676// Recursively builds lore tree HTML for a folder, fully async
1677async function buildLoreFolderHTML(folder, depth) {
1678  const entries = await parseOrderFile(folder);
1679  if (!entries) return '';
1680
1681  let html = '';
1682  for (const entry of entries) {
1683    if (entry.label === '(expand)') continue; // folder-only marker, skip
1684    if (!entry.name.endsWith('.md')) continue; // only process .md files here
1685
1686    const filePath = `${folder}/${entry.name}`;
1687    const nodeId = slugify(filePath);
1688    const depthClass = depth > 0 ? `lore-depth-${Math.min(depth, 3)}` : '';
1689    const label = entry.label || autoLabel(entry.name);
1690    const iconName = entry.icon || 'scroll';
1691
1692    // Check if a same-named subfolder exists (has its own _order.txt)
1693    const subfolderName = entry.name.replace(/\.md$/, '');
1694    const subfolderPath = `${folder}/${subfolderName}`;
1695    const subEntries = await parseOrderFile(subfolderPath);
1696    const hasChildren = subEntries && subEntries.length > 0;
1697
1698    if (hasChildren) {
1699      // Recursively build children now (fully async, no placeholders)
1700      const childrenHtml = await buildLoreFolderHTML(subfolderPath, depth + 1);
1701      html += `<div class="lore-tree-node">
1702        <div class="lore-node-header has-children ${depthClass}" id="lore-hdr-${nodeId}"
1703          onclick="toggleLoreNode('${nodeId}', '${filePath}')">
1704          ${icon(iconName)}
1705          <span>${label}</span>
1706          ${icon('chevron-right', 10)}
1707        </div>
1708        <div class="lore-node-children" id="lore-children-${nodeId}">
1709          ${childrenHtml}
1710        </div>
1711      </div>`;
1712    } else {
1713      html += `<div class="lore-tree-node">
1714        <div class="lore-node-header leaf ${depthClass}" id="lore-hdr-${nodeId}"
1715          onclick="navigateLore('${nodeId}', '${filePath}')">
1716          ${icon(iconName)}
1717          <span>${label}</span>
1718        </div>
1719      </div>`;
1720    }
1721  }
1722  return html;
1723}
1724
1725async function buildLoreTreeFromFS(loreFolder) {
1726  const html = await buildLoreFolderHTML(loreFolder, 0);
1727  const mount = document.getElementById('loreTreeMount');
1728  if (mount) mount.innerHTML = html || '<div style="padding:8px 10px;font-size:0.8em;opacity:0.4">No lore entries yet.</div>';
1729}
1730
1731function toggleLoreNode(nodeId, filePath) {
1732  const children = document.getElementById('lore-children-' + nodeId);
1733  const header = document.getElementById('lore-hdr-' + nodeId);
1734  if (!children) return;
1735  const isOpen = children.classList.toggle('open');
1736  header.classList.toggle('open', isOpen);
1737  navigateLore(nodeId, filePath);
1738}
1739
1740async function navigateLore(nodeId, filePath) {
1741  document.getElementById('sidebar').classList.remove('open');
1742  hideFullscreen();
1743  showMainContent();
1744  window.scrollTo(0, 0);
1745
1746  document.querySelectorAll('.lore-node-header').forEach(el => el.classList.remove('active-lore'));
1747  const hdr = document.getElementById('lore-hdr-' + nodeId);
1748  if (hdr) hdr.classList.add('active-lore');
1749  document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active'));
1750
1751  const content = document.getElementById('content');
1752  content.innerHTML = '<div class="loading">Loading…</div>';
1753
1754  const md = await loadMarkdown(filePath);
1755  content.innerHTML = md
1756    ? renderMarkdown(md)
1757    : `<h1>${filePath.split('/').pop().replace('.md','').replace(/-/g,' ')}</h1><p style="opacity:0.5">No content yet. Create <code>${filePath}</code> to add content.</p>`;
1758  interceptEmbedLinks(content);
1759}
1760
1761function toggleTools() {
1762  const el = document.getElementById('tools-children-main');
1763  const header = document.getElementById('tools-header-main');
1764  if (!el) return;
1765  el.classList.toggle('open');
1766  header.classList.toggle('open');
1767}
1768
1769function openTool(index) {
1770  const tool = C.campaignTools[index];
1771  if (!tool) return;
1772  if (tool.type === 'external') {
1773    window.open(tool.url, '_blank');
1774  } else {
1775    openFullscreen(tool.url, tool.label);
1776  }
1777}
1778
1779function toggleToolsSub(i) {
1780  const el = document.getElementById('tools-sub-children-' + i);
1781  const header = document.getElementById('tools-sub-header-' + i);
1782  if (!el) return;
1783  el.classList.toggle('open');
1784  header.classList.toggle('open');
1785}
1786
1787function openSubTool(i, j) {
1788  const tool = ((C.campaignTools[i] || {}).children || [])[j];
1789  if (!tool) return;
1790  if (tool.type === 'external') {
1791    window.open(tool.url, '_blank');
1792  } else {
1793    openFullscreen(tool.url, tool.label);
1794  }
1795}
1796
1797function toggleNavFolder(id) {
1798  const el = document.getElementById('tools-children-nf-' + id);
1799  const header = document.getElementById('tools-header-nf-' + id);
1800  if (!el) return;
1801  el.classList.toggle('open');
1802  header.classList.toggle('open');
1803}
1804
1805function toggleToolFolder(k) {
1806  const el = document.getElementById('tools-children-g' + k);
1807  const header = document.getElementById('tools-header-g' + k);
1808  if (!el) return;
1809  el.classList.toggle('open');
1810  header.classList.toggle('open');
1811}
1812
1813function openFolderTool(k, j) {
1814  const tool = ((C.toolFolders[k] || {}).tools || [])[j];
1815  if (!tool) return;
1816  if (tool.type === 'external') {
1817    window.open(tool.url, '_blank');
1818  } else {
1819    openFullscreen(tool.url, tool.label);
1820  }
1821}
1822
1823// Flattens campaignTools (including sub-folder children) and toolFolders
1824// for search. Each entry carries an _open() action that opens the tool.
1825function flatCampaignTools(afterOpen) {
1826  const base = (C.campaignTools || []).flatMap((t, i) =>
1827    (t.children && t.children.length > 0)
1828      ? t.children.map((c, j) => ({ ...c, _open: () => { openSubTool(i, j); if (afterOpen) afterOpen(); } }))
1829      : [{ ...t, _open: () => { openTool(i); if (afterOpen) afterOpen(); } }]
1830  );
1831  const folderTools = (C.toolFolders || []).flatMap((f, k) =>
1832    (f.tools || []).map((t, j) => ({ ...t, _open: () => { openFolderTool(k, j); if (afterOpen) afterOpen(); } }))
1833  );
1834  return [...base, ...folderTools];
1835}
1836
1837function toggleWorldGroup(worldKey) {
1838  const el  = document.getElementById('world-' + worldKey);
1839  const hdr = document.getElementById('world-hdr-' + worldKey);
1840  if (!el) return;
1841  el.classList.toggle('open');
1842  if (hdr) hdr.classList.toggle('open');
1843}
1844
1845function toggleGuildType(typeId) {
1846  const el = document.getElementById('type-' + typeId);
1847  if (!el) return;
1848  el.classList.toggle('open');
1849  el.previousElementSibling.classList.toggle('open');
1850}
1851
1852function toggleSidebar() {
1853  document.getElementById('sidebar').classList.toggle('open');
1854}
1855
1856function toggleDesktopSidebar() {
1857  const body = document.body;
1858  const btn = document.getElementById('sidebarCollapseBtn');
1859  body.classList.toggle('sidebar-collapsed');
1860  const collapsed = body.classList.contains('sidebar-collapsed');
1861  btn.style.transform = collapsed ? 'rotate(0deg)' : 'rotate(180deg)';
1862  try { localStorage.setItem('bnb_sidebar_collapsed', collapsed ? '1' : ''); } catch {}
1863}
1864
1865// Restore sidebar collapsed state
1866(function() {
1867  try {
1868    if (localStorage.getItem('bnb_sidebar_collapsed') === '1') {
1869      document.body.classList.add('sidebar-collapsed');
1870      const btn = document.getElementById('sidebarCollapseBtn');
1871      if (btn) btn.style.transform = 'rotate(0deg)';
1872    } else {
1873      const btn = document.getElementById('sidebarCollapseBtn');
1874      if (btn) btn.style.transform = 'rotate(180deg)';
1875    }
1876  } catch {}
1877})();
1878
1879function setActiveNav(id) {
1880  document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active'));
1881  const el = document.getElementById('nav-' + id);
1882  if (el) el.classList.add('active');
1883}
1884
1885// ----------------------------------------------------------
1886//  FULL-SCREEN EMBED (map, character creator, tools)
1887// ----------------------------------------------------------
1888function openFullscreen(url, title) {
1889  document.getElementById('sidebar').classList.remove('open');
1890  hideFullscreen();
1891  document.getElementById('mainContent').style.display = 'none';
1892  document.getElementById('siteFooter').style.display = 'none';
1893  const embed = document.getElementById('fullscreenEmbed');
1894  document.getElementById('fullscreenIframe').src = url;
1895  embed.style.display = 'block';
1896  // Store current section so back button could restore it
1897  window._lastSection = window._currentSection;
1898}
1899
1900function hideFullscreen() {
1901  document.getElementById('fullscreenEmbed').style.display = 'none';
1902  document.getElementById('fullscreenIframe').src = '';
1903}
1904
1905// ----------------------------------------------------------
1906//  INLINE MODAL PANEL (attendance form, etc.)
1907// ----------------------------------------------------------
1908function openInlinePanel(url, title) {
1909  document.getElementById('inlinePanelTitle').textContent = title;
1910  document.getElementById('inlinePanelIframe').src = url;
1911  document.getElementById('inlinePanelOverlay').classList.add('open');
1912}
1913
1914function closeInlinePanel(event) {
1915  if (event.target === document.getElementById('inlinePanelOverlay')) {
1916    closeInlinePanelDirect();
1917  }
1918}
1919
1920function closeInlinePanelDirect() {
1921  document.getElementById('inlinePanelOverlay').classList.remove('open');
1922  document.getElementById('inlinePanelIframe').src = '';
1923}
1924
1925// ----------------------------------------------------------
1926//  NAVIGATION
1927// ----------------------------------------------------------
1928window._currentSection = null;
1929
1930async function navigateTo(sectionId, skipHash) {
1931  document.getElementById('sidebar').classList.remove('open');
1932  const section = C.nav.find(s => s.id === sectionId);
1933  if (!section) return;
1934
1935  // Update URL hash for deep linking & browser history
1936  if (!skipHash) {
1937    const target = '#' + sectionId;
1938    if (window.location.hash !== target) {
1939      history.pushState(null, '', target);
1940    }
1941  }
1942
1943  window._currentSection = sectionId;
1944  hideFullscreen();
1945  showMainContent();
1946  setActiveNav(sectionId);
1947  window.scrollTo(0, 0);
1948
1949  if (section.type === 'map') {
1950    openFullscreen(C.mapUrl, 'Map of Firnum');
1951    return;
1952  }
1953
1954  if (section.type === 'embed') {
1955    openFullscreen(section.url, section.label);
1956    return;
1957  }
1958
1959  if (section.type === 'lore') {
1960    // Navigate to the root lore page (first .md in the lore folder)
1961    const loreFolder = section.folder || 'lore';
1962    const rootEntries = await parseOrderFile(loreFolder);
1963    if (rootEntries) {
1964      const first = rootEntries.find(e => e.name.endsWith('.md'));
1965      if (first) {
1966        const filePath = `${loreFolder}/${first.name}`;
1967        const nodeId = slugify(filePath);
1968        // Open the root node's children if it has a subfolder
1969        const subEntries = await parseOrderFile(`${loreFolder}/${first.name.replace('.md','')}`);
1970        if (subEntries) {
1971          const ch = document.getElementById('lore-children-' + nodeId);
1972          const hd = document.getElementById('lore-hdr-' + nodeId);
1973          if (ch) ch.classList.add('open');
1974          if (hd) hd.classList.add('open');
1975        }
1976        navigateLore(nodeId, filePath);
1977      }
1978    }
1979    return;
1980  }
1981
1982  if (section.type === 'guilds') {
1983    showGuildDirectory();
1984    return;
1985  }
1986
1987  if (section.type === 'markdown') {
1988    const content = document.getElementById('content');
1989    content.innerHTML = '<div class="loading">Loading…</div>';
1990    const md = await loadMarkdown(`${section.folder}/${section.file}`);
1991    if (md) {
1992      content.innerHTML = renderMarkdown(md);
1993      interceptEmbedLinks(content);
1994      // Inject live Discord events on Upcoming Sessions page
1995      if (sectionId === 'sessions' && C.discordEvents && C.discordEvents.enabled && C.discordEvents.showOnSessions) {
1996        loadDiscordEvents(content);
1997      }
1998    } else {
1999      content.innerHTML = `<h1>${section.label}</h1><p>File not found: <code>${section.folder}/${section.file}</code></p>`;
2000    }
2001    return;
2002  }
2003
2004  if (section.type === 'index') {
2005    showIndexPage(section);
2006    return;
2007  }
2008
2009  if (section.type === 'roster') {
2010    showRosterPage();
2011    return;
2012  }
2013}
2014
2015
2016
2017async function navigateGuild(typeId, file) {
2018  document.getElementById('sidebar').classList.remove('open');
2019  hideFullscreen();
2020  showMainContent();
2021  window.scrollTo(0, 0);
2022
2023  const content = document.getElementById('content');
2024  content.innerHTML = '<div class="loading">Loading guild data…</div>';
2025
2026  const typeEl = document.getElementById('type-' + typeId);
2027  if (typeEl) {
2028    typeEl.classList.add('open');
2029    typeEl.previousElementSibling.classList.add('open');
2030  }
2031  document.querySelectorAll(`.sidebar-link[data-guild="${file}"]`).forEach(el => el.classList.add('active'));
2032
2033  // Use the folder path from guildTypes config (supports Thernn subfolders)
2034  const guildType = C.guildTypes.find(t => t.id === typeId);
2035  const folder = guildType?.folder || `guilds/${typeId}`;
2036
2037  // Also expand the parent world group if this is a Thernn guild
2038  if (guildType?.world === 'thernn') {
2039    const worldEl = document.getElementById('world-thernn');
2040    const worldHdr = document.getElementById('world-hdr-thernn');
2041    if (worldEl) { worldEl.classList.add('open'); }
2042    if (worldHdr) { worldHdr.classList.add('open'); }
2043  } else {
2044    const worldEl = document.getElementById('world-firnum');
2045    const worldHdr = document.getElementById('world-hdr-firnum');
2046    if (worldEl) { worldEl.classList.add('open'); }
2047    if (worldHdr) { worldHdr.classList.add('open'); }
2048  }
2049
2050  const md = await loadMarkdown(`${folder}/${file}`);
2051  if (md) {
2052    content.innerHTML = renderMarkdown(md);
2053    interceptEmbedLinks(content);
2054  } else {
2055    const guild = C.guilds.find(g => g.file === file);
2056    content.innerHTML = `<h1>${guild ? guild.name : 'Guild'}</h1><p>File not found: <code>${folder}/${file}</code></p>`;
2057  }
2058}
2059
2060function navigateGuildOverview() {
2061  document.getElementById('sidebar').classList.remove('open');
2062  hideFullscreen();
2063  showMainContent();
2064  const content = document.getElementById('content');
2065  content.innerHTML = `<div class="loading">Loading…</div>`;
2066  loadMarkdown('guilds/overview/Guild-Overview.md').then(md => {
2067    if (md) content.innerHTML = renderMarkdown(md);
2068    else content.innerHTML = `<h1>Guild Overview</h1><p>No overview file found.</p>`;
2069  });
2070  setActiveNav('guild-overview');
2071}
2072
2073async function showGuildTypeOverview(typeId) {
2074  document.getElementById('sidebar').classList.remove('open');
2075  hideFullscreen();
2076  showMainContent();
2077  const content = document.getElementById('content');
2078
2079  const type = C.guildTypes.find(t => t.id === typeId);
2080  if (!type) return;
2081
2082  const guilds = C.guilds.filter(g => g.type === typeId);
2083  const worldLabel = type.world === 'thernn' ? 'Thernn' : 'Firnum';
2084
2085  // Show loading state
2086  content.innerHTML = `<div class="loading">Loading…</div>`;
2087
2088  // Try to load an overview markdown file from the type's folder
2089  // e.g. guilds/Fighting-Types/overview.md
2090  const overviewMd = await loadMarkdown(`${type.folder}/overview.md`);
2091
2092  let html = '';
2093  if (overviewMd) {
2094    // Use the markdown file as the main content
2095    html = renderMarkdown(overviewMd);
2096  } else {
2097    // Fallback: simple generated header + desc
2098    html = `<h1>${type.name} Guilds — ${worldLabel}</h1>
2099    <p style="font-style:italic;opacity:0.6;margin-bottom:18px">${worldLabel} · ${guilds.length} guild${guilds.length !== 1 ? 's' : ''} registered</p>
2100    <p>${type.desc}</p>
2101    <p style="opacity:0.45;font-size:0.85em;margin-top:18px">
2102      <em>To customise this page, create <code>${type.folder}/overview.md</code></em>
2103    </p>`;
2104  }
2105
2106  // Always append the guild card grid at the bottom
2107  if (guilds.length > 0) {
2108    html += `<h2>Registered Guilds</h2>
2109      <div class="guild-directory-grid">`;
2110    for (const guild of guilds) {
2111      const stars = '★'.repeat(guild.stars) + '☆'.repeat(Math.max(0, 6 - guild.stars));
2112      html += `
2113        <div class="guild-type-card" onclick="navigateGuild('${typeId}','${guild.file}')">
2114          <div class="card-title">${guild.name}</div>
2115          <div class="card-desc guild-stars" style="font-size:1.05em;letter-spacing:2px">${stars}</div>
2116        </div>`;
2117    }
2118    html += `</div>`;
2119  } else {
2120    html += `<p style="opacity:0.5;margin-top:20px"><em>No guilds registered in this category yet.</em></p>`;
2121  }
2122
2123  content.innerHTML = html;
2124  setActiveNav('guilds');
2125
2126  // Expand the world group in the sidebar
2127  if (type.world) {
2128    const worldEl = document.getElementById(`world-${type.world}`);
2129    const worldHdr = document.getElementById(`world-hdr-${type.world}`);
2130    if (worldEl && !worldEl.classList.contains('open')) {
2131      worldEl.classList.add('open');
2132      if (worldHdr) worldHdr.classList.add('open');
2133    }
2134  }
2135}
2136
2137function showGuildDirectory() {
2138  showMainContent();
2139  const content = document.getElementById('content');
2140
2141  const worlds = [
2142    { key: 'firnum', label: 'Firnum Guilds', desc: 'Guilds operating within the Borderlands of Firnum.' },
2143    { key: 'thernn', label: 'Thernn Guilds', desc: 'Guilds and organisations from the world of Thernn.' },
2144  ];
2145
2146  let html = `<h1>Guild Registry</h1>
2147    <blockquote>A living record of every Guild across the known worlds.</blockquote>`;
2148
2149  for (const world of worlds) {
2150    const worldTypes = C.guildTypes.filter(t => t.world === world.key);
2151    const totalGuilds = C.guilds.filter(g => worldTypes.some(t => t.id === g.type)).length;
2152    html += `<h2>${world.label} <span style="font-size:0.65em;opacity:0.5;font-weight:400;">${totalGuilds} guilds</span></h2>
2153      <p style="margin-bottom:14px;opacity:0.7;">${world.desc}</p>
2154      <div class="guild-directory-grid">`;
2155    for (const type of worldTypes) {
2156      const guilds = C.guilds.filter(g => g.type === type.id);
2157      html += `
2158        <div class="guild-type-card" onclick="toggleGuildType('${type.id}');toggleWorldGroup('${world.key}')">
2159          <div class="card-title">${icon(type.icon, 15)} ${type.name}</div>
2160          <div class="card-desc">${type.desc}</div>
2161          <div class="card-count">${guilds.length} guild${guilds.length !== 1 ? 's' : ''} registered</div>
2162        </div>`;
2163    }
2164    html += `</div>`;
2165  }
2166
2167  html += `<h2>Start a Guild</h2>
2168    <p>Copy the template from <code>templates/Guild-Sheet-Template.md</code>, fill it in, and add it to <code>config.js</code>.</p>`;
2169
2170  content.innerHTML = html;
2171  setActiveNav('guilds');
2172}
2173
2174async function showIndexPage(section) {
2175  showMainContent();
2176  const content = document.getElementById('content');
2177  content.innerHTML = `<div class="loading">Loading ${section.label}…</div>`;
2178
2179  const indexMd = await loadMarkdown(`${section.folder}/index.md`);
2180  if (indexMd) {
2181    content.innerHTML = renderMarkdown(indexMd);
2182    return;
2183  }
2184
2185  content.innerHTML = `
2186    <h1>${section.label}</h1>
2187    <p>Add <code>.md</code> files to the <code>${section.folder}/</code> folder to populate this section.</p>
2188    <div class="session-list" id="indexList"><div class="loading">Scanning for files…</div></div>`;
2189
2190  await autoScanIndex(section);
2191}
2192
2193async function autoScanIndex(section) {
2194  const manifest = await loadMarkdown(`${section.folder}/manifest.txt`);
2195  const listEl = document.getElementById('indexList');
2196  if (!listEl) return;
2197
2198  if (manifest) {
2199    const files = manifest.split('\n').map(f => f.trim()).filter(f => f && f.endsWith('.md'));
2200    if (files.length === 0) {
2201      listEl.innerHTML = '<p>No entries yet. Add <code>.md</code> files and list them in <code>manifest.txt</code>.</p>';
2202      return;
2203    }
2204    listEl.innerHTML = files.map(f => {
2205      const name = f.replace('.md','').replace(/-/g,' ');
2206      return `<div class="session-item" onclick="loadIndexFile('${section.folder}','${f}')">
2207        <span class="session-name">${name}</span>
2208        <span class="session-file">${f}</span>
2209      </div>`;
2210    }).join('');
2211  } else {
2212    listEl.innerHTML = `
2213      <p>Create a file called <code>${section.folder}/manifest.txt</code> listing one filename per line to show entries here:</p>
2214      <pre style="background:var(--bg-card);padding:12px;border-radius:6px;font-family:var(--mono-font);font-size:0.85em;margin-top:8px">Entry-One.md
2215Entry-Two.md</pre>`;
2216  }
2217}
2218
2219async function loadIndexFile(folder, file) {
2220  showMainContent();
2221  const content = document.getElementById('content');
2222  content.innerHTML = '<div class="loading">Loading…</div>';
2223  window.scrollTo(0, 0);
2224  const md = await loadMarkdown(`${folder}/${file}`);
2225  content.innerHTML = md ? renderMarkdown(md) : `<p>File not found: <code>${folder}/${file}</code></p>`;
2226}
2227
2228function escHtml(str) {
2229  return String(str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
2230}
2231
2232function showMainContent() {
2233  document.getElementById('mainContent').style.display = '';
2234  document.getElementById('siteFooter').style.display = '';
2235}
2236
2237// ----------------------------------------------------------
2238//  MARKDOWN
2239// ----------------------------------------------------------
2240async function loadMarkdown(path) {
2241  try {
2242    const resp = await fetch(path);
2243    if (!resp.ok) return null;
2244    return await resp.text();
2245  } catch(e) { return null; }
2246}
2247
2248function renderMarkdown(md) {
2249  md = md.replace(/^\s*---[\s\S]*?---\n*/, '');
2250  // Convert Obsidian image embeds ![[filename]] to standard markdown images
2251  md = md.replace(/!\[\[([^\]]+)\]\]/g, (match, filename) => {
2252    // Strip any path, use just the filename, look in images/
2253    const name = filename.split('/').pop().split('\\').pop();
2254    return `![${name}](images/${encodeURIComponent(name)})`;
2255  });
2256  return marked.parse(md);
2257}
2258
2259// ----------------------------------------------------------
2260//  INTERCEPT EMBED LINKS
2261//  After rendering markdown into the DOM, find any links that
2262//  should open inside the app and wire them up.
2263//  Call this after setting content.innerHTML.
2264// ----------------------------------------------------------
2265function interceptEmbedLinks(container) {
2266  if (!container) return;
2267  container.querySelectorAll('a').forEach(a => {
2268    const href = a.href || '';
2269    // Forms, attendance links, discord events — open as inline panel
2270    if (href.includes('forms/d/e') || href.includes('docs.google.com/forms')) {
2271      a.addEventListener('click', e => {
2272        e.preventDefault();
2273        const title = a.textContent.trim() || 'Form';
2274        // Append ?embedded=true for Google Forms so they render cleanly
2275        const url = href.includes('?') ? href + '&embedded=true' : href + '?embedded=true';
2276        openInlinePanel(url, title);
2277      });
2278    }
2279  });
2280}
2281
2282// ----------------------------------------------------------
2283//  SEARCH
2284// ----------------------------------------------------------
2285function handleSearch(query) {
2286  const results = document.getElementById('searchResults');
2287  if (!query.trim()) { results.classList.remove('visible'); return; }
2288
2289  const q = query.toLowerCase();
2290  const guildMatches = C.guilds.filter(g =>
2291    g.name.toLowerCase().includes(q) ||
2292    (C.guildTypes.find(t => t.id === g.type)?.name.toLowerCase().includes(q))
2293  );
2294  const navMatches = C.nav.filter(s =>
2295    s.type !== 'guilds' && s.label.toLowerCase().includes(q)
2296  );
2297  const toolMatches = flatCampaignTools().filter(t =>
2298    t.label.toLowerCase().includes(q)
2299  );
2300
2301  const allMatches = [
2302    ...navMatches.map(s => ({ title: s.label, sub: 'Wiki Section', iconName: s.icon, action: () => navigateTo(s.id) })),
2303    ...toolMatches.map(t => ({ title: t.label, sub: 'Campaign Tool', iconName: t.icon, action: t._open })),
2304    ...guildMatches.map(g => {
2305      const type = C.guildTypes.find(t => t.id === g.type);
2306      return { title: g.name + ' ' + '★'.repeat(g.stars), sub: type?.name || '', iconName: type?.icon || '', action: () => navigateGuild(g.type, g.file) };
2307    })
2308  ];
2309
2310  if (allMatches.length === 0) {
2311    results.innerHTML = '<div class="search-result-item"><div class="search-result-title" style="opacity:0.5">No results found</div></div>';
2312  } else {
2313    results.innerHTML = allMatches.map((m, i) => `
2314      <div class="search-result-item" onmousedown="searchActions[${i}]()">
2315        <div class="search-result-title" style="display:flex;align-items:center;gap:6px">${m.iconName ? icon(m.iconName, 14) : ''} ${m.title}</div>
2316        <div class="search-result-type">${m.sub}</div>
2317      </div>`).join('');
2318    window.searchActions = allMatches.map(m => m.action);
2319  }
2320  results.classList.add('visible');
2321}
2322
2323function closeSearch() {
2324  document.getElementById('searchResults').classList.remove('visible');
2325}
2326
2327// Mobile sidebar search — mirrors handleSearch but targets sidebar elements
2328function handleMobileSearch(query) {
2329  const results = document.getElementById('sidebarSearchResults');
2330  if (!results) return;
2331  if (!query.trim()) { results.classList.remove('visible'); return; }
2332
2333  const q = query.toLowerCase();
2334  const guildMatches = C.guilds.filter(g =>
2335    g.name.toLowerCase().includes(q) ||
2336    (C.guildTypes.find(t => t.id === g.type)?.name.toLowerCase().includes(q))
2337  );
2338  const navMatches = C.nav.filter(s =>
2339    s.type !== 'guilds' && s.label.toLowerCase().includes(q)
2340  );
2341  const toolMatches = flatCampaignTools(toggleSidebar).filter(t =>
2342    t.label.toLowerCase().includes(q)
2343  );
2344
2345  const allMatches = [
2346    ...navMatches.map(s => ({ title: s.label, sub: 'Wiki Section', iconName: s.icon, action: () => { navigateTo(s.id); toggleSidebar(); } })),
2347    ...toolMatches.map(t => ({ title: t.label, sub: 'Campaign Tool', iconName: t.icon, action: t._open })),
2348    ...guildMatches.map(g => {
2349      const type = C.guildTypes.find(t => t.id === g.type);
2350      return { title: g.name + ' ' + '★'.repeat(g.stars), sub: type?.name || '', iconName: type?.icon || '', action: () => { navigateGuild(g.type, g.file); toggleSidebar(); } };
2351    })
2352  ];
2353
2354  if (allMatches.length === 0) {
2355    results.innerHTML = '<div class="search-result-item"><div class="search-result-title" style="opacity:0.5">No results found</div></div>';
2356  } else {
2357    results.innerHTML = allMatches.map((m, i) => `
2358      <div class="search-result-item" onmousedown="mobileSearchActions[${i}]()">
2359        <div class="search-result-title" style="display:flex;align-items:center;gap:6px">${m.iconName ? icon(m.iconName, 14) : ''} ${m.title}</div>
2360        <div class="search-result-type">${m.sub}</div>
2361      </div>`).join('');
2362    window.mobileSearchActions = allMatches.map(m => m.action);
2363  }
2364  results.classList.add('visible');
2365}
2366
2367function closeMobileSearch() {
2368  const results = document.getElementById('sidebarSearchResults');
2369  if (results) results.classList.remove('visible');
2370}
2371
2372// ----------------------------------------------------------
2373//  INIT
2374// ----------------------------------------------------------
2375applyConfig();
2376buildSidebar();
2377
2378// Hash-based routing: support deep links and browser back/forward
2379window.addEventListener('popstate', () => {
2380  const id = window.location.hash.slice(1) || 'overview';
2381  if (id !== window._currentSection) navigateTo(id, true);
2382});
2383const initialSection = window.location.hash.slice(1) || 'overview';
2384navigateTo(initialSection);
2385</script>
2385
2386</body>
2387</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.