Files
kjvstudy.org/kjvstudy_org/templates/base.html
T
kennethreitz ec7fcf91ef Fix site-wide verse linking to use chapter anchors and skip already-linked content
- Updated base.html JavaScript to use chapter anchors instead of verse pages
- Added filter to skip text nodes inside anchor tags to prevent double-linking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 12:32:19 -05:00

2683 lines
98 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{% block title %}Authorized King James Version Bible{% endblock %}</title>
<meta name="description" content="{% block description %}Study the Authorized King James Version Bible{% endblock %}"/>
<!-- Canonical URL -->
<link rel="canonical" href="https://kjvstudy.org{% block canonical_path %}{{ request.url.path }}{% endblock %}"/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="{% block og_type %}website{% endblock %}"/>
<meta property="og:url" content="https://kjvstudy.org{{ request.url.path }}"/>
<meta property="og:title" content="{% block og_title %}{% block title_text %}Authorized King James Version Bible{% endblock %}{% endblock %}"/>
<meta property="og:description" content="{% block og_description %}{% block description_text %}Study the Authorized King James Version Bible{% endblock %}{% endblock %}"/>
<meta property="og:site_name" content="KJV Study"/>
<meta property="og:image" content="{% block og_image %}https://kjvstudy.org/static/og-image.png{% endblock %}"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:url" content="https://kjvstudy.org{{ request.url.path }}"/>
<meta name="twitter:title" content="{% block twitter_title %}{% block title_plain %}Authorized King James Version Bible{% endblock %}{% endblock %}"/>
<meta name="twitter:description" content="{% block twitter_description %}{% block description_plain %}Study the Authorized King James Version Bible{% endblock %}{% endblock %}"/>
<meta name="twitter:image" content="{% block twitter_image %}https://kjvstudy.org/static/og-image.png{% endblock %}"/>
<link rel="stylesheet" href="/static/tufte.css"/>
<link rel="manifest" href="/static/manifest.json"/>
<!-- Analytics opt-out detection (kr=1 parameter) -->
<script>
(function() {
// Check for ?kr=1 to disable analytics
const urlParams = new URLSearchParams(window.location.search);
const krParam = urlParams.get('kr');
if (krParam === '1') {
// Disable analytics
localStorage.setItem('analytics_disabled', 'true');
document.cookie = 'analytics_disabled=1; max-age=315360000; path=/; SameSite=Lax';
// Redirect to clean URL
window.location.href = window.location.pathname;
} else if (krParam === '0') {
// Re-enable analytics
localStorage.removeItem('analytics_disabled');
document.cookie = 'analytics_disabled=0; max-age=0; path=/; SameSite=Lax';
// Redirect to clean URL
window.location.href = window.location.pathname;
}
// Store analytics status for template use
window.analyticsDisabled = localStorage.getItem('analytics_disabled') === 'true'
|| document.cookie.includes('analytics_disabled=1');
})();
</script>
{% block head %}{% endblock %}
<!-- Structured Data (Schema.org) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "KJV Study",
"url": "https://kjvstudy.org",
"description": "Study the Authorized King James Version Bible with commentary, study guides, and biblical resources",
"potentialAction": {
"@type": "SearchAction",
"target": "https://kjvstudy.org/search?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
{% block structured_data %}{% endblock %}
<style>
/* Color scheme variables */
:root {
color-scheme: light;
--bg-color: #fffff8;
--text-color: #111;
--text-secondary: #666;
--text-tertiary: #888;
--text-quaternary: #999;
--border-color: #eee;
--border-color-dark: #ddd;
--border-color-darker: #ccc;
--link-color: #333;
--link-hover: #000;
--code-bg: #f5f5f5;
--text-strong: #111;
}
[data-theme="dark"] {
color-scheme: dark;
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
--text-secondary: #b0b0b0;
--text-tertiary: #909090;
--text-quaternary: #707070;
--border-color: #333;
--border-color-dark: #444;
--border-color-darker: #555;
--link-color: #d0d0d0;
--link-hover: #fff;
--code-bg: #2a2a2a;
--text-strong: #e0e0e0;
}
/* Red letter toggle - hide red letters when disabled */
[data-red-letters="disabled"] .words-of-christ {
color: inherit !important;
}
/* Override Tufte CSS sidenote constraints */
.sidenote,
.marginnote {
max-height: none !important;
}
label.sidenote-number {
max-height: none !important;
}
/* Mobile: hide sidenotes by default, show on click */
@media (max-width: 760px) {
.sidenote,
.marginnote {
display: none !important;
}
.margin-toggle:checked + .sidenote,
.margin-toggle:checked + .marginnote {
display: block !important;
float: left !important;
left: 1rem !important;
clear: both !important;
width: 95% !important;
margin: 1rem 2.5% !important;
vertical-align: baseline !important;
position: relative !important;
background-color: var(--code-bg);
padding: 1rem;
border-radius: 4px;
border-left: 3px solid var(--border-color-darker);
}
label.sidenote-number {
cursor: pointer;
}
}
/* Ensure sidenotes respect explicit theme (override OS dark prefs from tufte.css) */
.sidenote,
.marginnote {
background-color: transparent !important;
color: var(--text-color) !important;
}
@media (max-width: 760px) {
.margin-toggle:checked + .sidenote,
.margin-toggle:checked + .marginnote {
background-color: var(--code-bg) !important;
}
}
[data-theme="dark"] .sidenote,
[data-theme="dark"] .marginnote {
background-color: var(--bg-color) !important;
color: var(--text-color) !important;
}
/* Verse tooltip styles */
/* Links to verses get normal pointer cursor (tooltips shown on hover) */
a {
color: var(--link-color) !important;
}
a:hover {
color: var(--link-hover) !important;
}
a[href*="/book/"][href*="/chapter/"][href*="/verse/"]:not(.verse-number-link),
a[href*="/book/"][href*="/chapter/"][href*="#verse-"]:not(.verse-number-link) {
cursor: pointer;
}
/* Verse numbers get normal pointer cursor */
.verse-number-link {
cursor: pointer;
}
/* But not in search dropdowns */
.search-results-dropdown a,
.search-results-dropdown a[href*="/book/"],
.search-dropdown a,
.search-dropdown a[href*="/book/"],
.lookup-results-dropdown a,
.lookup-results-dropdown a[href*="/book/"],
.story-search-dropdown a,
.story-search-dropdown a[href*="/book/"] {
cursor: pointer !important;
}
.verse-tooltip {
position: absolute;
background: var(--bg-color);
border: 1px solid var(--border-color-darker);
border-radius: 6px;
padding: 0.75rem 1rem;
min-width: 300px;
max-width: 400px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 9999;
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-color);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.verse-tooltip.show {
opacity: 1;
}
.verse-tooltip-reference {
font-weight: 600;
color: var(--link-color);
margin-bottom: 0.5rem;
display: block;
}
.verse-tooltip-text {
font-weight: 600;
font-style: italic;
color: var(--text-secondary);
}
.verse-tooltip-loading {
color: var(--text-tertiary);
}
[data-theme="dark"] .verse-tooltip {
background: #2a2a2a;
border-color: #444;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .verse-tooltip {
background: #2a2a2a;
border-color: #444;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
}
/* Enhanced typography and spacing */
body {
counter-reset: sidenote-counter;
background-color: var(--bg-color) !important;
color: var(--text-color) !important;
transition: background-color 0.3s ease, color 0.3s ease;
}
article {
padding: 2rem 5%;
}
h1, h2, h3 {
font-weight: 400;
font-style: normal;
color: var(--text-color) !important;
}
h1 {
font-size: 2.8rem;
line-height: 1.2;
margin-top: 3rem;
margin-bottom: 1.5rem;
letter-spacing: -0.02em;
}
h2 {
font-size: 2rem;
line-height: 1.3;
margin-top: 3rem;
margin-bottom: 1rem;
font-style: italic;
}
p {
font-size: 1.4rem;
line-height: 2;
margin-top: 1.4rem;
}
/* Drop cap for opening paragraphs */
.newthought {
font-variant: small-caps;
letter-spacing: 0.05em;
}
/* Refined breadcrumb navigation */
.breadcrumb {
margin: 2rem 0 3rem 0;
font-size: 0.95rem;
color: var(--text-secondary);
font-style: italic;
border-bottom: 1px solid var(--border-color);
padding-bottom: 1rem;
}
.breadcrumb a {
color: var(--link-color);
text-decoration: none;
transition: color 0.2s;
}
.breadcrumb a:hover {
color: var(--link-hover);
}
.breadcrumb-separator {
margin: 0 0.75rem;
color: var(--text-quaternary);
}
/* Enhanced link styling */
a {
color: var(--link-color);
text-decoration: none;
border-bottom: 1px solid var(--border-color-dark);
transition: all 0.2s;
}
a:hover {
color: var(--link-hover);
border-bottom-color: var(--link-hover);
}
/* Section dividers */
section {
margin-top: 3rem;
padding-top: 2rem;
}
/* Ornamental breaks */
.ornament {
text-align: center;
margin: 3rem 0;
color: var(--text-quaternary);
font-size: 1.5rem;
letter-spacing: 1rem;
}
/* Floating Navigation Sidebar */
.nav-sidebar {
position: fixed;
top: 0;
left: 0;
width: 140px;
height: 100vh;
overflow-y: auto;
background: var(--bg-color);
padding: 1.25rem 0.85rem 1.5rem 0.85rem;
font-size: 0.8rem;
line-height: 1.4;
z-index: 100;
border-right: 1px solid var(--border-color);
direction: rtl;
}
.nav-sidebar > * {
direction: ltr;
}
.nav-sidebar::-webkit-scrollbar {
width: 3px;
}
.nav-sidebar::-webkit-scrollbar-track {
background: transparent;
}
.nav-sidebar::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.nav-sidebar::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* Section headers */
.nav-sidebar h3 {
font-size: 0.6rem;
font-weight: 500;
font-style: normal;
margin: 1.25rem 0 0.5rem 0;
padding: 0;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.1em;
border: none;
}
.nav-sidebar h3:first-of-type {
margin-top: 0.5rem;
}
/* Lists */
.nav-sidebar ul {
list-style: none;
padding: 0;
margin: 0 0 0.5rem 0;
}
.nav-sidebar li {
margin: 0;
}
/* Links */
.nav-sidebar a {
color: var(--text-secondary);
text-decoration: none;
border-bottom: none;
display: block;
padding: 0.25rem 0.4rem;
margin: 0 -0.4rem;
border-radius: 4px;
transition: all 0.15s ease;
font-size: 0.8rem;
}
.nav-sidebar a:hover {
color: var(--text-color);
background: rgba(74, 124, 89, 0.08);
}
.nav-sidebar a.current {
color: #4a7c59;
background: rgba(74, 124, 89, 0.1);
font-weight: 500;
}
/* Book type color indicators in sidebar */
.nav-sidebar .book-list a {
border-left: 2px solid transparent;
padding-left: 0.5rem;
margin-left: 0;
border-radius: 0 4px 4px 0;
}
.nav-sidebar .book-list a.law { border-left-color: #3b82f6; }
.nav-sidebar .book-list a.historical { border-left-color: #22c55e; }
.nav-sidebar .book-list a.wisdom { border-left-color: #a855f7; }
.nav-sidebar .book-list a.major-prophets { border-left-color: #f97316; }
.nav-sidebar .book-list a.minor-prophets { border-left-color: #ef4444; }
.nav-sidebar .book-list a.gospels { border-left-color: #eab308; }
.nav-sidebar .book-list a.acts { border-left-color: #14b8a6; }
.nav-sidebar .book-list a.pauline { border-left-color: #6366f1; }
.nav-sidebar .book-list a.general { border-left-color: #ec4899; }
.nav-sidebar .book-list a.apocalyptic { border-left-color: #dc2626; }
.nav-sidebar .book-list {
font-size: 0.75rem;
}
.nav-sidebar .book-list a {
padding: 0.2rem 0.4rem 0.2rem 0.5rem;
}
/* Collapsible sections */
.nav-sidebar details {
margin-bottom: 0.35rem;
}
.nav-sidebar summary {
cursor: pointer;
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-tertiary);
padding: 0.35rem 0;
font-weight: 500;
list-style: none;
user-select: none;
transition: color 0.15s ease;
display: flex;
align-items: center;
gap: 0.35rem;
}
.nav-sidebar summary:hover {
color: var(--text-secondary);
}
.nav-sidebar summary::-webkit-details-marker {
display: none;
}
.nav-sidebar summary::before {
content: '';
display: inline-block;
width: 0;
height: 0;
border-left: 4px solid currentColor;
border-top: 3px solid transparent;
border-bottom: 3px solid transparent;
transition: transform 0.2s ease;
opacity: 0.6;
}
.nav-sidebar details[open] summary::before {
transform: rotate(90deg);
}
/* Smooth expand/collapse animation */
.nav-sidebar details > ul {
animation: navSlideDown 0.15s ease-out;
padding-left: 0.15rem;
}
@keyframes navSlideDown {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Resource group sections */
.nav-sidebar .resource-group {
margin-bottom: 0.25rem;
}
.nav-sidebar .resource-group ul {
margin-bottom: 0.25rem;
}
/* Smooth scrolling */
.nav-sidebar {
scroll-behavior: smooth;
}
/* Divider between Bible books and resources */
.nav-sidebar details#new-testament {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
/* Nav footer */
.nav-footer {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
text-align: center;
}
.nav-footer a {
font-size: 0.75rem;
color: var(--text-tertiary);
text-decoration: none;
padding: 0.3rem 0.6rem;
border-radius: 4px;
transition: all 0.15s ease;
}
.nav-footer a:hover {
color: var(--text-secondary);
background: rgba(74, 124, 89, 0.08);
}
/* Sidebar search box */
.sidebar-search {
margin-bottom: 1rem;
position: relative;
}
.sidebar-search input {
width: 100%;
padding: 0.45rem 0.5rem;
border: 1px solid var(--border-color);
border-radius: 5px;
font-size: 0.75rem;
background: var(--bg-color);
color: var(--text-color);
transition: all 0.15s ease;
box-sizing: border-box;
}
.sidebar-search input::placeholder {
color: var(--text-tertiary);
font-size: 0.7rem;
}
.sidebar-search input:focus {
outline: none;
border-color: #4a7c59;
box-shadow: 0 0 0 2px rgba(74, 124, 89, 0.1);
}
/* Universal search dropdown */
.search-results-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-color);
border: 1px solid var(--border-color-dark);
border-top: none;
border-radius: 0 0 3px 3px;
max-height: 400px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: none;
}
.search-results-dropdown.show {
display: block;
}
.search-results-category {
padding: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
.search-results-category:last-child {
border-bottom: none;
}
.search-results-category-title {
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-tertiary);
margin-bottom: 0.3rem;
font-weight: 600;
}
.search-result-item {
display: block;
padding: 0.3rem 0.4rem;
font-size: 0.75rem;
color: var(--text-color);
text-decoration: none;
border-bottom: none;
border-radius: 2px;
transition: background-color 0.15s;
}
.search-result-item:hover,
.search-result-item.selected {
background: var(--border-color);
color: var(--link-hover);
}
.search-result-item .result-title {
display: block;
font-weight: 500;
}
.search-result-item .result-meta {
font-size: 0.65rem;
color: var(--text-tertiary);
display: block;
margin-top: 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-no-results {
padding: 0.75rem;
text-align: center;
color: var(--text-tertiary);
font-size: 0.75rem;
font-style: italic;
}
.search-loading {
padding: 0.75rem;
text-align: center;
color: var(--text-tertiary);
font-size: 0.75rem;
}
.search-view-all {
display: block;
padding: 0.5rem;
text-align: center;
font-size: 0.7rem;
background: var(--border-color);
color: var(--text-secondary);
text-decoration: none;
border-bottom: none;
}
.search-view-all:hover {
background: var(--border-color-dark);
color: var(--link-hover);
}
[data-theme="dark"] .search-results-dropdown {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
/* Resource grouping */
.resource-group {
margin-bottom: 1.5rem;
}
.resource-group summary {
cursor: pointer;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-tertiary);
font-weight: 600;
margin: 1rem 0 0.5rem 0;
padding-top: 0.5rem;
border-top: 1px solid var(--border-color);
user-select: none;
}
.resource-group:first-of-type summary {
border-top: none;
margin-top: 0;
padding-top: 0;
}
.resource-group summary:hover {
color: var(--text-color);
}
/* Sidebar flourish */
.sidebar-flourish {
text-align: center;
margin: 2rem 0 1rem 0;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
color: var(--text-tertiary);
font-size: 1rem;
letter-spacing: 0.5em;
opacity: 0.6;
}
/* Sidebar collapse functionality */
#sidebar-toggle {
display: none;
}
.sidebar-toggle-container {
position: fixed;
top: 1rem;
left: 0.75rem;
z-index: 101;
margin: 0;
padding: 0;
}
.sidebar-toggle-btn {
width: auto;
min-width: 20px;
height: 20px;
padding: 0 0.4rem;
cursor: pointer;
background: var(--bg-color);
border: 1px solid var(--border-color-darker);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
color: var(--text-secondary);
transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.sidebar-toggle-btn:hover {
border-color: var(--text-tertiary);
color: var(--link-hover);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
/* Dark mode toggle */
.dark-mode-toggle {
position: fixed;
top: 2rem;
right: 2rem;
z-index: 101;
margin: -25px 0 0 0;
padding: 0;
}
.dark-mode-btn {
width: 24px;
height: 24px;
cursor: pointer;
background: transparent;
border: 1px solid var(--border-color-darker);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
color: var(--text-secondary);
transition: all 0.3s;
}
.dark-mode-btn:hover {
border-color: var(--text-tertiary);
color: var(--link-hover);
}
.dark-mode-btn::before {
content: '☀';
}
[data-theme="dark"] .dark-mode-btn::before {
content: '☾';
}
.nav-sidebar {
transform: translateX(-100%);
opacity: 0;
pointer-events: none;
}
.sidebar-toggle-container {
left: 0.75rem;
}
.sidebar-toggle-btn::before {
content: 'Menu';
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
#sidebar-toggle:checked ~ .nav-sidebar {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
#sidebar-toggle:checked ~ .sidebar-toggle-container {
left: 0.75rem;
}
#sidebar-toggle:checked ~ .sidebar-toggle-container .sidebar-toggle-btn::before {
content: '×';
font-size: 1.2rem;
font-weight: 400;
}
/* Responsive design for tablets and mobile */
@media (max-width: 1200px) {
.nav-sidebar {
width: 140px;
left: -140px;
top: 0;
bottom: 0;
max-height: 100vh;
border-radius: 0;
border-left: none;
padding-top: 4rem;
}
#sidebar-toggle:checked ~ .nav-sidebar {
left: 0;
}
.sidebar-toggle-container {
display: block;
top: 1rem;
left: 1rem;
}
.sidebar-toggle-btn {
width: 36px;
height: 36px;
font-size: 1.2rem;
background: var(--bg-color);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
@media (max-width: 768px) {
/* iOS-specific optimizations */
body {
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
/* Keep dark mode toggle on mobile, just reposition */
.dark-mode-toggle {
top: 1rem;
right: 1rem;
margin: 0;
min-width: 44px;
min-height: 44px;
}
/* Adjust article width for mobile */
article {
width: 95%;
padding: 1rem;
}
/* Optimize typography for mobile with better contrast */
body {
font-size: 16px;
}
h1 {
font-size: 2.2rem;
line-height: 2.4rem;
font-weight: 800;
color: #000;
letter-spacing: -0.02em;
}
[data-theme="dark"] h1 {
color: #fff;
}
h2 {
font-size: 1.65rem;
line-height: 1.85rem;
font-weight: 700;
color: #000;
font-style: normal;
}
[data-theme="dark"] h2 {
color: #fff;
}
h3 {
font-size: 1.3rem;
font-weight: 700;
color: #000;
}
[data-theme="dark"] h3 {
color: #fff;
}
p {
font-size: 1rem;
line-height: 1.8;
color: var(--text-color);
}
/* Enhanced link visibility for mobile - strong contrast */
a {
border-bottom: 2px solid #999;
padding-bottom: 1px;
color: #000;
font-weight: 500;
}
[data-theme="dark"] a {
border-bottom-color: #666;
color: #e0e0e0;
}
a:hover, a:active {
border-bottom-color: #333;
}
[data-theme="dark"] a:hover,
[data-theme="dark"] a:active {
border-bottom-color: #999;
}
/* Touch target sizing only for interactive elements, not inline text links */
button, .btn, input[type="submit"], .quick-link-btn {
min-height: 44px;
min-width: 44px;
}
/* Make max-width sections full width on mobile */
section p[style*="max-width"],
section div[style*="max-width"],
.intro-text,
.plan-overview,
.topic-overview,
.verse-lookup,
.share-container,
.sample-days,
.subtopics-container,
.plan-stats {
max-width: 100% !important;
}
/* Optimize sidenotes for mobile with strong contrast */
.sidenote,
.marginnote {
display: block;
float: none;
width: 100%;
margin: 1rem 0 1rem 1.5rem;
padding: 0.75rem 0 0.75rem 1rem;
border-left: 4px solid var(--border-color-darker);
font-size: 0.95rem;
line-height: 1.6;
color: var(--text-color) !important;
background-color: var(--code-bg) !important;
font-weight: 400;
}
[data-theme="dark"] .sidenote,
[data-theme="dark"] .marginnote {
border-left-color: var(--border-color-darker);
color: var(--text-color) !important;
background-color: var(--bg-color) !important;
}
.sidenote-number,
.margin-toggle {
display: inline;
}
/* Stack plan stats vertically */
.plan-stats {
flex-direction: column;
gap: 1rem;
}
/* Optimize grids for mobile */
.book-grid,
.plan-grid,
.topic-grid {
grid-template-columns: 1fr !important;
gap: 1rem;
max-width: 100% !important;
}
/* Optimize verse lookup form */
.lookup-form {
flex-direction: column;
}
.lookup-input,
.lookup-btn {
width: 100%;
}
/* Optimize share buttons */
.share-buttons {
flex-direction: column;
}
.share-btn {
width: 100%;
justify-content: center;
}
/* Optimize breadcrumbs with strong contrast */
.breadcrumb {
font-size: 0.95rem;
flex-wrap: wrap;
border-bottom: 3px solid #999;
padding-bottom: 0.75rem;
}
[data-theme="dark"] .breadcrumb {
border-bottom-color: #666;
}
.breadcrumb a {
font-weight: 600;
border-bottom: 2px solid #999;
color: #000;
}
[data-theme="dark"] .breadcrumb a {
border-bottom-color: #666;
color: #e0e0e0;
}
/* Improve button visibility on mobile with strong contrast */
button, .btn, input[type="submit"] {
min-height: 44px;
min-width: 44px;
padding: 0.75rem 1.25rem;
font-size: 1rem;
font-weight: 700;
border: 3px solid #111;
background: #111;
color: #fff;
cursor: pointer;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] button,
[data-theme="dark"] .btn,
[data-theme="dark"] input[type="submit"] {
border-color: #ddd;
background: #ddd;
color: #111;
}
/* Improve section dividers with strong contrast */
/* Optimize verse text */
.verse-text {
font-size: 1.4rem;
line-height: 2rem;
}
/* Make tables responsive */
table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
/* Optimize epigraph */
.epigraph {
margin: 2rem 0;
}
.epigraph blockquote {
padding: 1rem;
font-size: 1.1rem;
}
}
/* Extra small screens (phones in portrait) */
@media (max-width: 480px) {
article {
width: 100%;
padding: 0.5rem;
transition: transform 0.3s ease;
}
#sidebar-toggle:checked ~ article {
transform: translateX(140px);
}
h1 {
font-size: 1.75rem;
}
h2 {
font-size: 1.3rem;
}
.verse-text {
font-size: 1.2rem;
line-height: 1.8rem;
}
.dark-mode-toggle,
.sidebar-toggle-container {
top: 0.5rem;
}
.dark-mode-toggle {
right: 0.5rem;
}
#sidebar-toggle:checked ~ .dark-mode-toggle {
transform: translateX(140px);
}
}
/* Print styles for beautiful printed pages */
@media print {
/* Use light colors for print */
:root {
--bg-color: white;
--text-color: black;
--text-secondary: #333;
--border-color: #ccc;
}
body {
background: white;
color: black;
font-size: 12pt;
line-height: 1.6;
}
/* Hide navigation elements */
.nav-sidebar,
.sidebar-toggle-container,
.dark-mode-toggle,
.breadcrumb {
display: none !important;
}
/* Expand article to full width */
article {
width: 100% !important;
max-width: 100% !important;
padding: 0;
margin: 0;
}
/* Optimize typography for print */
h1 {
font-size: 24pt;
margin-top: 0;
page-break-after: avoid;
}
h2 {
font-size: 18pt;
page-break-after: avoid;
}
h3 {
font-size: 14pt;
page-break-after: avoid;
}
p {
font-size: 12pt;
orphans: 3;
widows: 3;
}
/* Avoid breaking inside elements */
section,
.verse-item,
.angel-entry,
.prophet-entry,
.name-entry,
.parable-entry,
.covenant-entry,
.apostle-entry,
.woman-entry,
.festival-entry {
page-break-inside: avoid;
}
/* Make sidenotes visible in print */
.sidenote,
.marginnote {
display: block !important;
float: none !important;
width: 100% !important;
margin: 0.5rem 0 0.5rem 2rem !important;
padding: 0.5rem;
border-left: 2px solid #ccc;
font-size: 10pt;
color: #333;
}
/* Hide sidenote toggles */
.margin-toggle {
display: none !important;
}
/* Show URLs for links */
a[href^="http"]:after {
content: " (" attr(href) ")";
font-size: 10pt;
color: #666;
}
/* Don't show URLs for internal links */
a[href^="/"]:after {
content: "";
}
/* Optimize verse display */
.verse-item {
margin: 0.5rem 0;
padding-left: 1rem;
}
.verse-ref {
font-weight: bold;
}
.verse-text {
max-width: 100%;
}
/* Remove link underlines for cleaner print */
a {
color: black;
text-decoration: none;
border-bottom: none;
}
/* Page breaks */
h1, h2 {
page-break-before: auto;
}
/* Add chapter/verse numbers in margin for reference */
@page {
margin: 2cm;
}
}
</style>
</head>
<body>
<input type="checkbox" id="sidebar-toggle" aria-hidden="true" checked>
<p class="sidebar-toggle-container">
<label for="sidebar-toggle" class="sidebar-toggle-btn" title="Toggle sidebar" aria-label="Toggle navigation sidebar"></label>
</p>
<p class="dark-mode-toggle">
<button class="dark-mode-btn" title="Toggle dark mode" onclick="toggleDarkMode()" aria-label="Toggle dark mode"></button>
</p>
<article role="main">
{% if breadcrumbs %}
<nav class="breadcrumb" aria-label="Breadcrumb">
{% for crumb in breadcrumbs %}
{% if not loop.last %}
<a href="{{ crumb.url }}">{{ crumb.text }}</a>
<span class="breadcrumb-separator">&gt;</span>
{% else %}
<span>{{ crumb.text }}</span>
{% endif %}
{% endfor %}
</nav>
{% endif %}
{% block content %}{% endblock %}
</article>
<!-- Floating Navigation Sidebar -->
<nav class="nav-sidebar" role="navigation" aria-label="Primary navigation">
<!-- Universal Search Box -->
<div class="sidebar-search" role="search">
<input type="text" id="sidebar-search-input" placeholder="Search everything..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" aria-label="Search Bible, topics, and resources">
<div id="search-results-dropdown" class="search-results-dropdown" role="listbox" aria-live="polite"></div>
</div>
<!-- Quick Access -->
<h3>Quick Access</h3>
<ul>
<li><a href="/" {% if request.url.path == "/" %}class="current"{% endif %}>Home</a></li>
<li><a href="/books" {% if request.url.path == "/books" %}class="current"{% endif %}>Books</a></li>
<li><a href="/search">Search</a></li>
</ul>
<!-- Bible Books -->
{% if books %}
<h3>Bible Books</h3>
<details id="old-testament" aria-label="Old Testament books">
<summary aria-expanded="false">Old Testament</summary>
<ul class="book-list">
{% set book_types = {'Genesis': 'law', 'Exodus': 'law', 'Leviticus': 'law', 'Numbers': 'law', 'Deuteronomy': 'law', 'Joshua': 'historical', 'Judges': 'historical', 'Ruth': 'historical', '1 Samuel': 'historical', '2 Samuel': 'historical', '1 Kings': 'historical', '2 Kings': 'historical', '1 Chronicles': 'historical', '2 Chronicles': 'historical', 'Ezra': 'historical', 'Nehemiah': 'historical', 'Esther': 'historical', 'Job': 'wisdom', 'Psalms': 'wisdom', 'Proverbs': 'wisdom', 'Ecclesiastes': 'wisdom', 'Song of Solomon': 'wisdom', 'Isaiah': 'major-prophets', 'Jeremiah': 'major-prophets', 'Lamentations': 'major-prophets', 'Ezekiel': 'major-prophets', 'Daniel': 'major-prophets', 'Hosea': 'minor-prophets', 'Joel': 'minor-prophets', 'Amos': 'minor-prophets', 'Obadiah': 'minor-prophets', 'Jonah': 'minor-prophets', 'Micah': 'minor-prophets', 'Nahum': 'minor-prophets', 'Habakkuk': 'minor-prophets', 'Zephaniah': 'minor-prophets', 'Haggai': 'minor-prophets', 'Zechariah': 'minor-prophets', 'Malachi': 'minor-prophets'} %}
{% for book in ['Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy', 'Joshua', 'Judges', 'Ruth', '1 Samuel', '2 Samuel', '1 Kings', '2 Kings', '1 Chronicles', '2 Chronicles', 'Ezra', 'Nehemiah', 'Esther', 'Job', 'Psalms', 'Proverbs', 'Ecclesiastes', 'Song of Solomon', 'Isaiah', 'Jeremiah', 'Lamentations', 'Ezekiel', 'Daniel', 'Hosea', 'Joel', 'Amos', 'Obadiah', 'Jonah', 'Micah', 'Nahum', 'Habakkuk', 'Zephaniah', 'Haggai', 'Zechariah', 'Malachi'] %}
{% if book in books %}
<li><a href="/book/{{ book }}" class="{{ book_types[book] }}{% if current_book == book %} current{% endif %}">{{ book }}</a></li>
{% endif %}
{% endfor %}
</ul>
</details>
<details id="new-testament" aria-label="New Testament books">
<summary aria-expanded="false">New Testament</summary>
<ul class="book-list">
{% set nt_book_types = {'Matthew': 'gospels', 'Mark': 'gospels', 'Luke': 'gospels', 'John': 'gospels', 'Acts': 'acts', 'Romans': 'pauline', '1 Corinthians': 'pauline', '2 Corinthians': 'pauline', 'Galatians': 'pauline', 'Ephesians': 'pauline', 'Philippians': 'pauline', 'Colossians': 'pauline', '1 Thessalonians': 'pauline', '2 Thessalonians': 'pauline', '1 Timothy': 'pauline', '2 Timothy': 'pauline', 'Titus': 'pauline', 'Philemon': 'pauline', 'Hebrews': 'general', 'James': 'general', '1 Peter': 'general', '2 Peter': 'general', '1 John': 'general', '2 John': 'general', '3 John': 'general', 'Jude': 'general', 'Revelation': 'apocalyptic'} %}
{% for book in ['Matthew', 'Mark', 'Luke', 'John', 'Acts', 'Romans', '1 Corinthians', '2 Corinthians', 'Galatians', 'Ephesians', 'Philippians', 'Colossians', '1 Thessalonians', '2 Thessalonians', '1 Timothy', '2 Timothy', 'Titus', 'Philemon', 'Hebrews', 'James', '1 Peter', '2 Peter', '1 John', '2 John', '3 John', 'Jude', 'Revelation'] %}
{% if book in books %}
<li><a href="/book/{{ book }}" class="{{ nt_book_types[book] }}{% if current_book == book %} current{% endif %}">{{ book }}</a></li>
{% endif %}
{% endfor %}
</ul>
</details>
{% endif %}
<!-- Daily Reading -->
<details class="resource-group" open aria-label="Daily reading resources">
<summary aria-expanded="true">Daily Reading</summary>
<ul>
<li><a href="/verse-of-the-day" {% if request.url.path == "/verse-of-the-day" %}class="current"{% endif %}>Verse of the Day</a></li>
<li><a href="/random-verse" {% if request.url.path == "/random-verse" %}class="current"{% endif %}>Random Verse</a></li>
<li><a href="/reading-plans" {% if request.url.path.startswith("/reading-plans") %}class="current"{% endif %}>Reading Plans</a></li>
</ul>
</details>
<!-- Study Resources -->
<details class="resource-group" open aria-label="Study resources">
<summary aria-expanded="true">Study Resources</summary>
<ul>
<li><a href="/study-guides" {% if request.url.path.startswith("/study-guides") %}class="current"{% endif %}>Study Guides</a></li>
<li><a href="/stories" {% if request.url.path.startswith("/stories") %}class="current"{% endif %}>Bible Stories</a></li>
<li><a href="/topics" {% if request.url.path.startswith("/topics") %}class="current"{% endif %}>Topics</a></li>
<li><a href="/resources" {% if request.url.path == "/resources" %}class="current"{% endif %}>Resources</a></li>
</ul>
</details>
<!-- Reference Tools -->
<details class="resource-group" open aria-label="Reference tools">
<summary aria-expanded="true">Reference Tools</summary>
<ul>
<li><a href="/strongs" {% if request.url.path.startswith("/strongs") %}class="current"{% endif %}>Strong's Concordance</a></li>
<li><a href="/interlinear" {% if request.url.path.startswith("/interlinear") %}class="current"{% endif %}>Interlinear</a></li>
</ul>
</details>
<!-- About -->
<div class="nav-footer">
<a href="/about" {% if request.url.path == "/about" %}class="current"{% endif %}>About</a>
</div>
</nav>
<script type="text/javascript">
// Dark mode functionality
(function() {
// Check for saved theme preference or default to light mode
const currentTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', currentTheme);
})();
function toggleDarkMode() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
// Red letter toggle functionality
(function() {
// Check for saved red letter preference or default to enabled
const redLettersEnabled = localStorage.getItem('redLetters') !== 'disabled';
if (!redLettersEnabled) {
document.documentElement.setAttribute('data-red-letters', 'disabled');
}
})();
function toggleRedLetters() {
const currentState = document.documentElement.getAttribute('data-red-letters');
const newState = currentState === 'disabled' ? 'enabled' : 'disabled';
if (newState === 'disabled') {
document.documentElement.setAttribute('data-red-letters', 'disabled');
localStorage.setItem('redLetters', 'disabled');
} else {
document.documentElement.removeAttribute('data-red-letters');
localStorage.removeItem('redLetters');
}
}
// Sidebar collapse state persistence
(function() {
var toggle = document.getElementById('sidebar-toggle');
var savedState = localStorage.getItem('sidebarExpanded');
var isMobile = window.innerWidth <= 768;
// If user has explicitly set a preference, respect that
if (savedState === 'false') {
toggle.checked = false;
} else if (savedState === 'true') {
toggle.checked = true;
} else {
// No saved state - default to collapsed on mobile, expanded on desktop
toggle.checked = !isMobile;
}
toggle.addEventListener('change', function() {
localStorage.setItem('sidebarExpanded', toggle.checked ? 'true' : 'false');
});
})();
// Details elements (subsections) collapse state persistence
(function() {
// Get all details elements in sidebar
var detailsElements = document.querySelectorAll('.nav-sidebar details');
// Restore saved states
detailsElements.forEach(function(details) {
var id = details.id || details.querySelector('summary')?.textContent.trim();
if (id) {
var savedState = localStorage.getItem('sidebar-details-' + id);
if (savedState === 'open') {
details.open = true;
} else if (savedState === 'closed') {
details.open = false;
}
}
});
// Save state on toggle
detailsElements.forEach(function(details) {
details.addEventListener('toggle', function() {
var id = this.id || this.querySelector('summary')?.textContent.trim();
if (id) {
localStorage.setItem('sidebar-details-' + id, this.open ? 'open' : 'closed');
}
});
});
})();
// Universal search functionality with smart verse navigation
(function() {
var searchInput = document.getElementById('sidebar-search-input');
var dropdown = document.getElementById('search-results-dropdown');
if (!searchInput || !dropdown) return;
var searchTimeout = null;
var selectedIndex = -1;
var currentResults = [];
// Book name mapping (same as homepage)
var bookMap = {
'genesis': 'Genesis', 'exodus': 'Exodus', 'leviticus': 'Leviticus', 'numbers': 'Numbers',
'deuteronomy': 'Deuteronomy', 'joshua': 'Joshua', 'judges': 'Judges', 'ruth': 'Ruth',
'1 samuel': '1 Samuel', '2 samuel': '2 Samuel', '1 kings': '1 Kings', '2 kings': '2 Kings',
'1 chronicles': '1 Chronicles', '2 chronicles': '2 Chronicles', 'ezra': 'Ezra', 'nehemiah': 'Nehemiah',
'esther': 'Esther', 'job': 'Job', 'psalms': 'Psalms', 'psalm': 'Psalms', 'proverbs': 'Proverbs',
'ecclesiastes': 'Ecclesiastes', 'song of solomon': 'Song of Solomon', 'isaiah': 'Isaiah',
'jeremiah': 'Jeremiah', 'lamentations': 'Lamentations', 'ezekiel': 'Ezekiel', 'daniel': 'Daniel',
'hosea': 'Hosea', 'joel': 'Joel', 'amos': 'Amos', 'obadiah': 'Obadiah', 'jonah': 'Jonah',
'micah': 'Micah', 'nahum': 'Nahum', 'habakkuk': 'Habakkuk', 'zephaniah': 'Zephaniah',
'haggai': 'Haggai', 'zechariah': 'Zechariah', 'malachi': 'Malachi', 'matthew': 'Matthew',
'mark': 'Mark', 'luke': 'Luke', 'john': 'John', 'acts': 'Acts', 'romans': 'Romans',
'1 corinthians': '1 Corinthians', '2 corinthians': '2 Corinthians', 'galatians': 'Galatians',
'ephesians': 'Ephesians', 'philippians': 'Philippians', 'colossians': 'Colossians',
'1 thessalonians': '1 Thessalonians', '2 thessalonians': '2 Thessalonians',
'1 timothy': '1 Timothy', '2 timothy': '2 Timothy', 'titus': 'Titus', 'philemon': 'Philemon',
'hebrews': 'Hebrews', 'james': 'James', '1 peter': '1 Peter', '2 peter': '2 Peter',
'1 john': '1 John', '2 john': '2 John', '3 john': '3 John', 'jude': 'Jude', 'revelation': 'Revelation',
'gen': 'Genesis', 'ex': 'Exodus', 'lev': 'Leviticus', 'num': 'Numbers', 'deut': 'Deuteronomy',
'josh': 'Joshua', 'judg': 'Judges', 'ru': 'Ruth', '1sam': '1 Samuel', '2sam': '2 Samuel',
'1ki': '1 Kings', '2ki': '2 Kings', '1chr': '1 Chronicles', '2chr': '2 Chronicles',
'neh': 'Nehemiah', 'est': 'Esther', 'ps': 'Psalms', 'prov': 'Proverbs', 'eccl': 'Ecclesiastes',
'isa': 'Isaiah', 'jer': 'Jeremiah', 'lam': 'Lamentations', 'ezek': 'Ezekiel', 'dan': 'Daniel',
'hos': 'Hosea', 'mic': 'Micah', 'hab': 'Habakkuk', 'zech': 'Zechariah', 'mal': 'Malachi',
'matt': 'Matthew', 'mk': 'Mark', 'lk': 'Luke', 'jn': 'John', 'rom': 'Romans',
'1cor': '1 Corinthians', '2cor': '2 Corinthians', 'gal': 'Galatians', 'eph': 'Ephesians',
'phil': 'Philippians', 'col': 'Colossians', '1thess': '1 Thessalonians', '2thess': '2 Thessalonians',
'1tim': '1 Timothy', '2tim': '2 Timothy', 'tit': 'Titus', 'heb': 'Hebrews', 'jas': 'James',
'1pet': '1 Peter', '2pet': '2 Peter', '1jn': '1 John', '2jn': '2 John', '3jn': '3 John', 'rev': 'Revelation'
};
function capitalizeBook(name) {
return bookMap[name.toLowerCase()] || name;
}
// Try to parse as verse reference and return URL, or null
function parseVerseReference(input) {
// Book Chapter:Verse
var match = input.match(/^(.+)\s+(\d+):(\d+)$/i);
if (match) {
var book = capitalizeBook(match[1].trim());
return '/book/' + encodeURIComponent(book) + '/chapter/' + match[2] + '/verse/' + match[3];
}
// Book Chapter
match = input.match(/^(.+)\s+(\d+)$/i);
if (match) {
var book = capitalizeBook(match[1].trim());
return '/book/' + encodeURIComponent(book) + '/chapter/' + match[2];
}
return null;
}
// Category labels
var categoryLabels = {
books: 'Books',
verses: 'Verses',
topics: 'Topics',
resources: 'Resources',
stories: 'Stories',
plans: 'Reading Plans'
};
// Render search results
function renderResults(data) {
var results = data.results;
var html = '';
currentResults = [];
// Check if query looks like a verse reference
var verseUrl = parseVerseReference(data.query);
if (verseUrl) {
html += '<div class="search-results-category">';
html += '<div class="search-results-category-title">Go to</div>';
currentResults.push(verseUrl);
html += '<a href="' + verseUrl + '" class="search-result-item selected">';
html += '<span class="result-title">' + data.query + '</span>';
html += '<span class="result-meta">Press Enter to navigate</span>';
html += '</a></div>';
selectedIndex = 0;
}
if (Object.keys(results).length === 0 && !verseUrl) {
html = '<div class="search-no-results">No results found</div>';
} else {
// Render categories in specific order: books, topics, resources, stories, plans, verses
var categoryOrder = ['books', 'topics', 'resources', 'stories', 'plans', 'verses'];
categoryOrder.forEach(function(category) {
if (results[category] && results[category].length > 0) {
html += '<div class="search-results-category">';
html += '<div class="search-results-category-title">' + (categoryLabels[category] || category) + '</div>';
results[category].forEach(function(item) {
var title = item.name || item.title || item.reference;
var meta = '';
if (item.text) meta = item.text;
else if (item.category) meta = item.category;
currentResults.push(item.url);
html += '<a href="' + item.url + '" class="search-result-item">';
html += '<span class="result-title">' + title + '</span>';
if (meta) html += '<span class="result-meta">' + meta + '</span>';
html += '</a>';
});
html += '</div>';
}
});
// Add "View all results" link
html += '<a href="/search?q=' + encodeURIComponent(data.query) + '" class="search-view-all">View all verse results →</a>';
}
dropdown.innerHTML = html;
dropdown.classList.add('show');
if (!verseUrl) selectedIndex = -1;
}
// Perform search
function doSearch(query) {
if (query.length < 2) {
dropdown.classList.remove('show');
return;
}
dropdown.innerHTML = '<div class="search-loading">Searching...</div>';
dropdown.classList.add('show');
fetch('/api/universal-search?q=' + encodeURIComponent(query) + '&limit=4')
.then(function(r) { return r.json(); })
.then(renderResults)
.catch(function() {
dropdown.innerHTML = '<div class="search-no-results">Search error</div>';
});
}
// Input handler with debounce
searchInput.addEventListener('input', function() {
var query = this.value.trim();
clearTimeout(searchTimeout);
if (query.length < 2) {
dropdown.classList.remove('show');
return;
}
searchTimeout = setTimeout(function() {
doSearch(query);
}, 150);
});
// Keyboard navigation
searchInput.addEventListener('keydown', function(e) {
var items = dropdown.querySelectorAll('.search-result-item');
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
updateSelection(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
updateSelection(items);
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0 && currentResults[selectedIndex]) {
window.location.href = currentResults[selectedIndex];
} else {
// Try verse reference first, then search
var verseUrl = parseVerseReference(this.value.trim());
if (verseUrl) {
window.location.href = verseUrl;
} else if (this.value.trim()) {
window.location.href = '/search?q=' + encodeURIComponent(this.value.trim());
}
}
} else if (e.key === 'Escape') {
dropdown.classList.remove('show');
this.blur();
}
});
function updateSelection(items) {
items.forEach(function(item, i) {
item.classList.toggle('selected', i === selectedIndex);
});
}
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.sidebar-search')) {
dropdown.classList.remove('show');
}
});
// Reopen on focus if there's a query
searchInput.addEventListener('focus', function() {
if (this.value.trim().length >= 2) {
doSearch(this.value.trim());
}
});
})();
// Global viewport helpers for keyboard navigation
window.KJVNav = {
isInViewport: function(el) {
if (!el) return false;
var rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
},
findFirstVisibleIndex: function(elements) {
for (var i = 0; i < elements.length; i++) {
if (this.isInViewport(elements[i])) return i;
}
return 0;
},
isSelectionOffScreen: function(elements, selectedIndex) {
if (selectedIndex < 0) return true;
if (selectedIndex >= elements.length) return true;
return !this.isInViewport(elements[selectedIndex]);
},
// Sidebar navigation state
sidebarActive: false,
sidebarIndex: -1,
sidebarLinks: [],
currentPageNav: null,
// Activate sidebar navigation with 'n' key
initSidebarNav: function() {
var self = this;
var sidebar = document.querySelector('.nav-sidebar');
if (!sidebar) return;
// Get all navigable links in sidebar
self.sidebarLinks = Array.from(sidebar.querySelectorAll('a'));
function clearSidebarSelection() {
self.sidebarLinks.forEach(function(link) {
link.style.outline = '';
link.style.background = '';
});
}
function selectSidebarLink(index) {
clearSidebarSelection();
if (self.sidebarLinks.length === 0) return;
self.sidebarIndex = Math.max(0, Math.min(index, self.sidebarLinks.length - 1));
var link = self.sidebarLinks[self.sidebarIndex];
link.style.outline = '2px solid #4a7c59';
link.style.background = 'rgba(74, 124, 89, 0.1)';
link.scrollIntoView({ behavior: 'auto', block: 'center' });
// Expand parent details if collapsed
var details = link.closest('details');
if (details && !details.open) {
details.open = true;
}
}
function exitSidebar() {
clearSidebarSelection();
self.sidebarActive = false;
self.sidebarIndex = -1;
// Clear page nav selection if exists
if (self.currentPageNav && self.currentPageNav.clearSelection) {
self.currentPageNav.clearSelection();
}
}
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// 'n' to enter sidebar nav mode
if (e.key === 'n' && !self.sidebarActive) {
e.preventDefault();
self.sidebarActive = true;
// Clear any page content selection
if (self.currentPageNav && self.currentPageNav.clearSelection) {
self.currentPageNav.clearSelection();
}
// Start at first link or current page link
var currentLink = sidebar.querySelector('a.current');
var startIndex = currentLink ? self.sidebarLinks.indexOf(currentLink) : 0;
selectSidebarLink(startIndex >= 0 ? startIndex : 0);
return;
}
// Handle sidebar navigation when active
if (self.sidebarActive) {
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
selectSidebarLink(self.sidebarIndex + 1);
} else if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
selectSidebarLink(self.sidebarIndex - 1);
} else if (e.key === 'Enter') {
e.preventDefault();
if (self.sidebarIndex >= 0 && self.sidebarLinks[self.sidebarIndex]) {
window.location.href = self.sidebarLinks[self.sidebarIndex].href;
}
} else if (e.key === 'Escape') {
e.preventDefault();
exitSidebar();
} else if (e.key === 'ArrowRight' || e.key === 'l') {
// Expand details or go to link
e.preventDefault();
if (self.sidebarIndex >= 0) {
var link = self.sidebarLinks[self.sidebarIndex];
var details = link.closest('details');
var summary = link.closest('summary');
if (summary && details && !details.open) {
details.open = true;
} else {
window.location.href = link.href;
}
}
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
// Collapse details or exit sidebar
e.preventDefault();
if (self.sidebarIndex >= 0) {
var link = self.sidebarLinks[self.sidebarIndex];
var details = link.closest('details');
if (details && details.open) {
details.open = false;
} else {
exitSidebar();
}
} else {
exitSidebar();
}
}
}
});
},
// Simple linear keyboard navigation - just pass a CSS selector
initSimpleNav: function(selector, options) {
options = options || {};
var elements = Array.from(document.querySelectorAll(selector));
var selectedIndex = -1;
var pdfSelector = options.pdfSelector || '[class*="-download-btn"]';
var self = this;
function clearSelection() {
if (selectedIndex >= 0 && selectedIndex < elements.length) {
elements[selectedIndex].style.outline = '';
elements[selectedIndex].style.outlineOffset = '';
elements[selectedIndex].classList.remove('selected');
}
}
function selectElement(index) {
// Exit sidebar mode if active
if (self.sidebarActive) {
self.sidebarActive = false;
self.sidebarIndex = -1;
document.querySelectorAll('.nav-sidebar a').forEach(function(link) {
link.style.outline = '';
link.style.background = '';
});
}
clearSelection();
if (elements.length === 0) return;
selectedIndex = Math.max(0, Math.min(index, elements.length - 1));
elements[selectedIndex].style.outline = '2px solid #4a7c59';
elements[selectedIndex].style.outlineOffset = '8px';
elements[selectedIndex].classList.add('selected');
elements[selectedIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
}
// Store reference for sidebar to clear
var navInstance = { elements: elements, selectElement: selectElement, clearSelection: clearSelection };
self.currentPageNav = navInstance;
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// Don't handle if sidebar is active (except 'n' is handled above)
if (self.sidebarActive) return;
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
if (selectedIndex < 0 || KJVNav.isSelectionOffScreen(elements, selectedIndex)) {
selectElement(KJVNav.findFirstVisibleIndex(elements));
} else {
selectElement(selectedIndex + 1);
}
} else if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
if (selectedIndex < 0 || KJVNav.isSelectionOffScreen(elements, selectedIndex)) {
selectElement(KJVNav.findFirstVisibleIndex(elements));
} else {
selectElement(selectedIndex - 1);
}
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
e.preventDefault();
history.back();
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0) {
var el = elements[selectedIndex];
var link = el.tagName === 'A' ? el : el.querySelector('a');
if (link) window.location.href = link.href;
}
} else if (e.key === 'Escape') {
e.preventDefault();
clearSelection();
selectedIndex = -1;
} else if (e.key === 'p') {
e.preventDefault();
var pdfBtn = document.querySelector(pdfSelector);
if (pdfBtn) window.location.href = pdfBtn.href;
} else if (e.key === ' ') {
e.preventDefault();
if (selectedIndex >= 0 && window.KJVSpeech) {
var text = elements[selectedIndex].textContent || elements[selectedIndex].innerText;
KJVSpeech.speak(text);
}
}
});
return navInstance;
}
};
// Initialize sidebar navigation globally
document.addEventListener('DOMContentLoaded', function() {
KJVNav.initSidebarNav();
});
// Text-to-speech for any selected content
window.KJVSpeech = {
utterance: null,
speaking: false,
speak: function(text) {
if (!('speechSynthesis' in window)) {
console.log('Speech synthesis not supported');
return;
}
// Stop any current speech
this.stop();
// Clean up the text - remove verse numbers at start, collapse whitespace
text = text.replace(/^\s*\d+\s*/, '').replace(/\s+/g, ' ').trim();
if (!text) return;
this.utterance = new SpeechSynthesisUtterance(text);
this.utterance.rate = 0.9;
this.utterance.pitch = 1;
// Try to use a good English voice
var voices = speechSynthesis.getVoices();
var englishVoice = voices.find(function(v) {
return v.lang.startsWith('en') && v.name.includes('Daniel');
}) || voices.find(function(v) {
return v.lang.startsWith('en-GB');
}) || voices.find(function(v) {
return v.lang.startsWith('en');
});
if (englishVoice) {
this.utterance.voice = englishVoice;
}
this.speaking = true;
this.utterance.onend = function() {
KJVSpeech.speaking = false;
};
this.utterance.onerror = function() {
KJVSpeech.speaking = false;
};
speechSynthesis.speak(this.utterance);
},
stop: function() {
if ('speechSynthesis' in window) {
speechSynthesis.cancel();
}
this.speaking = false;
},
toggle: function(text) {
if (this.speaking) {
this.stop();
} else {
this.speak(text);
}
},
// Get text from the currently selected element (green box)
getSelectedText: function() {
// Find element with our green selection outline
var selected = document.querySelector('[style*="outline: 2px solid"]') ||
document.querySelector('[style*="outline:2px solid"]');
if (selected) {
return selected.textContent || selected.innerText;
}
// Fallback: Try specific verse selectors
var verseEl = document.querySelector('.verse-text-content') ||
document.querySelector('.verse-display .text') ||
document.querySelector('.verse-text') ||
document.querySelector('[data-verse-text]');
if (verseEl) {
return verseEl.textContent || verseEl.innerText;
}
return null;
}
};
// Simple resource reader state (for resource pages)
window.KJVResourceSpeech = {
speaking: false,
utterance: null,
suppressSpace: false
};
// Load voices (they may not be available immediately)
if ('speechSynthesis' in window) {
speechSynthesis.getVoices();
speechSynthesis.onvoiceschanged = function() {
speechSynthesis.getVoices();
};
}
// Default resource reader unless explicitly disabled
document.addEventListener('DOMContentLoaded', function() {
if (document.body && !document.body.dataset.resourceReader) {
document.body.dataset.resourceReader = 'true';
}
});
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Don't trigger if user is typing in an input field
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
// Allow Escape to clear focus
if (e.key === 'Escape') {
e.target.blur();
}
return;
}
// Cmd/Ctrl + D: Toggle dark mode
if ((e.metaKey || e.ctrlKey) && e.key === 'd') {
e.preventDefault();
toggleDarkMode();
}
// Cmd/Ctrl + B: Toggle sidebar
if ((e.metaKey || e.ctrlKey) && e.key === 'b') {
e.preventDefault();
var toggle = document.getElementById('sidebar-toggle');
if (toggle) {
toggle.checked = !toggle.checked;
toggle.dispatchEvent(new Event('change'));
}
}
// Cmd/Ctrl + K or /: Focus search
if (((e.metaKey || e.ctrlKey) && e.key === 'k') || e.key === '/') {
e.preventDefault();
window.location.href = '/search';
}
// Single key shortcuts for navigation (only work without modifiers)
if (!e.metaKey && !e.ctrlKey && !e.altKey) {
switch(e.key) {
case '1':
e.preventDefault();
window.location.href = '/';
break;
case '`':
e.preventDefault();
var toggle = document.getElementById('sidebar-toggle');
if (toggle) {
toggle.checked = !toggle.checked;
toggle.dispatchEvent(new Event('change'));
}
break;
case 'g':
e.preventDefault();
showVerseLookup();
break;
case 'r':
e.preventDefault();
window.location.href = '/resources';
break;
case 'b':
e.preventDefault();
window.location.href = '/books';
break;
case 's':
e.preventDefault();
window.location.href = '/stories';
break;
case '/':
e.preventDefault();
window.location.href = '/search';
break;
case 't':
e.preventDefault();
window.location.href = '/topics';
break;
case 'p':
e.preventDefault();
window.location.href = '/reading-plans';
break;
case 'f':
e.preventDefault();
window.location.href = '/family-tree';
break;
case 'c':
e.preventDefault();
window.location.href = '/strongs';
break;
case 'v':
e.preventDefault();
window.location.href = '/verse-of-the-day';
break;
case '.':
e.preventDefault();
window.location.href = '/random-verse';
break;
case '?':
showKeyboardHelp();
break;
case 'R':
e.preventDefault();
toggleRedLetters();
break;
case ' ':
// Space: Read aloud selected text (with optional resource-reader handling)
if (document.body && document.body.dataset && document.body.dataset.resourceReader === 'false') {
// explicitly disabled
} else if (document.body) {
e.preventDefault();
if (window.KJVResourceSpeech.suppressSpace) return;
if (!('speechSynthesis' in window)) return;
// If something is currently speaking or queued, stop it instead of starting a new read
if (window.KJVResourceSpeech.utterance && (speechSynthesis.speaking || speechSynthesis.pending || speechSynthesis.paused)) {
speechSynthesis.cancel();
window.KJVResourceSpeech.speaking = false;
window.KJVResourceSpeech.utterance = null;
window.KJVResourceSpeech.suppressSpace = true;
return;
}
if (window.KJVResourceSpeech.speaking) {
speechSynthesis.cancel();
window.KJVResourceSpeech.speaking = false;
window.KJVResourceSpeech.utterance = null;
window.KJVResourceSpeech.suppressSpace = true;
return;
}
var highlighted = document.querySelector('[style*=\"outline: 2px solid\"]:not(.toc):not(.breadcrumb):not(.chapters-section)') ||
document.querySelector('[style*=\"outline:2px solid\"]:not(.toc):not(.breadcrumb):not(.chapters-section)') ||
document.querySelector('.selected:not(.toc):not(.breadcrumb):not(.chapters-section)');
var combined = '';
if (highlighted) {
var hclone = highlighted.cloneNode(true);
hclone.querySelectorAll('.sidenote, .marginnote, .sidenote-number, .margin-toggle, .breadcrumb, .toc, .chapters-section, script, style').forEach(function(el) { el.remove(); });
combined = (hclone.textContent || hclone.innerText || '').trim();
} else {
var article = document.querySelector('article');
if (article) {
// If this is a card grid, concatenate the highlighted or first card
var selectedCard = article.querySelector('.resource-card[style*=\"outline\"]') || article.querySelector('.resource-card');
if (selectedCard) {
var cclone = selectedCard.cloneNode(true);
cclone.querySelectorAll('.sidenote, .marginnote, .sidenote-number, .margin-toggle, .breadcrumb, .toc, .chapters-section, script, style').forEach(function(el) { el.remove(); });
combined = (cclone.textContent || cclone.innerText || '').trim();
} else {
// Prefer reading substantive text blocks instead of the whole article
var textBlocks = Array.from(article.querySelectorAll('h1, h2, h3, p, li, blockquote, .intro-text, .verse-text, .resource-description, .resource-description-body, .resource-item-description, .parable-description, .angel-description, .covenant-description, .festival-description, .prophet-description, .fruit-description, .name-description, .woman-description, .apostle-description'))
.filter(function(el) {
if (el.closest('.toc') || el.closest('.chapters-section') || el.closest('.breadcrumb')) return false;
var txt = (el.textContent || el.innerText || '').trim();
return txt.length > 120;
})
.map(function(el) {
var clone = el.cloneNode(true);
clone.querySelectorAll('.sidenote, .marginnote, .sidenote-number, .margin-toggle, .breadcrumb, .toc, .chapters-section, script, style').forEach(function(x) { x.remove(); });
return (clone.textContent || clone.innerText || '').trim();
})
.filter(Boolean);
if (textBlocks.length > 0) {
combined = textBlocks.join('\\n\\n');
} else {
var aclone = article.cloneNode(true);
aclone.querySelectorAll('.sidenote, .marginnote, .sidenote-number, .margin-toggle, .breadcrumb, .toc, .chapters-section, script, style').forEach(function(el) { el.remove(); });
combined = (aclone.textContent || aclone.innerText || '').trim();
}
}
}
}
if (combined) {
var utter = new SpeechSynthesisUtterance(combined);
var voices = speechSynthesis.getVoices();
var englishVoice = voices.find(function(v) { return v.lang && v.lang.toLowerCase().startsWith('en') && v.name.includes('Daniel'); }) ||
voices.find(function(v) { return v.lang && v.lang.toLowerCase().startsWith('en-gb'); }) ||
voices.find(function(v) { return v.lang && v.lang.toLowerCase().startsWith('en'); });
if (englishVoice) utter.voice = englishVoice;
utter.onend = function() {
window.KJVResourceSpeech.speaking = false;
window.KJVResourceSpeech.utterance = null;
};
utter.onerror = function() {
window.KJVResourceSpeech.speaking = false;
window.KJVResourceSpeech.utterance = null;
};
window.KJVResourceSpeech.speaking = true;
window.KJVResourceSpeech.utterance = utter;
window.KJVResourceSpeech.suppressSpace = false;
speechSynthesis.cancel();
speechSynthesis.speak(utter);
}
} else {
var selectedText = KJVSpeech.getSelectedText();
if (selectedText) {
e.preventDefault();
KJVSpeech.toggle(selectedText);
}
}
break;
}
}
});
// Clear suppression after spacebar is released
document.addEventListener('keyup', function(e) {
if (e.key === ' ') {
window.KJVResourceSpeech.suppressSpace = false;
}
});
// Quick verse lookup
function showVerseLookup() {
var reference = prompt('Enter verse reference (e.g., John 3:16, Psalm 23, Genesis 1):');
if (!reference) return;
reference = reference.trim();
// Try to match: Book Chapter:Verse
var match = reference.match(/^(.+?)\s+(\d+):(\d+)$/i);
if (match) {
var book = match[1].trim();
var chapter = match[2];
var verse = match[3];
window.location.href = '/book/' + encodeURIComponent(book) + '/chapter/' + chapter + '/verse/' + verse;
return;
}
// Try to match: Book Chapter
match = reference.match(/^(.+?)\s+(\d+)$/i);
if (match) {
var book = match[1].trim();
var chapter = match[2];
window.location.href = '/book/' + encodeURIComponent(book) + '/chapter/' + chapter;
return;
}
// Try to match: Book
match = reference.match(/^(.+)$/i);
if (match) {
var book = match[1].trim();
window.location.href = '/book/' + encodeURIComponent(book);
return;
}
}
// Keyboard shortcuts help modal
function showKeyboardHelp() {
// Remove existing modal if present
var existingModal = document.getElementById('keyboard-help-modal');
if (existingModal) {
existingModal.remove();
return;
}
var modal = document.createElement('div');
modal.id = 'keyboard-help-modal';
modal.innerHTML =
'<div class="keyboard-help-backdrop"></div>' +
'<div class="keyboard-help-content">' +
'<button class="keyboard-help-close" aria-label="Close">&times;</button>' +
'<h2>Keyboard Shortcuts</h2>' +
'<div class="keyboard-help-columns">' +
'<div class="keyboard-help-section">' +
'<h3>Navigation</h3>' +
'<div class="shortcut"><kbd>b</kbd><span>Books</span></div>' +
'<div class="shortcut"><kbd>s</kbd><span>Stories</span></div>' +
'<div class="shortcut"><kbd>r</kbd><span>Resources</span></div>' +
'<div class="shortcut"><kbd>t</kbd><span>Topics</span></div>' +
'<div class="shortcut"><kbd>p</kbd><span>Reading Plans</span></div>' +
'<div class="shortcut"><kbd>f</kbd><span>Family Tree</span></div>' +
'<div class="shortcut"><kbd>c</kbd><span>Strong\'s Concordance</span></div>' +
'<div class="shortcut"><kbd>v</kbd><span>Verse of the Day</span></div>' +
'<div class="shortcut"><kbd>.</kbd><span>Random Verse</span></div>' +
'<div class="shortcut"><kbd>g</kbd><span>Quick verse lookup</span></div>' +
'</div>' +
'<div class="keyboard-help-section">' +
'<h3>Page Navigation</h3>' +
'<div class="shortcut"><kbd>↑</kbd> / <kbd>k</kbd><span>Previous item</span></div>' +
'<div class="shortcut"><kbd>↓</kbd> / <kbd>j</kbd><span>Next item</span></div>' +
'<div class="shortcut"><kbd>←</kbd> / <kbd>h</kbd><span>Go back</span></div>' +
'<div class="shortcut"><kbd>→</kbd> / <kbd>l</kbd><span>Next page</span></div>' +
'<div class="shortcut"><kbd>Enter</kbd><span>Select / Open</span></div>' +
'</div>' +
'<div class="keyboard-help-section">' +
'<h3>Other</h3>' +
'<div class="shortcut"><kbd>Space</kbd><span>Read aloud</span></div>' +
'<div class="shortcut"><kbd>`</kbd><span>Toggle sidebar</span></div>' +
'<div class="shortcut"><kbd>⌘</kbd>+<kbd>D</kbd><span>Toggle dark mode</span></div>' +
'<div class="shortcut"><kbd>R</kbd><span>Toggle red letters</span></div>' +
'<div class="shortcut"><kbd>/</kbd><span>Search</span></div>' +
'<div class="shortcut"><kbd>?</kbd><span>Show this help</span></div>' +
'<div class="shortcut"><kbd>Esc</kbd><span>Close / Clear focus</span></div>' +
'</div>' +
'</div>' +
'</div>';
// Add styles
var style = document.createElement('style');
style.textContent =
'#keyboard-help-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 10000; display: flex; align-items: center; justify-content: center; }' +
'.keyboard-help-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); }' +
'.keyboard-help-content { position: relative; background: var(--bg-color); border-radius: 8px; padding: 1.5rem 2rem; max-width: 700px; max-height: 90vh; overflow-y: auto; box-shadow: 0 10px 40px rgba(0,0,0,0.3); }' +
'.keyboard-help-content h2 { margin: 0 0 1rem 0; font-size: 1.4rem; font-weight: 600; }' +
'.keyboard-help-close { position: absolute; top: 0.75rem; right: 0.75rem; background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-secondary); padding: 0.25rem 0.5rem; line-height: 1; }' +
'.keyboard-help-close:hover { color: var(--text-color); }' +
'.keyboard-help-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }' +
'.keyboard-help-section h3 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-secondary); margin: 0 0 0.75rem 0; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-color); }' +
'.shortcut { display: flex; align-items: center; gap: 0.5rem; margin: 0.4rem 0; font-size: 0.9rem; }' +
'.shortcut kbd { display: inline-block; padding: 0.15rem 0.4rem; font-family: inherit; font-size: 0.8rem; background: var(--code-bg); border: 1px solid var(--border-color-darker); border-radius: 3px; min-width: 1.5rem; text-align: center; }' +
'.shortcut span { color: var(--text-secondary); }' +
'@media (max-width: 700px) { .keyboard-help-columns { grid-template-columns: 1fr; } .keyboard-help-content { margin: 1rem; padding: 1rem 1.5rem; } }';
modal.appendChild(style);
document.body.appendChild(modal);
// Close handlers
modal.querySelector('.keyboard-help-backdrop').addEventListener('click', function() {
modal.remove();
});
modal.querySelector('.keyboard-help-close').addEventListener('click', function() {
modal.remove();
});
document.addEventListener('keydown', function closeOnEsc(e) {
if (e.key === 'Escape') {
modal.remove();
document.removeEventListener('keydown', closeOnEsc);
}
});
}
// Verse tooltip functionality
(function() {
// Create tooltip element
var tooltip = document.createElement('div');
tooltip.className = 'verse-tooltip';
document.body.appendChild(tooltip);
// Cache for fetched verses
var verseCache = {};
// Parse verse URL to extract book, chapter, and verse
function parseVerseUrl(url) {
// Try to match single verse URL: /book/John/chapter/3/verse/16
var match = url.match(/\/book\/([^\/]+)\/chapter\/(\d+)\/verse\/(\d+)/);
if (match) {
return {
book: decodeURIComponent(match[1]),
chapter: match[2],
verse: match[3],
verseEnd: null,
cacheKey: match[1] + '_' + match[2] + '_' + match[3],
isRange: false
};
}
// Try to match verse range URL: /book/Proverbs/chapter/31#verse-26-28
match = url.match(/\/book\/([^\/]+)\/chapter\/(\d+)#verse-(\d+)-(\d+)/);
if (match) {
return {
book: decodeURIComponent(match[1]),
chapter: match[2],
verse: match[3],
verseEnd: match[4],
cacheKey: match[1] + '_' + match[2] + '_' + match[3] + '-' + match[4],
isRange: true
};
}
return null;
}
// Fetch verse text from server using API
async function fetchVerseText(book, chapter, verse, verseEnd, cacheKey) {
// Check cache first
if (verseCache[cacheKey]) {
return verseCache[cacheKey];
}
try {
var url;
if (verseEnd) {
// Use verse range API endpoint
url = '/api/verse-range/' + encodeURIComponent(book) + '/' + chapter + '/' + verse + '/' + verseEnd;
} else {
// Use single verse API endpoint
url = '/api/verse/' + encodeURIComponent(book) + '/' + chapter + '/' + verse;
}
var response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch verse');
var data = await response.json();
// Cache the result
verseCache[cacheKey] = {
reference: data.reference,
text: data.text
};
return verseCache[cacheKey];
} catch (error) {
console.error('Error fetching verse:', error);
return {
reference: verseEnd ? book + ' ' + chapter + ':' + verse + '-' + verseEnd : book + ' ' + chapter + ':' + verse,
text: 'Error loading verse'
};
}
}
// Show tooltip
function showTooltip(verseData, mouseX, mouseY) {
tooltip.innerHTML =
'<span class="verse-tooltip-reference">' + verseData.reference + '</span>' +
'<span class="verse-tooltip-text">' + verseData.text + '</span>';
// Position tooltip
var tooltipRect = tooltip.getBoundingClientRect();
var x = mouseX + 15;
var y = mouseY + 15;
// Adjust if tooltip goes off right edge
if (x + tooltipRect.width > window.innerWidth) {
x = mouseX - tooltipRect.width - 15;
}
// Adjust if tooltip goes off bottom edge
if (y + tooltipRect.height > window.innerHeight) {
y = mouseY - tooltipRect.height - 15;
}
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
tooltip.classList.add('show');
}
// Hide tooltip
function hideTooltip() {
tooltip.classList.remove('show');
}
// Event delegation for verse links
document.addEventListener('mouseover', function(e) {
var target = e.target;
// Check if target is a link or inside a link
if (target.tagName !== 'A') {
target = target.closest('a');
}
if (!target || !target.href) return;
// Skip if this is a cross-reference link (has its own tooltip system)
if (target.classList.contains('cross-ref-link')) return;
// Skip if this is a verse number link (the number at the start of each verse)
if (target.classList.contains('verse-number-link')) return;
// Skip links in search dropdowns
if (target.closest('.search-results-dropdown') || target.closest('.search-dropdown') ||
target.closest('.lookup-results-dropdown') || target.closest('.story-search-dropdown')) return;
// Skip links in concordance results (verse text already shown inline)
if (target.closest('.occurrence-reference') || target.closest('.occurrence')) return;
// Skip links in cross-references section (has its own CSS tooltip system)
if (target.closest('.cross-references-section')) return;
// Skip links in navigation, buttons, actions, headers, and non-content areas
if (target.closest('nav') || target.closest('header') || target.closest('footer') ||
target.closest('.toc') || target.closest('.breadcrumb') ||
target.closest('button') || target.closest('[class*="-btn"]') || target.closest('[class*="-actions"]') ||
target.closest('[class*="download"]') || target.closest('[class*="print"]') ||
target.closest('.share-container') || target.closest('.share-buttons') ||
target.closest('.chapter-nav') || target.closest('.verse-nav') ||
target.closest('h1') || target.closest('h2') || target.closest('h3')) return;
// Only show tooltips for links inside content paragraphs, sections, or article content
if (!target.closest('p') && !target.closest('section') && !target.closest('article') &&
!target.closest('.verse-item') && !target.closest('.verse-text') &&
!target.closest('.intro-text') && !target.closest('.description') &&
!target.closest('.sidenote') && !target.closest('.marginnote') &&
!target.closest('li')) return;
var verseInfo = parseVerseUrl(target.pathname + target.hash);
if (!verseInfo) return;
// Show loading state
tooltip.innerHTML = '<span class="verse-tooltip-loading">Loading...</span>';
tooltip.style.left = (e.pageX + 15) + 'px';
tooltip.style.top = (e.pageY + 15) + 'px';
tooltip.classList.add('show');
// Fetch and display verse
fetchVerseText(verseInfo.book, verseInfo.chapter, verseInfo.verse, verseInfo.verseEnd, verseInfo.cacheKey)
.then(function(verseData) {
// Only show if still hovering
if (target.matches(':hover')) {
showTooltip(verseData, e.pageX, e.pageY);
}
});
// Track mouse movement for tooltip positioning
var mouseMoveHandler = function(e) {
if (tooltip.classList.contains('show')) {
var x = e.pageX + 15;
var y = e.pageY + 15;
tooltip.style.left = x + 'px';
tooltip.style.top = y + 'px';
}
};
target.addEventListener('mousemove', mouseMoveHandler);
// Hide tooltip on mouse leave
target.addEventListener('mouseleave', function() {
hideTooltip();
target.removeEventListener('mousemove', mouseMoveHandler);
}, { once: true });
});
})();
// Site-wide verse linking
(function() {
function linkVerseReferences(element) {
if (!element) return;
// Get all text nodes, but skip those inside anchors (already linked)
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, {
acceptNode: function(node) {
// Skip text nodes that are inside anchor tags (already linked)
if (node.parentNode && node.parentNode.tagName === 'A') {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}, false);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
textNodes.forEach(function(textNode) {
let text = textNode.textContent;
let changed = false;
// First, handle comma-separated verse lists like "Psalms 7:17, 9:2, 18:13"
text = text.replace(/\b(\d?\s?[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+((?:\d+:\d+(?:-\d+)?(?:\s*,\s*)?)+)/g, function(match, book, verseList) {
book = book.trim();
if (!verseList.includes(',')) {
return match;
}
changed = true;
var verses = verseList.split(/\s*,\s*/);
var links = verses.map(function(verseRef) {
verseRef = verseRef.trim();
var parts = verseRef.match(/^(\d+):(\d+)(?:-(\d+))?$/);
if (parts) {
var chapter = parts[1];
var verseStart = parts[2];
var verseEnd = parts[3];
if (verseEnd) {
return '<a href="/book/' + book + '/chapter/' + chapter + '#verse-' + verseStart + '-' + verseEnd + '">' + book + ' ' + chapter + ':' + verseStart + '-' + verseEnd + '</a>';
} else {
// Use chapter anchor instead of verse page
return '<a href="/book/' + book + '/chapter/' + chapter + '#verse-' + verseStart + '">' + book + ' ' + chapter + ':' + verseStart + '</a>';
}
}
return verseRef;
});
return links.join(', ');
});
// Then handle individual verse references like "John 3:16" or "Romans 8:28-29"
text = text.replace(/\b(\d?\s?[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(\d+):(\d+)(?:-(\d+))?\b/g, function(match, book, chapter, verseStart, verseEnd) {
changed = true;
book = book.trim();
if (verseEnd) {
return '<a href="/book/' + book + '/chapter/' + chapter + '#verse-' + verseStart + '-' + verseEnd + '">' + match + '</a>';
} else {
// Use chapter anchor instead of verse page
return '<a href="/book/' + book + '/chapter/' + chapter + '#verse-' + verseStart + '">' + match + '</a>';
}
});
if (changed) {
const span = document.createElement('span');
span.innerHTML = text;
textNode.parentNode.replaceChild(span, textNode);
while (span.firstChild) {
span.parentNode.insertBefore(span.firstChild, span);
}
span.parentNode.removeChild(span);
}
});
}
// Link verses in common content areas
document.querySelectorAll('.intro-text, .prophet-description, .angel-description, .covenant-description, p, li').forEach(function(element) {
// Skip if already processed or if it's in the sidebar
if (element.closest('.nav-sidebar') || element.dataset.verseLinked) {
return;
}
element.dataset.verseLinked = 'true';
linkVerseReferences(element);
});
})();
{% if not disable_analytics %}
// Gauges analytics (respects kr=1 opt-out)
if (!window.analyticsDisabled) {
var _gauges = _gauges || [];
(function() {
var t = document.createElement('script');
t.type = 'text/javascript';
t.async = true;
t.id = 'gauges-tracker';
t.setAttribute('data-site-id', '6834bd650d851064ae28dc13');
t.setAttribute('data-track-path', 'https://track.gaug.es/track.gif');
t.src = 'https://d2fuc4clr7gvcn.cloudfront.net/track.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(t, s);
})();
}
{% endif %}
</script>
</body>
</html>