<!DOCTYPE html>
<html lang="en" {{THEME_ATTR}}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{FILENAME}} — birta</title>
<link rel="icon" type="image/png" href="/favicon.png">
<style>{{GITHUB_CSS}}</style>
<style>{{THEME_OVERRIDES}}</style>
<style>{{PAGE_CSS}}</style>
<style>{{SYNTAX_CSS}}</style>
<style>{{ALERTS_CSS}}</style>
<style id="theme-vars">{{THEME_VARS_CSS}}</style>
<style>{{FONT_CSS}}</style>
{{CUSTOM_CSS}}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
</head>
<body class="{{BODY_CLASS}}">
<header class="header{{HEADER_CLASS}}">
<div class="header-right">
<div class="theme-controls">
<div class="theme-dropdown" id="theme-dropdown">
<button class="theme-dropdown-btn" id="theme-dropdown-btn" title="Switch theme">
<span id="theme-dropdown-label">{{ACTIVE_THEME}}</span>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
<path d="m4.427 7.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z"/>
</svg>
</button>
<div class="theme-dropdown-menu" id="theme-dropdown-menu">
{{THEME_OPTIONS}}
</div>
</div>
<button class="theme-toggle" id="theme-toggle" title="Toggle light/dark">
<svg id="icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm0-1.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5ZM8 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0Zm0 13a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 8 13ZM2.343 2.343a.75.75 0 0 1 1.061 0l1.06 1.061a.75.75 0 0 1-1.06 1.06l-1.06-1.06a.75.75 0 0 1 0-1.06Zm8.193 8.192a.75.75 0 0 1 1.06 0l1.061 1.061a.75.75 0 0 1-1.06 1.061l-1.06-1.061a.75.75 0 0 1 0-1.06ZM0 8a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8Zm13 0a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5h-1.5A.75.75 0 0 1 13 8ZM2.343 13.657a.75.75 0 0 1 0-1.06l1.06-1.061a.75.75 0 0 1 1.061 1.06l-1.06 1.061a.75.75 0 0 1-1.061 0Zm8.193-8.192a.75.75 0 0 1 0-1.061l1.061-1.06a.75.75 0 1 1 1.06 1.06l-1.06 1.06a.75.75 0 0 1-1.06 0Z"/>
</svg>
<svg id="icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Zm1.616 1.945a7 7 0 0 1-7.678 7.678 5.499 5.499 0 1 0 7.678-7.678Z"/>
</svg>
</button>
</div>
<button class="reading-toggle" id="reading-toggle" title="Reading mode (r)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.743 3.743 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z"/>
</svg>
</button>
</div>
</header>
<main class="container">
<div class="file-header">
<div class="file-header-left">
<svg class="file-header-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0 1 14.25 16H1.75A1.75 1.75 0 0 1 0 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25Zm7.47 3.97a.75.75 0 0 1 1.06 0l2 2a.75.75 0 0 1 0 1.06l-2 2a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L10.69 8 9.22 6.53a.75.75 0 0 1 0-1.06ZM6.78 6.53 5.31 8l1.47 1.47a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215l-2-2a.75.75 0 0 1 0-1.06l2-2a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z"/>
</svg>
<span class="file-header-name">{{FILENAME}}</span>
<span class="file-stats" id="file-stats" style="display:none">{{FILE_STATS}}</span>
</div>
<div class="view-toggle" id="view-toggle">
<button class="view-btn active" data-view="preview" title="Preview">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011ZM5 8.75a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 5 8.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3a.75.75 0 0 0 0-1.5Z"/>
</svg>
</button>
<button class="view-btn" data-view="raw" title="Raw">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="m11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z"/>
</svg>
</button>
</div>
</div>
<article class="markdown-body" id="content">{{CONTENT}}</article>
<div class="source-container" id="source-view" style="display:none">{{SOURCE_HTML}}</div>
</main>
<div class="reading-progress" id="reading-progress"></div>
<div class="reading-exit-zone" id="reading-exit-zone"></div>
<div class="reading-exit-bar" id="reading-exit-bar">
<button class="reading-exit-btn" id="reading-exit-btn" title="Exit reading mode (Esc)">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"/>
</svg>
</button>
</div>
<div class="status" id="status"></div>
<script>
// --- State ---
var STATIC_MODE = {{STATIC_MODE}};
var THEME_MODE = '{{THEME_MODE}}';
var activeVariant = '{{ACTIVE_VARIANT}}';
var VARIANT_EXPLICIT = {{VARIANT_EXPLICIT}};
var variants = {{VARIANTS_JSON}};
var KEYBINDINGS = {{KEYBINDINGS_JSON}};
var CURRENT_PATH = '{{CURRENT_PATH}}';
var INITIAL_VIEW = '{{INITIAL_VIEW}}';
var html = document.documentElement;
var mdBody = document.getElementById('content');
var toggleBtn = document.getElementById('theme-toggle');
var iconSun = document.getElementById('icon-sun');
var iconMoon = document.getElementById('icon-moon');
var themeDropdown = document.getElementById('theme-dropdown');
var themeDropdownBtn = document.getElementById('theme-dropdown-btn');
var themeDropdownMenu = document.getElementById('theme-dropdown-menu');
var themeDropdownLabel = document.getElementById('theme-dropdown-label');
var themeVarsEl = document.getElementById('theme-vars');
var sourceView = document.getElementById('source-view');
var currentWs = null;
// --- View Mode (Preview / Raw) ---
var storedView = null;
try { storedView = sessionStorage.getItem('birta-view-mode'); } catch(e) {}
var viewMode = (storedView === 'raw' || (!storedView && INITIAL_VIEW === 'raw')) ? 'raw' : 'preview';
var viewModeBeforeReading = viewMode;
function setViewMode(mode) {
viewMode = mode;
var isRaw = mode === 'raw';
mdBody.style.display = isRaw ? 'none' : '';
sourceView.style.display = isRaw ? '' : 'none';
var statsEl = document.getElementById('file-stats');
if (statsEl) statsEl.style.display = isRaw ? '' : 'none';
var btns = document.querySelectorAll('#view-toggle .view-btn');
for (var i = 0; i < btns.length; i++) {
btns[i].classList.toggle('active', btns[i].getAttribute('data-view') === mode);
}
try { sessionStorage.setItem('birta-view-mode', mode); } catch(e) {}
}
// Restore view mode on page load
if (viewMode === 'raw') setViewMode('raw');
// Toggle button clicks
(function() {
var btns = document.querySelectorAll('#view-toggle .view-btn');
for (var i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', function() {
setViewMode(this.getAttribute('data-view'));
});
}
})();
// --- Reading Mode ---
var readingToggle = document.getElementById('reading-toggle');
var readingProgress = document.getElementById('reading-progress');
var readingExitZone = document.getElementById('reading-exit-zone');
var readingExitBar = document.getElementById('reading-exit-bar');
var readingExitBtn = document.getElementById('reading-exit-btn');
var storedReading = null;
try { storedReading = sessionStorage.getItem('birta-reading-mode'); } catch(e) {}
var isReadingMode = storedReading !== null
? storedReading === '1'
: document.body.classList.contains('reading-mode');
if (isReadingMode) document.body.classList.add('reading-mode');
function toggleReadingMode(force) {
isReadingMode = typeof force === 'boolean' ? force : !isReadingMode;
document.body.classList.toggle('reading-mode', isReadingMode);
readingProgress.style.display = isReadingMode ? 'block' : 'none';
readingExitZone.style.display = isReadingMode ? 'block' : 'none';
try { sessionStorage.setItem('birta-reading-mode', isReadingMode ? '1' : '0'); } catch(e) {}
if (isReadingMode) {
viewModeBeforeReading = viewMode;
if (viewMode === 'raw') setViewMode('preview');
updateProgress();
} else if (viewModeBeforeReading === 'raw') {
setViewMode('raw');
}
}
function updateProgress() {
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
var scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
var pct = scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
readingProgress.style.width = pct + '%';
}
window.addEventListener('scroll', function() {
if (isReadingMode) updateProgress();
}, { passive: true });
readingToggle.addEventListener('click', function() { toggleReadingMode(); });
readingExitBtn.addEventListener('click', function() { toggleReadingMode(false); });
// Initialize reading mode UI if started via --reading-mode
if (isReadingMode) {
readingProgress.style.display = 'block';
readingExitZone.style.display = 'block';
updateProgress();
}
// --- Keybinding matcher ---
// Uses e.key for plain keys (e.g. "r", "Escape") and e.code for modified
// keys (e.g. "Alt+r") because macOS Option/Alt produces special characters
// in e.key (e.g. Option+r = "®"), making e.key unreliable with modifiers.
function matchesBinding(e, binding) {
if (!binding) return false;
var parts = binding.split('+');
var key = parts[parts.length - 1];
var needAlt = parts.indexOf('Alt') !== -1;
var needCtrl = parts.indexOf('Ctrl') !== -1;
var needShift = parts.indexOf('Shift') !== -1;
var needMeta = parts.indexOf('Meta') !== -1;
var hasModifiers = needAlt || needCtrl || needMeta;
var keyMatches;
if (!hasModifiers) {
keyMatches = e.key === key;
} else if (key.length === 1 && key >= 'a' && key <= 'z') {
keyMatches = e.code === 'Key' + key.toUpperCase();
} else if (key.length === 1 && key >= '0' && key <= '9') {
keyMatches = e.code === 'Digit' + key;
} else {
// Special keys (Escape, etc.) or unknown — fall back to e.key,
// then e.code if that fails
keyMatches = e.key === key || e.code === key;
}
return keyMatches
&& e.altKey === needAlt
&& e.ctrlKey === needCtrl
&& e.shiftKey === needShift
&& e.metaKey === needMeta;
}
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return;
if (matchesBinding(e, KEYBINDINGS.toggle_reading)) {
e.preventDefault();
toggleReadingMode();
} else if (matchesBinding(e, KEYBINDINGS.exit_reading) && isReadingMode) {
e.preventDefault();
toggleReadingMode(false);
} else if (matchesBinding(e, KEYBINDINGS.toggle_dark)) {
e.preventDefault();
if (!toggleBtn.classList.contains('disabled')) toggleBtn.click();
} else if (matchesBinding(e, KEYBINDINGS.focus_theme)) {
e.preventDefault();
themeDropdown.classList.toggle('open');
} else if (matchesBinding(e, KEYBINDINGS.toggle_raw)) {
e.preventDefault();
setViewMode(viewMode === 'raw' ? 'preview' : 'raw');
}
});
// --- Theme UI ---
function applyVariant(variant) {
activeVariant = variant;
html.setAttribute('data-theme', variant);
mdBody.setAttribute('data-theme', variant);
updateToggleIcon();
try { sessionStorage.setItem('birta-variant', variant); } catch(e) {}
}
function updateToggleIcon() {
var isDark = activeVariant === 'dark';
iconSun.style.display = isDark ? 'block' : 'none';
iconMoon.style.display = isDark ? 'none' : 'block';
}
function updateToggleVisibility() {
var canToggle = variants.length === 2 && THEME_MODE === 'toggle';
toggleBtn.classList.toggle('disabled', !canToggle);
}
function updateThemeSelect(themeName) {
themeDropdownLabel.textContent = themeName;
var items = themeDropdownMenu.querySelectorAll('.theme-dropdown-item');
for (var i = 0; i < items.length; i++) {
items[i].classList.toggle('active', items[i].getAttribute('data-theme') === themeName);
}
}
// Initial setup
(function() {
// If only one theme, hide dropdown
var themeItems = themeDropdownMenu.querySelectorAll('.theme-dropdown-item');
if (STATIC_MODE || themeItems.length <= 1) {
themeDropdown.style.display = 'none';
}
// Restore variant from session (persists across navigation), fall back
// to OS preference for dual-variant themes, then to server-rendered value.
// An explicit --light/--dark (or config theme.variant) wins over the OS
// preference, so skip the matchMedia fallback when VARIANT_EXPLICIT is set.
var storedVariant = null;
try { storedVariant = sessionStorage.getItem('birta-variant'); } catch(e) {}
if (storedVariant === 'dark' || storedVariant === 'light') {
activeVariant = storedVariant;
} else if (!VARIANT_EXPLICIT && THEME_MODE === 'toggle' && window.matchMedia) {
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
activeVariant = prefersDark ? 'dark' : 'light';
}
applyVariant(activeVariant);
updateToggleVisibility();
// Toggle sends variant change to server
toggleBtn.addEventListener('click', function() {
if (toggleBtn.classList.contains('disabled')) return;
var next = activeVariant === 'dark' ? 'light' : 'dark';
if (STATIC_MODE) {
applyVariant(next);
return;
}
if (!currentWs || currentWs.readyState !== WebSocket.OPEN) return;
currentWs.send(JSON.stringify({ type: 'variant_change', variant: next }));
});
// Custom theme dropdown
themeDropdownBtn.addEventListener('click', function() {
themeDropdown.classList.toggle('open');
});
// Close on outside click
document.addEventListener('click', function(e) {
if (!themeDropdown.contains(e.target)) {
themeDropdown.classList.remove('open');
}
});
// Close on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && themeDropdown.classList.contains('open')) {
themeDropdown.classList.remove('open');
e.stopPropagation();
}
}, true);
// Theme item click
if (!STATIC_MODE) {
themeDropdownMenu.addEventListener('click', function(e) {
var item = e.target.closest('.theme-dropdown-item');
if (!item) return;
var themeName = item.getAttribute('data-theme');
if (!currentWs || currentWs.readyState !== WebSocket.OPEN) return;
currentWs.send(JSON.stringify({ type: 'theme_change', theme: themeName }));
themeDropdown.classList.remove('open');
});
}
})();
// --- Math rendering via KaTeX ---
function renderMath() {
if (typeof katex === 'undefined') return;
document.querySelectorAll('[data-math-style]').forEach(function(el) {
if (el.hasAttribute('data-math-rendered')) return;
var displayMode = el.getAttribute('data-math-style') === 'display';
var tex = el.textContent;
try {
katex.render(tex, el, { displayMode: displayMode, throwOnError: false });
el.setAttribute('data-math-rendered', '');
} catch (e) { /* leave raw text on error */ }
});
}
// --- Mermaid diagram rendering ---
var mermaidReady = false;
function initMermaid() {
if (typeof mermaid === 'undefined') return;
if (mermaidReady) return;
var theme = activeVariant === 'dark' ? 'dark' : 'default';
mermaid.initialize({ startOnLoad: false, theme: theme });
mermaidReady = true;
}
function renderMermaid() {
if (typeof mermaid === 'undefined') return;
initMermaid();
mermaid.run({ querySelector: 'pre.mermaid' });
}
// Render on initial load
function renderAll() { renderMath(); renderMermaid(); }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', renderAll);
} else {
setTimeout(renderAll, 0);
}
// --- Scroll synchronization ---
function scrollToLine(targetLine) {
var els = mdBody.querySelectorAll('[data-sourcepos]');
if (!els.length) return;
var best = null;
var lo = 0, hi = els.length - 1;
while (lo <= hi) {
var mid = (lo + hi) >> 1;
var line = parseInt(els[mid].getAttribute('data-sourcepos'), 10);
if (line <= targetLine) {
best = els[mid];
lo = mid + 1;
} else {
hi = mid - 1;
}
}
if (best) {
best.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// --- Checkbox write-back ---
function enableCheckboxes() {
mdBody.querySelectorAll('input[type="checkbox"][disabled]').forEach(function(cb) {
cb.disabled = false;
cb.addEventListener('change', function(e) {
var li = e.target.closest('li[data-sourcepos]');
if (!li || !currentWs || currentWs.readyState !== WebSocket.OPEN) return;
var line = parseInt(li.getAttribute('data-sourcepos'), 10);
currentWs.send(JSON.stringify({
type: 'checkbox', line: line, checked: e.target.checked, path: CURRENT_PATH
}));
});
});
}
// --- WebSocket ---
if (!STATIC_MODE) (function() {
var status = document.getElementById('status');
var reconnectDelay = 1000;
var maxDelay = 10000;
function showStatus(msg) {
status.textContent = msg;
status.classList.add('visible');
}
function hideStatus() {
status.classList.remove('visible');
}
function connect() {
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var ws = new WebSocket(proto + '//' + location.host + '/ws');
currentWs = ws;
ws.onopen = function() {
reconnectDelay = 1000;
hideStatus();
// Sync client variant preference to server (OS/session may differ from server default)
if (activeVariant !== '{{ACTIVE_VARIANT}}') {
ws.send(JSON.stringify({ type: 'variant_change', variant: activeVariant }));
}
};
ws.onmessage = function(event) {
try {
var msg = JSON.parse(event.data);
} catch (e) {
return;
}
switch (msg.type) {
case 'content':
if (msg.path && CURRENT_PATH && msg.path !== CURRENT_PATH) break;
mdBody.innerHTML = msg.html;
if (msg.source) sourceView.innerHTML = msg.source;
if (msg.file_stats) {
var statsEl = document.getElementById('file-stats');
if (statsEl) statsEl.textContent = msg.file_stats;
}
renderMath();
renderMermaid();
enableCheckboxes();
break;
case 'theme_update':
// Update CSS variables
themeVarsEl.textContent = msg.css_vars;
// Update theme attribute
if (msg.theme_attr) {
html.setAttribute('data-birta-theme', msg.theme_attr);
} else {
html.removeAttribute('data-birta-theme');
}
// Update variant state
variants = msg.variants;
activeVariant = msg.active_variant;
THEME_MODE = msg.has_toggle ? 'toggle' : 'fixed-' + msg.active_variant;
applyVariant(activeVariant);
updateToggleVisibility();
updateThemeSelect(msg.theme_name);
// Re-render content with new syntax theme
if (CURRENT_PATH && msg.path !== CURRENT_PATH) {
// Viewing a different file — fetch fresh render for both views
fetch('/render/' + CURRENT_PATH).then(function(r) {
if (!r.ok) return;
return r.text();
}).then(function(freshHtml) {
if (!freshHtml) return;
mdBody.innerHTML = freshHtml;
renderMath();
mermaidReady = false;
renderMermaid();
enableCheckboxes();
});
fetch('/render/' + CURRENT_PATH + '?mode=source').then(function(r) {
if (!r.ok) return;
return r.text();
}).then(function(freshSource) {
if (freshSource) sourceView.innerHTML = freshSource;
});
} else {
mdBody.innerHTML = msg.html;
if (msg.source) sourceView.innerHTML = msg.source;
renderMath();
mermaidReady = false;
renderMermaid();
enableCheckboxes();
}
break;
case 'scroll':
scrollToLine(msg.line);
break;
}
};
ws.onclose = function() {
currentWs = null;
showStatus('Disconnected \u2014 reconnecting\u2026');
setTimeout(function() {
reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
connect();
}, reconnectDelay);
};
}
connect();
})();
// Enable checkboxes on initial page load (not in static mode)
if (!STATIC_MODE) enableCheckboxes();
</script>
</body>
</html>