tractatus/public/js/components/navbar.js
TheFlow e9511afd85 fix: Hide feedback FAB on mobile, add to drawer, persist install dismissal
FAB overlaps PWA install prompt and update notifications on small screens.
Mobile users now access feedback via the navbar drawer instead. Install
prompt dismissal persists in localStorage and is skipped in standalone mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 05:55:30 +13:00

526 lines
26 KiB
JavaScript

/**
* Tractatus Framework - Responsive Navbar Component
* Desktop: visible nav links with dropdowns
* Mobile: slide-out drawer
*/
class TractatusNavbar {
constructor() {
this.mobileMenuOpen = false;
this.openDropdown = null;
this.init();
}
init() {
this.render();
this.attachEventListeners();
this.setActivePageIndicator();
this.loadResearchPapersModal();
// Dispatch event to signal navbar is ready
window.dispatchEvent(new CustomEvent('navbarReady'));
}
render() {
const navHTML = `
<nav class="bg-white border-b border-gray-200 sticky top-0 z-50 shadow-sm" id="tractatus-navbar">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<!-- Left: Logo + Brand -->
<div class="flex items-center">
<a href="/"
class="flex items-center space-x-2 px-2 py-2 -ml-2 rounded-lg hover:bg-blue-50 transition-all duration-200 group"
title="Return to homepage">
<img src="/images/tractatus-icon-new.svg" alt="Tractatus Icon" class="w-8 h-8">
<span class="text-lg font-bold text-gray-900 group-hover:text-blue-700 transition-colors hidden sm:inline">Tractatus</span>
</a>
</div>
<!-- Centre: Desktop Navigation (hidden on mobile) -->
<div class="hidden lg:flex items-center space-x-1">
<!-- Research Dropdown -->
<div class="relative" data-dropdown="research">
<button class="nav-dropdown-btn px-3 py-2 text-sm font-medium text-gray-700 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition flex items-center gap-1">
Research
<svg class="w-4 h-4 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="nav-dropdown-panel hidden absolute left-0 top-full mt-1 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
<button data-research-papers-trigger class="w-full text-left block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">Publications</span>
<span class="block text-xs text-gray-500 mt-0.5">Browse research papers</span>
</button>
<div class="border-t border-gray-100 my-1"></div>
<a href="/researcher.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">For Researchers</span>
<span class="block text-xs text-gray-500 mt-0.5">Open questions and collaboration</span>
</a>
<a href="/timeline.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">Research Timeline</span>
<span class="block text-xs text-gray-500 mt-0.5">Evolution of the research</span>
</a>
</div>
</div>
<!-- Framework Dropdown (Architecture + Implementation) -->
<div class="relative" data-dropdown="framework">
<button class="nav-dropdown-btn px-3 py-2 text-sm font-medium text-gray-700 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition flex items-center gap-1">
Framework
<svg class="w-4 h-4 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="nav-dropdown-panel hidden absolute left-0 top-full mt-1 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
<a href="/architecture.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">System Architecture</span>
<span class="block text-xs text-gray-500 mt-0.5">Technical architecture overview</span>
</a>
<div class="border-t border-gray-100 my-1"></div>
<a href="/implementer.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">For Implementers</span>
<span class="block text-xs text-gray-500 mt-0.5">Integration guide and code examples</span>
</a>
<a href="/village-case-study.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">Village Case Study</span>
<span class="block text-xs text-gray-500 mt-0.5">Production deployment evidence</span>
</a>
<a href="/home-ai.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">Home AI</span>
<span class="block text-xs text-gray-500 mt-0.5">Sovereign locally-trained language model</span>
</a>
<div class="border-t border-gray-100 my-1"></div>
<a href="/integrations/agent-lightning.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">Agent Lightning</span>
<span class="block text-xs text-gray-500 mt-0.5">Performance optimisation integration</span>
</a>
<a href="/leader.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">For Leaders</span>
<span class="block text-xs text-gray-500 mt-0.5">Strategic overview and business case</span>
</a>
</div>
</div>
<!-- Blog (direct link) -->
<a href="/blog.html" class="px-3 py-2 text-sm font-medium text-gray-700 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition">
Blog
</a>
<!-- About Dropdown -->
<div class="relative" data-dropdown="about">
<button class="nav-dropdown-btn px-3 py-2 text-sm font-medium text-gray-700 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition flex items-center gap-1">
About
<svg class="w-4 h-4 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="nav-dropdown-panel hidden absolute left-0 top-full mt-1 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
<a href="/about.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">About Tractatus</span>
</a>
<a href="/about/values.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 transition">
<span class="font-medium">Values</span>
</a>
</div>
</div>
</div>
<!-- Right: Secondary links + Language + Menu -->
<div class="flex items-center gap-2">
<!-- Desktop secondary links -->
<div class="hidden lg:flex items-center gap-1">
<a href="/docs.html" class="px-3 py-2 text-sm text-gray-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition">Docs</a>
<a href="https://github.com/AgenticGovernance/tractatus-framework" target="_blank" rel="noopener noreferrer" class="px-3 py-2 text-sm text-gray-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition">GitHub</a>
<a href="/koha.html" class="px-3 py-1.5 text-sm font-medium text-teal-700 bg-teal-50 hover:bg-teal-100 rounded-lg transition border border-teal-200">Koha</a>
</div>
<!-- Language Selector Container -->
<div id="language-selector-container"></div>
<!-- Mobile menu button (hidden on desktop) -->
<button id="mobile-menu-btn" class="lg:hidden text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded p-2" aria-label="Toggle menu">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Navigation Drawer -->
<div id="mobile-menu" class="hidden fixed inset-0 z-[9999]">
<div id="mobile-menu-backdrop" class="absolute inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity"></div>
<div id="mobile-menu-panel" class="absolute right-0 top-0 bottom-0 w-80 max-w-[85vw] bg-white shadow-2xl transform transition-transform duration-300 ease-out overflow-y-auto">
<div class="flex justify-between items-center px-5 h-16 border-b border-gray-200">
<div class="flex items-center space-x-2">
<img src="/images/tractatus-icon-new.svg" alt="Tractatus Icon" class="w-6 h-6">
<span class="font-bold text-gray-900">Navigation</span>
</div>
<button id="mobile-menu-close-btn" class="text-gray-600 hover:text-gray-900 p-2 rounded hover:bg-gray-100 transition" aria-label="Close menu">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav class="p-5 space-y-1">
<!-- Research (collapsible) -->
<div class="mobile-nav-section border-b border-gray-200 pb-1 mb-1" data-section="research">
<button class="mobile-nav-section-btn flex items-center justify-between w-full px-3 py-2.5 text-xs font-semibold text-gray-500 uppercase tracking-wider hover:bg-gray-50 rounded-lg transition">
Research
<svg class="w-4 h-4 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="mobile-nav-section-content overflow-hidden transition-all duration-200" style="max-height:0">
<button data-research-papers-trigger class="w-full text-left block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">Publications</button>
<a href="/researcher.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">For Researchers</a>
<a href="/timeline.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">Research Timeline</a>
</div>
</div>
<!-- Framework (collapsible) -->
<div class="mobile-nav-section border-b border-gray-200 pb-1 mb-1" data-section="framework">
<button class="mobile-nav-section-btn flex items-center justify-between w-full px-3 py-2.5 text-xs font-semibold text-gray-500 uppercase tracking-wider hover:bg-gray-50 rounded-lg transition">
Framework
<svg class="w-4 h-4 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="mobile-nav-section-content overflow-hidden transition-all duration-200" style="max-height:0">
<a href="/architecture.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">System Architecture</a>
<a href="/implementer.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">For Implementers</a>
<a href="/village-case-study.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">Village Case Study</a>
<a href="/home-ai.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">Home AI</a>
<a href="/integrations/agent-lightning.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">Agent Lightning</a>
<a href="/leader.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">For Leaders</a>
</div>
</div>
<!-- Blog (direct link) -->
<div class="border-b border-gray-200 pb-1 mb-1">
<a href="/blog.html" class="block px-3 py-2.5 text-sm font-medium text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">Blog</a>
</div>
<!-- About (collapsible) -->
<div class="mobile-nav-section border-b border-gray-200 pb-1 mb-1" data-section="about">
<button class="mobile-nav-section-btn flex items-center justify-between w-full px-3 py-2.5 text-xs font-semibold text-gray-500 uppercase tracking-wider hover:bg-gray-50 rounded-lg transition">
About
<svg class="w-4 h-4 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="mobile-nav-section-content overflow-hidden transition-all duration-200" style="max-height:0">
<a href="/about.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">About Tractatus</a>
<a href="/about/values.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">Values</a>
</div>
</div>
<!-- Resources (collapsible) -->
<div class="mobile-nav-section border-b border-gray-200 pb-1 mb-1" data-section="resources">
<button class="mobile-nav-section-btn flex items-center justify-between w-full px-3 py-2.5 text-xs font-semibold text-gray-500 uppercase tracking-wider hover:bg-gray-50 rounded-lg transition">
Resources
<svg class="w-4 h-4 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<div class="mobile-nav-section-content overflow-hidden transition-all duration-200" style="max-height:0">
<a href="/docs.html" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">Documentation</a>
<a href="https://github.com/AgenticGovernance/tractatus-framework" target="_blank" rel="noopener noreferrer" class="block px-3 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">GitHub</a>
</div>
</div>
<!-- Koha (standalone prominent link) -->
<div class="pt-1">
<a href="/koha.html" class="block px-3 py-2.5 text-sm font-medium text-teal-700 bg-teal-50 hover:bg-teal-100 rounded-lg transition border border-teal-200">Koha</a>
</div>
<!-- Feedback (mobile-only, replaces hidden FAB) -->
<div class="pt-1">
<button id="mobile-feedback-btn" class="w-full text-left block px-3 py-2.5 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg transition border border-blue-200">Feedback</button>
</div>
</nav>
</div>
</div>
</nav>
`;
const existingNavbar = document.querySelector('nav.bg-white.border-b.border-gray-200.sticky');
if (existingNavbar) {
existingNavbar.outerHTML = navHTML;
} else {
const placeholder = document.getElementById('navbar-placeholder');
if (placeholder) {
placeholder.outerHTML = navHTML;
} else {
document.body.insertAdjacentHTML('afterbegin', navHTML);
}
}
}
attachEventListeners() {
// Desktop Dropdowns
const dropdowns = document.querySelectorAll('[data-dropdown]');
dropdowns.forEach(dropdown => {
const btn = dropdown.querySelector('.nav-dropdown-btn');
const panel = dropdown.querySelector('.nav-dropdown-panel');
if (!btn || !panel) return;
let closeTimeout = null;
const openDropdown = () => {
if (closeTimeout) {
clearTimeout(closeTimeout);
closeTimeout = null;
}
// Close other dropdowns
dropdowns.forEach(d => {
if (d !== dropdown) {
const p = d.querySelector('.nav-dropdown-panel');
if (p) p.classList.add('hidden');
const b = d.querySelector('.nav-dropdown-btn svg');
if (b) b.style.transform = '';
}
});
panel.classList.remove('hidden');
const arrow = btn.querySelector('svg');
if (arrow) arrow.style.transform = 'rotate(180deg)';
};
const closeDropdown = () => {
closeTimeout = setTimeout(() => {
panel.classList.add('hidden');
const arrow = btn.querySelector('svg');
if (arrow) arrow.style.transform = '';
}, 150);
};
btn.addEventListener('mouseenter', openDropdown);
btn.addEventListener('mouseleave', closeDropdown);
panel.addEventListener('mouseenter', () => {
if (closeTimeout) {
clearTimeout(closeTimeout);
closeTimeout = null;
}
});
panel.addEventListener('mouseleave', closeDropdown);
// Keyboard accessibility
btn.addEventListener('click', (e) => {
e.preventDefault();
const isHidden = panel.classList.contains('hidden');
// Close all
dropdowns.forEach(d => {
const p = d.querySelector('.nav-dropdown-panel');
if (p) p.classList.add('hidden');
});
if (isHidden) {
panel.classList.remove('hidden');
}
});
});
// Close dropdowns on click outside
document.addEventListener('click', (e) => {
if (!e.target.closest('[data-dropdown]')) {
dropdowns.forEach(d => {
const p = d.querySelector('.nav-dropdown-panel');
if (p) p.classList.add('hidden');
const b = d.querySelector('.nav-dropdown-btn svg');
if (b) b.style.transform = '';
});
}
});
// Close dropdowns on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
dropdowns.forEach(d => {
const p = d.querySelector('.nav-dropdown-panel');
if (p) p.classList.add('hidden');
const b = d.querySelector('.nav-dropdown-btn svg');
if (b) b.style.transform = '';
});
}
});
// Mobile Menu
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const mobileMenuCloseBtn = document.getElementById('mobile-menu-close-btn');
const mobileMenu = document.getElementById('mobile-menu');
const mobileMenuPanel = document.getElementById('mobile-menu-panel');
const mobileMenuBackdrop = document.getElementById('mobile-menu-backdrop');
const toggleMobileMenu = () => {
this.mobileMenuOpen = !this.mobileMenuOpen;
if (this.mobileMenuOpen) {
mobileMenu.classList.remove('hidden');
setTimeout(() => {
mobileMenuPanel.classList.remove('translate-x-full');
mobileMenuPanel.classList.add('translate-x-0');
}, 10);
document.body.style.overflow = 'hidden';
} else {
mobileMenuPanel.classList.remove('translate-x-0');
mobileMenuPanel.classList.add('translate-x-full');
setTimeout(() => {
mobileMenu.classList.add('hidden');
}, 300);
document.body.style.overflow = '';
}
};
if (mobileMenuPanel) {
mobileMenuPanel.classList.add('translate-x-full');
}
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', toggleMobileMenu);
}
if (mobileMenuCloseBtn) {
mobileMenuCloseBtn.addEventListener('click', toggleMobileMenu);
}
if (mobileMenuBackdrop) {
mobileMenuBackdrop.addEventListener('click', toggleMobileMenu);
}
// Close mobile menu on navigation (links and modal triggers)
const mobileClickables = document.querySelectorAll('#mobile-menu a, #mobile-menu [data-research-papers-trigger]');
mobileClickables.forEach(el => {
el.addEventListener('click', () => {
if (this.mobileMenuOpen) {
toggleMobileMenu();
}
});
});
// Mobile feedback button — close drawer then open feedback modal
const mobileFeedbackBtn = document.getElementById('mobile-feedback-btn');
if (mobileFeedbackBtn) {
mobileFeedbackBtn.addEventListener('click', () => {
if (this.mobileMenuOpen) {
toggleMobileMenu();
}
window.dispatchEvent(new CustomEvent('openFeedbackModal'));
});
}
// Mobile accordion sections
const mobileSections = document.querySelectorAll('.mobile-nav-section');
mobileSections.forEach(section => {
const btn = section.querySelector('.mobile-nav-section-btn');
const content = section.querySelector('.mobile-nav-section-content');
const chevron = btn ? btn.querySelector('svg') : null;
if (!btn || !content) return;
btn.addEventListener('click', () => {
const isCollapsed = content.style.maxHeight === '0px' || content.style.maxHeight === '';
if (isCollapsed) {
content.style.maxHeight = content.scrollHeight + 'px';
if (chevron) chevron.style.transform = 'rotate(180deg)';
} else {
content.style.maxHeight = '0px';
if (chevron) chevron.style.transform = '';
}
});
});
// Auto-expand mobile section containing current page
this.autoExpandActiveSection();
}
normalizePath(path) {
if (!path || path.startsWith('http')) return path;
if (path === '/' || path === '/index.html') return '/';
return path.replace('.html', '').replace(/\/$/, '');
}
setActivePageIndicator() {
const currentPath = window.location.pathname;
const normalizedCurrent = this.normalizePath(currentPath);
// Desktop nav links — highlight top-level links and dropdown trigger buttons
const desktopLinks = document.querySelectorAll('#tractatus-navbar a[href]');
desktopLinks.forEach(link => {
const linkPath = link.getAttribute('href');
const normalizedLink = this.normalizePath(linkPath);
if (normalizedLink === normalizedCurrent) {
// Highlight the link itself if it's a direct top-level link (not inside dropdown panel)
if (!link.closest('.nav-dropdown-panel')) {
link.classList.add('text-blue-700', 'bg-blue-50');
link.classList.remove('text-gray-700', 'text-gray-600');
}
// Also highlight the parent dropdown button if this link is inside a dropdown
const dropdownContainer = link.closest('[data-dropdown]');
if (dropdownContainer) {
const dropdownBtn = dropdownContainer.querySelector('.nav-dropdown-btn');
if (dropdownBtn) {
dropdownBtn.classList.add('text-blue-700');
dropdownBtn.classList.remove('text-gray-700');
}
}
}
});
// Mobile nav links
const mobileLinks = document.querySelectorAll('#mobile-menu a');
mobileLinks.forEach(link => {
const linkPath = link.getAttribute('href');
const normalizedLink = this.normalizePath(linkPath);
if (normalizedLink === normalizedCurrent) {
link.classList.add('border-l-4', 'bg-sky-50', 'text-blue-700', 'font-medium');
link.style.borderLeftColor = 'var(--tractatus-core-end, #3b82f6)';
link.classList.remove('text-gray-700');
}
});
}
autoExpandActiveSection() {
const currentPath = window.location.pathname;
const normalizedCurrent = this.normalizePath(currentPath);
const mobileSections = document.querySelectorAll('.mobile-nav-section');
mobileSections.forEach(section => {
const content = section.querySelector('.mobile-nav-section-content');
const chevron = section.querySelector('.mobile-nav-section-btn svg');
if (!content) return;
const links = content.querySelectorAll('a[href]');
let hasActive = false;
links.forEach(link => {
const linkPath = link.getAttribute('href');
if (this.normalizePath(linkPath) === normalizedCurrent) {
hasActive = true;
}
});
if (hasActive) {
content.style.maxHeight = content.scrollHeight + 'px';
if (chevron) chevron.style.transform = 'rotate(180deg)';
}
});
}
loadResearchPapersModal() {
// Dynamically load the research papers modal if not already present
if (window.researchPapersModal || document.getElementById('research-papers-modal')) return;
const script = document.createElement('script');
script.src = '/js/components/research-papers-modal.js?v=' + (document.querySelector('script[src*="navbar.js"]')?.src.match(/v=([^&]*)/)?.[1] || Date.now());
document.body.appendChild(script);
}
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => new TractatusNavbar());
} else {
new TractatusNavbar();
}