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 & 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); 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 `})`; 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.