1<!DOCTYPE html> 2<html lang="en"> 3 <!-- Reconciled with GitHub main (commit b5563525): site-wide Home/Read/Gospel/Resources tabs, sidepanel shell, compact reader + search UI. --> 4 <head> 5
5<script> 6 (function () { 7 var proto = XMLHttpRequest.prototype; 8 var open = proto.open, setRequestHeader = proto.setRequestHeader, send = proto.send; 9 var TOKEN_KEY = 'base44_access_token'; 10 var storedTokenHeader = function () { 11 try { return 'Bearer ' + localStorage.getItem(TOKEN_KEY); } catch (e) { return null; } 12 }; 13 var forgetStoredToken = function () { 14 try { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem('token'); } catch (e) {} 15 }; 16 17 proto.open = function (method, url) { 18 this.__b44IsAuthCheck = /\/entities\/User\/me(\?|$)/.test(url); 19 return open.apply(this, arguments); 20 }; 21 22 proto.setRequestHeader = function (name, value) { 23 if (this.__b44IsAuthCheck && name.toLowerCase() === 'authorization' && value === storedTokenHeader()) { 24 this.__b44SentToken = value; 25 } 26 return setRequestHeader.call(this, name, value); 27 }; 28 29 proto.send = function () { 30 this.addEventListener('loadend', function () { 31 var rejectedTokenStillStored = this.status === 401 && this.__b44SentToken === storedTokenHeader(); 32 if (rejectedTokenStillStored) forgetStoredToken(); 33 }); 34 return send.apply(this, arguments); 35 }; 36})(); 37 </script>
37 38 <meta charset="utf-8"/> 39 <!-- Pre-empt the vite plugin's unhandledrejection handler, which crashes 40 ("Cannot read properties of undefined (reading 'match')") when the 41 rejected value is not an Error object. This capturing-phase listener 42 runs before the plugin's handler and stops propagation for non-Error 43 rejections so the plugin never sees them. --> 44
44<script> 45 (function () { 46 window.addEventListener('unhandledrejection', function (event) { 47 var reason = event && event.reason; 48 // The vite plugin's handler accesses reason.stack.match(...). If the 49 // rejection is not a real Error (no string .stack), the plugin crashes 50 // with "Cannot read properties of undefined (reading 'match')". Stop 51 // propagation for any non-Error rejection so the plugin never sees it. 52 if (!reason || typeof reason.stack !== 'string') { 53 event.preventDefault(); 54 event.stopImmediatePropagation(); 55 return; 56 } 57 // Swallow SW not-found rejections (preview sandbox can't serve /sw.js) 58 var msg = (typeof reason.message === 'string') ? reason.message : ''; 59 if (/Failed to (update|register) a ServiceWorker/i.test(msg)) { 60 event.preventDefault(); 61 event.stopImmediatePropagation(); 62 return; 63 } 64 }, true); 65 })(); 66 </script>
66 67 <!-- Detect Internet Explorer / very old browsers and redirect to the legacy 68 reader BEFORE any modern JS runs (the React app can't run on IE). --> 69
69<script> 70 (function () { 71 try { 72 var ua = navigator.userAgent || ''; 73 // IE10/11 ("Trident") and older IE ("MSIE"). Not Edge. 74 var isIE = /Trident|MSIE/i.test(ua); 75 // A dedicated legacy domain should ALWAYS land on the server-rendered 76 // legacy reader, regardless of browser. Send every visitor straight 77 // to the function endpoint (which the React /legacy page only redirects 78 // to anyway). 79 var legacyHost = (window.location.hostname || '').toLowerCase(); 80 var qsEarly = window.location.search || ''; 81 var optedOutEarly = qsEarly.indexOf('nolegacy=1') !== -1; 82 var STATIC_LEGACY_FILE = 'https://media.base44.com/files/public/6a05d76723afe58d80c589e8/kjb-reader-legacy.html'; 83 // Static file redirect removed â legacy domains now serve the function page directly. 84 // Only treat as "too old" if it's clearly NOT a modern browser. 85 // The bare Promise/fetch check can misfire in some embedded/in-app 86 // browsers, so we additionally require an old-IE-style userAgent. 87 var tooOld = isIE && ((typeof window.Promise === 'undefined') || (typeof window.fetch === 'undefined')); 88 // Parse query string manually (IE doesn't support URLSearchParams reliably) 89 var qs = window.location.search || ''; 90 var optedOut = qs.indexOf('nolegacy=1') !== -1; 91 // A ?tab= param AT THE ROOT path ("/") means a legacy deep link 92 // (gospel/resources/about/debug) â route it to the server-rendered 93 // legacy reader. Only fires on "/" so it never interferes with the 94 // "/legacy" route's own ?tab= links/form submits. 95 var atRoot = window.location.pathname === '/' || window.location.pathname === ''; 96 var wantsLegacyTab = atRoot && qs.indexOf('tab=') !== -1; 97 98 // Native app origins (capacitor://localhost â the iOS offline-fallback 99 // copy; Android always serves /legacy through the React route) can't 100 // reach the server-rendered function page, and offline they must not 101 // even try: the React /legacy page loads the identical page from the 102 // app bundle instead (see src/pages/LegacyReader.jsx). So on a 103 // capacitor: origin a /legacy visit falls through to the React route 104 // instead of redirecting to a path that would 404. 105 var isNativeCap = /^capacitor:/i.test((window.location.protocol || '')); 106 107 // A direct visit to "/legacy" always means the server-rendered Full 108 // Bible reader â send EVERY browser straight to the function endpoint 109 // (the React /legacy page can't run on IE and only redirects anyway). 110 if (window.location.pathname === '/legacy' && !optedOut && !isNativeCap) { 111 var lHost = window.location.hostname || ''; 112 var lIsCustom = lHost.indexOf('base44.app') === -1 && lHost.indexOf('localhost') === -1; 113 var lOrigin = window.location.origin || (window.location.protocol + '//' + window.location.host); 114 if (lIsCustom) { 115 window.location.replace(lOrigin + '/functions/legacy'); 116 } else { 117 var lAppId = null; 118 var lIdx = qs.indexOf('app_id='); 119 if (lIdx !== -1) { 120 var lPart = qs.substring(lIdx + 7); 121 var lAmp = lPart.indexOf('&'); 122 lAppId = lAmp !== -1 ? lPart.substring(0, lAmp) : lPart; 123 } 124 if (!lAppId) { try { lAppId = localStorage.getItem('base44_app_id'); } catch (e) {} } 125 window.location.replace(lAppId ? '/api/apps/' + lAppId + '/functions/legacy' : '/functions/legacy'); 126 } 127 return; 128 } 129 if (wantsLegacyTab && !optedOut) { 130 if (isNativeCap) { 131 // Same reasoning as above: the native offline copy renders the 132 // legacy reader from the app bundle via the React /legacy route. 133 window.location.replace('/legacy' + qs); 134 return; 135 } 136 var tHost = window.location.hostname || ''; 137 var tIsCustom = tHost.indexOf('base44.app') === -1 && tHost.indexOf('localhost') === -1; 138 if (tIsCustom) { 139 window.location.replace(window.location.origin + '/legacy' + qs); 140 } else { 141 var tAppId = null;
142 var tIdx = qs.indexOf('app_id='); 143 if (tIdx !== -1) { 144 var tPart = qs.substring(tIdx + 7); 145 var tAmp = tPart.indexOf('&'); 146 tAppId = tAmp !== -1 ? tPart.substring(0, tAmp) : tPart; 147 } 148 if (!tAppId) { try { tAppId = localStorage.getItem('base44_app_id'); } catch (e) {} } 149 window.location.replace((tAppId ? '/api/apps/' + tAppId + '/functions/legacy' : '/functions/legacy') + qs); 150 } 151 return; 152 } 153 if ((isIE || tooOld) && !optedOut) { 154 var host = window.location.hostname || ''; 155 var isCustom = host.indexOf('base44.app') === -1 && host.indexOf('localhost') === -1; 156 // IE8 lacks window.location.origin â build it manually as a fallback. 157 var origin = window.location.origin || (window.location.protocol + '//' + window.location.host); 158 var legacyUrl; 159 if (isCustom) { 160 legacyUrl = origin + '/functions/legacy'; 161 } else { 162 var appId = null; 163 var appIdx = qs.indexOf('app_id='); 164 if (appIdx !== -1) { 165 var appPart = qs.substring(appIdx + 7); 166 var ampIdx = appPart.indexOf('&'); 167 appId = ampIdx !== -1 ? appPart.substring(0, ampIdx) : appPart; 168 } 169 if (!appId) { 170 try { appId = localStorage.getItem('base44_app_id'); } catch (e) {} 171 } 172 legacyUrl = appId 173 ? '/api/apps/' + appId + '/functions/legacy' 174 : '/functions/legacy'; 175 } 176 // Force immediate redirect before any rendering 177 window.location.replace(legacyUrl); 178 } 179 } catch (e) { 180 // If detection fails on an old browser, redirect STRAIGHT to the 181 // server-rendered function (never the React /legacy page, which IE 182 // can't run). Use the origin path which works on the custom domain. 183 try { 184 window.location.replace(window.location.origin + '/functions/legacy'); 185 } catch (e2) { 186 try { window.location.replace('/functions/legacy'); } catch (e3) {} 187 } 188 } 189 })(); 190 </script>
190 191 <!-- Favicons: the same KJB Reader icon used by the PWA manifest, served 192 same-origin as static files in public/icons/ (kjb-icon-192/512 193 PNG assets) so it always resolves â never the dead cross-app media 194 reference from the previous app instance. Browsers prefer these 195 explicit <link rel="icon"> entries over the platform's default 196 /favicon.ico redirect. --> 197 <link href="/icons/kjb-icon-512.png" rel="icon" sizes="512x512" type="image/png"/> 198 <link href="/icons/kjb-icon-512.png" rel="shortcut icon" type="image/png"/> 199 <meta content="width=device-width, initial-scale=1.0, viewport-fit=cover" name="viewport"/> 200 <!-- Capture beforeinstallprompt early (Chrome/Edge/Samsung/Android mobile) --> 201
201<script> 202 (function() { 203 if (typeof window === 'undefined') return; 204 // Set up global deferred prompt storage immediately 205 window.kjbDeferredPrompt = null; 206 window.kjbPromptFired = false; 207 window.kjbPromptedThisSession = false; 208 209 // Capture beforeinstallprompt as early as possible (mobile + desktop). 210 // Chromium browsers (Chrome/Edge/Samsung Internet) only fire this event 211 // for an app that is NOT currently installed â so seeing it fire is the 212 // most reliable signal we have that any previously-remembered 213 // "installed" flag is now stale (e.g. the user uninstalled the PWA from 214 // their home screen/app list). Clearing it here is what makes the app 215 // automatically fall back to showing the Install button again, without 216 // the user having to manually reset anything. 217 window.addEventListener('beforeinstallprompt', function(e) { 218 e.preventDefault(); 219 window.kjbDeferredPrompt = e; 220 window.kjbPromptFired = true; 221 window.kjbPromptedThisSession = true; 222 try { localStorage.removeItem('kjb-is-installed'); } catch (err) {} 223 console.log('[PWA] beforeinstallprompt captured - not currently installed, native prompt ready'); 224 window.dispatchEvent(new Event('kjb-install-change')); 225 window.dispatchEvent(new Event('pwa-installable')); 226 }); 227 228 // Fires the moment the user accepts the native install prompt (or 229 // installs via the browser's own address-bar/menu affordance). 230 window.addEventListener('appinstalled', function() { 231 console.log('[PWA] appinstalled event fired'); 232 window.kjbDeferredPrompt = null; 233 window.kjbPromptFired = false; 234 try { 235 localStorage.setItem('kjb-is-installed', 'true'); 236 // Don't re-nag the install/welcome prompt after a successful install 237 localStorage.setItem('kjb-prompt-dismissed', 'true'); 238 localStorage.setItem('kjb-install-dismissed', 'true'); 239 } catch (e) {} 240 // Let the open browser tab (useInstallPrompt / FirstLoadPrompt / 241 // InstallAppSection) flip to the "Installed" state immediately. 242 window.dispatchEvent(new Event('storage')); 243 window.dispatchEvent(new Event('kjb-install-change')); 244 }); 245 })(); 246 </script>
246 247 <!-- Allow caching for offline support --> 248 <meta content="public, max-age=3600, stale-while-revalidate=86400" http-equiv="Cache-Control"/> 249 <title> 250 KJB Reader 251 </title> 252 <!-- Preload critical resources --> 253 <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;600;700&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=Dancing+Script:wght@300;400;500;600;700&family=Great+Vibes&family=Inter:wght@300;400;500;600;700&family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700;1,900&family=Comic+Neue:ital,wght@0,400;0,700;1,400&display=block" rel="stylesheet"/> 254 <meta content="Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word of God." name="description"/> 255 <!-- Tells the browser/WebView which canvas colour to paint for the blank 256 page BEFORE any of our CSS applies â without it, the very first frame 257 is the browser's default white, which shows as a white flash on a 258 dark-mode launch. --> 259 <meta content="dark light" name="color-scheme"/> 260 <meta content="#0f1117" name="theme-color"/> 261 <meta content="yes" name="apple-mobile-web-app-capable"/> 262 <meta content="black-translucent" name="apple-mobile-web-app-status-bar-style"/> 263 <meta content="KJB Reader" name="apple-mobile-web-app-title"/> 264 <link href="/icons/kjb-icon-512.png" rel="apple-touch-icon" sizes="512x512"/> 265 <meta content="yes" name="mobile-web-app-capable"/> 266 <!-- Performance optimizations --> 267 <link crossorigin="" href="https://media.base44.com" rel="preconnect"/> 268 <link href="https://media.base44.com" rel="dns-prefetch"/> 269 <link crossorigin="" href="https://fonts.googleapis.com" rel="preconnect"/> 270 <link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/> 271 <link as="image" href="https://media.base44.com/images/public/6a05d76723afe58d80c589e8/2279e016e_8e738d108_cfb4bf781_Untitled.png" rel="preload"/> 272 <link as="font" crossorigin="" href="/fonts/OpenDyslexic-regular.woff" rel="preload" type="font/woff"/> 273 <link as="font" crossorigin="" href="/fonts/OpenDyslexic-bold.woff" rel="preload" type="font/woff"/> 274 <link as="font" crossorigin="" href="/fonts/OpenDyslexic-italic.woff" rel="preload" type="font/woff"/> 275 <meta content="Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word of God., " name="description"/> 276 <meta content="Bible, King James Bible, KJB, Pure Cambridge Edition, offline Bible, Christian, scripture" name="keywords"/> 277 <!-- Social sharing preview (Open Graph / Twitter Card) --> 278 <meta content="KJB Reader" property="og:title"/> 279 <meta content="Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word of God., " property="og:description"/> 280 <meta content="https://media.base44.com/images/public/6a8011c360ff52dad38eb2f3/e85b31b2e_10fb7857-d202-4070-98ce-c14094770136.jpg/v1/fill/w_1200,h_630/e85b31b2e_10fb7857-d202-4070-98ce-c14094770136.jpg" property="og:image"/> 281 <meta content="1200" property="og:image:width"/> 282 <meta content="630" property="og:image:height"/> 283 <meta content="summary_large_image" name="twitter:card"/> 284 <meta content="KJB Reader" name="twitter:title"/> 285 <meta content="Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word of God., " name="twitter:description"/> 286 <meta content="https://media.base44.com/images/public/6a8011c360ff52dad38eb2f3/e85b31b2e_10fb7857-d202-4070-98ce-c14094770136.jpg/v1/fill/w_1200,h_630/e85b31b2e_10fb7857-d202-4070-98ce-c14094770136.jpg" name="twitter:image"/> 287 <!-- PWA manifest: served by the dynamic `manifest` backend function so the 288 icons/screenshots saved from the DevTools Manifest editor take effect 289 live (it reads the ManifestConfig entity and responds no-store). 290 The URL is STILL a single stable path (required for Chromium install 291 eligibility) â it does NOT change per load, so the "Add to Home Screen" 292 / install prompt on Edge/Chrome mobile keeps working. --> 293 <link href="/functions/manifest" id="kjb-manifest-link" rel="manifest"/> 294 <!-- Prevent flash of wrong theme: apply saved theme BEFORE React mounts --> 295
295<script> 296 (function() { 297 try { 298 // DIAGNOSTIC (temp, v20260904): dump origin + raw localStorage values 299 // at the earliest possible point (before any JS has a chance to touch 300 // them) so a cold offline launch tells us whether this is a wrong- 301 // origin/isolated-storage problem (all keys null, origin unexpected) 302 // or something narrower (only kjb-theme-mode missing). Remove once 303 // the offline-theme-reset bug is found. 304 console.log('[KJB-DEBUG] origin=' + location.origin + ' href=' + location.href); 305 console.log('[KJB-DEBUG] kjb-theme-mode=' + JSON.stringify(localStorage.getItem('kjb-theme-mode')) + 306 ' kjb-colour=' + JSON.stringify(localStorage.getItem('kjb-colour')) + 307 ' kjb-color-mode=' + JSON.stringify(localStorage.getItem('kjb-color-mode')) + 308 ' kjb-reader-font-family=' + JSON.stringify(localStorage.getItem('kjb-reader-font-family')) + 309 ' localStorage.length=' + localStorage.length); 310 } catch (e) { console.log('[KJB-DEBUG] error reading localStorage pre-mode: ' + e); } 311 try { 312 var mode = localStorage.getItem('kjb-theme-mode') || 'system'; 313 var isDark; 314 if (mode === 'dark') isDark = true; 315 else if (mode === 'light') isDark = false; 316 else if (mode === 'auto') { 317 var h = new Date().getHours(); 318 isDark = h < 6 || h >= 18; 319 } else { 320 isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 321 } 322 var rootEl = document.documentElement; 323 if (isDark) rootEl.classList.add('dark'); 324 // Paint the correct canvas colour from the very first frame. The 325 // <style> block below does this too, but an inline style + the 326 // matching color-scheme is applied instantly and also tells the 327 // browser what to use for the pre-stylesheet blank page, which is 328 // what caused the white-then-dark flash on launch. 329 rootEl.style.colorScheme = isDark ? 'dark' : 'light'; 330 331 // Apply the SAVED COLOUR PALETTE's surface tint before first paint.
332 // Without this, the first frame painted the default palette's canvas 333 // (and the fixed hex below), then visibly shifted once ThemeProvider 334 // applied the real palette after React mounted â the "portions still 335 // flash" problem. Values mirror applySurfaceTint() in 336 // src/lib/themeContext.jsx; keep the two in sync. 337 var KJB_HUES = { indigo: 245, sapphire: 222, sky: 200, teal: 174, forest: 142, amethyst: 271, rose: 347, crimson: 0, amber: 32, gold: 45, burgundy: 350, slate: 215, antique: 30 }; 338 var colourId = localStorage.getItem('kjb-colour') || 'sky'; 339 var colorMode = localStorage.getItem('kjb-color-mode') || 'fixed'; 340 var hue = KJB_HUES[colourId]; 341 if (colourId === 'custom') { 342 var hex = localStorage.getItem('kjb-custom-accent') || '#B8860B'; 343 var r = parseInt(hex.substring(1, 3), 16) / 255, 344 g = parseInt(hex.substring(3, 5), 16) / 255, 345 b = parseInt(hex.substring(5, 7), 16) / 255; 346 var mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn, hh = 0; 347 if (d !== 0) { 348 if (mx === r) hh = ((g - b) / d + (g < b ? 6 : 0)) / 6; 349 else if (mx === g) hh = ((b - r) / d + 2) / 6; 350 else hh = ((r - g) / d + 4) / 6; 351 } 352 hue = Math.round(hh * 360); 353 } 354 var bgCss; 355 // 'daily' colour mode derives its accent from the day's verse at 356 // runtime, so only the fixed-palette case can be resolved this early. 357 if (typeof hue === 'number' && colorMode === 'fixed') { 358 var s = rootEl.style; 359 if (isDark) { 360 s.setProperty('--background', hue + ' 30% 8%'); 361 s.setProperty('--foreground', hue + ' 25% 92%'); 362 s.setProperty('--card', hue + ' 28% 12%'); 363 s.setProperty('--card-foreground', hue + ' 25% 92%'); 364 s.setProperty('--popover', hue + ' 28% 12%'); 365 s.setProperty('--popover-foreground', hue + ' 25% 92%'); 366 s.setProperty('--secondary', hue + ' 22% 18%'); 367 s.setProperty('--secondary-foreground', hue + ' 25% 88%'); 368 s.setProperty('--muted', hue + ' 22% 18%'); 369 s.setProperty('--muted-foreground', hue + ' 15% 60%'); 370 s.setProperty('--border', hue + ' 20% 22%'); 371 s.setProperty('--input', hue + ' 20% 22%'); 372 s.setProperty('--sidebar-background', hue + ' 30% 9%'); 373 s.setProperty('--sidebar-foreground', hue + ' 25% 88%'); 374 s.setProperty('--sidebar-accent', hue + ' 22% 18%'); 375 s.setProperty('--sidebar-accent-foreground', hue + ' 25% 88%'); 376 s.setProperty('--sidebar-border', hue + ' 20% 22%'); 377 bgCss = 'hsl(' + hue + ' 30% 8%)'; 378 } else { 379 s.setProperty('--background', hue + ' 30% 99%'); 380 s.setProperty('--foreground', hue + ' 55% 8%'); 381 s.setProperty('--card', hue + ' 12% 95%'); 382 s.setProperty('--card-foreground', hue + ' 55% 8%'); 383 s.setProperty('--popover', '0 0% 100%'); 384 s.setProperty('--popover-foreground', hue + ' 55% 8%'); 385 s.setProperty('--secondary', hue + ' 14% 89%'); 386 s.setProperty('--secondary-foreground', hue + ' 30% 24%'); 387 s.setProperty('--muted', hue + ' 14% 91%'); 388 s.setProperty('--muted-foreground', hue + ' 12% 40%'); 389 s.setProperty('--border', hue + ' 30% 89%'); 390 s.setProperty('--input', hue + ' 30% 89%'); 391 s.setProperty('--sidebar-background', hue + ' 40% 98%'); 392 s.setProperty('--sidebar-foreground', hue + ' 35% 15%'); 393 s.setProperty('--sidebar-accent', hue + ' 40% 95%'); 394 s.setProperty('--sidebar-accent-foreground', hue + ' 45% 25%'); 395 s.setProperty('--sidebar-border', hue + ' 30% 89%'); 396 bgCss = 'hsl(' + hue + ' 30% 99%)'; 397 } 398 } else { 399 bgCss = isDark ? '#0f1117' : '#fef9f3'; 400 } 401 // The boot splash below paints this same colour so it matches too. 402 window.__kjbBootBg = bgCss; 403 rootEl.style.backgroundColor = bgCss; 404 } catch (e) {} 405 try { 406 // Apply the manual "App Zoom" setting BEFORE first paint, matching 407 // useBrowserZoom.js's manualFactor calc. Without this, the boot 408 // splash and first React paint render at 100% zoom, then visibly 409 // jump to the correct size once useBrowserZoom's effect runs. 410 var kjbZoomVal = parseInt(localStorage.getItem('kjb-layout-zoom') || '100', 10); 411 var kjbZoomFactor = (kjbZoomVal >= 50 && kjbZoomVal <= 200) ? kjbZoomVal / 100 : 1; 412 if (kjbZoomFactor !== 1) { 413 document.documentElement.style.setProperty('--kjb-zoom-scale', Str
413ing(kjbZoomFactor)); 414 // Also set the root font-size directly (not just the CSS variable) â 415 // the variable only takes effect once index.css's `:root { font-size: 416 // calc(...) }` rule loads, which can lag behind this inline script. 417 // Without this, the rem-sized boot splash below paints at the 418 // default 100% size first, then visibly jumps to the saved zoom 419 // once the stylesheet arrives. Setting it here makes the splash 420 // correctly sized from the very first frame, every reopen. 421 document.documentElement.style.fontSize = (kjbZoomFactor * 100) + '%'; 422 } 423 } catch (e) {} 424 try { 425 // A shared link can carry ?font=dyslexic or ?font=hyperlegible â 426 // persist it so the recipient opens (and stays) in that font mode. 427 var qsFont = window.location.search || ''; 428 if (qsFont.indexOf('font=dyslexic') !== -1) { 429 localStorage.setItem('kjb-a11y-font', 'dyslexic'); 430 } else if (qsFont.indexOf('font=hyperlegible') !== -1) { 431 localStorage.setItem('kjb-a11y-font', 'hyperlegible'); 432 } 433 var a11yFont = localStorage.getItem('kjb-a11y-font'); 434 if (a11yFont === 'dyslexic' || a11yFont === 'hyperlegible' || a11yFont === 'system') { 435 document.documentElement.setAttribute('data-a11y-font', a11yFont); 436 } 437 } catch (e) {} 438 })(); 439 </script>
439 440 <style> 441 /* Boot background matches the splash placeholder â light in light mode, 442 dark in dark mode â to prevent a flash of the wrong color before React 443 mounts. The inline theme script adds `html.dark` for dark mode. */ 444 html, body { background-color: #fef9f3; color: #0f1718; margin: 0; width: 100%; min-height: 100vh; } 445 html.dark, html.dark body { background-color: #0f1117; color: #ffffff; } 446 #root { min-height: 100vh; width: 100%; background-color: inherit; } 447 </style> 448
448<script crossorigin="" src="/assets/index-DwOsqt_4.js" type="module"> 449 </script>
449 450 <link crossorigin="" href="/assets/index-BqHR-vAv.css" rel="stylesheet"/> 451
451<script type="module"> 452 if (window.self === window.top) { 453 let lastPath = ""; 454 function getPageNameFromPath(path) { 455 const segments = path.split("/").filter(Boolean); 456 return segments[0] || null; 457 } 458 function trackPageView() { 459 const path = window.location.pathname; 460 if (path === lastPath) return; 461 lastPath = path; 462 const pageName = getPageNameFromPath(path) || "home"; 463 const appId = "6a8011c360ff52dad38eb2f3"; 464 if (!appId) return; 465 fetch(`/api/app-logs/${appId}/log-user-in-app/${pageName}`, { 466 method: "POST", 467 }).catch(() => {}); 468 } 469 const originalPushState = history.pushState.bind(history); 470 history.pushState = function (...args) { 471 originalPushState(...args); 472 trackPageView(); 473 }; 474 const originalReplaceState = history.replaceState.bind(history); 475 history.replaceState = function (...args) { 476 originalReplaceState(...args); 477 trackPageView(); 478 }; 479 window.addEventListener("popstate", trackPageView); 480 trackPageView(); 481} 482 </script>
482 483 <meta content="https://kingjamesbiblereader.com" property="og:url"/> 484 <meta content="website" property="og:type"/> 485 <meta content="KJB Reader" property="og:site_name"/> 486 <meta content="https://kingjamesbiblereader.com" name="twitter:url"/> 487
487<script> 488 (function () { 489 var titles = {"/login": "Login | KJB Reader", "/forgot-password": "Forgot Password | KJB Reader", "/reset-password": "Reset Password | KJB Reader", "/register": "Register | KJB Reader", "/oauth/consent": "O Auth Consent | KJB Reader", "/credits": "Credits Page | KJB Reader", "/changelog": "Changelog Page | KJB Reader", "/extension/change-log": "Changelog Page | KJB Reader", "/terms": "Terms Of Service Page | KJB Reader", "/privacy": "Privacy Policy Page | KJB Reader", "/contact": "Contact Page | KJB Reader", "/salvation": "Salvation Page | KJB Reader", "/discord": "Discord Invite Page | KJB Reader", "/extension": "Extension Page | KJB Reader", "/extension-privacy": "Extension Privacy Page | KJB Reader", "/extension/privacy": "Extension Privacy Page | KJB Reader", "/extension-terms": "Extension Terms Page | KJB Reader", "/extension-license": "Extension License Page | KJB Reader", "/landing": "Landing Page | KJB Reader", "/espanol-evangelio": "Spanish Gospel Page | KJB Reader", "/": "KJB Reader", "/read": "Bible Reader | KJB Reader", "/gospel": "Gospel Page | KJB Reader", "/resources": "Resources Page | KJB Reader", "/kjb-defence": "Kjb Defence Page | KJB Reader", "/about": "About Page | KJB Reader", "/contents": "Contents Page | KJB Reader", "/settings": "Settings Page | KJB Reader", "/search": "Search Page | KJB Reader", "/advanced-search": "Advanced Search Page | KJB Reader", "/saved": "Saved Verses Page | KJB Reader", "/refresh-cache": "Refresh Cache | KJB Reader", "/manifest-icons": "Manifest Icons | KJB Reader", "/manifest-screenshots": "Manifest Screenshots | KJB Reader", "/legacy": "Legacy Reader | KJB Reader", "/dev-tools": "Dev Tools Page | KJB Reader", "/espanol": "Spanish Resources Page | KJB Reader", "/bible.txt": "Bible Txt | KJB Reader", "/home-page": "KJB Reader", "/homepage": "KJB Reader"}; 490 // PascalCase boundaries -> "-", mirroring _PASCAL_BOUNDARY_RE without lookbehind 491 // (old Safari fails to PARSE a lookbehind, taking the whole script down). 492 var dashed = function (s) { 493 return s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2"); 494 }; 495 var decode = function (s) { 496 // A %2F inside ONE segment must not become a structural separator: /blog/foo%2Fbar is a 497 // dynamic slug, and letting it split would match a static /blog/foo/bar and write ITS title. 498 try { var d = decodeURIComponent(s); return d.indexOf("/") < 0 ? d : s; } catch (e) { return s; } 499 }; 500 var segments = function (path) { return path.split("/").filter(Boolean).map(decode); }; 501 var norm = function (path) { 502 var parts = segments(path).map(function (s) { return dashed(s).toLowerCase(); }); 503 return parts.length ? "/" + parts.join("/") : "/"; 504 }; 505 var lastPath = location.pathname, written = document.title, on = true; 506 var sync = function () { 507 // Path only: a hash/query change is the same page, and the SDK strips its own auth 508 // params with replaceState -- neither must clobber a server-rendered title in place. 509 if (!on || location.pathname === lastPath) return; 510 lastPath = location.pathname; 511 if (document.title !== written) { on = false; return; } 512 // Write ONLY a title the map proves. An unmapped path is a dynamic route (/blog/:slug) the 513 // map cannot carry, and writing a placeholder there is destructive, not merely vague: the 514 // prerender worker runs no setUserAgent, so its capture browser reads as human and executes 515 // this script, and coordinator.js's settle wait explicitly tolerates a full navigation -- 516 // a redirect landing on an unmapped path would bake the placeholder into the stored 517 // snapshot. Leaving the title alone degrades to today's behavior instead. 518 // Exact lookup only. A dash-stripped retry here would let a genuinely different /set-up 519 // claim /setup: the map carries a dash-free key only where a PascalCase boundary put the 520 // dashes there, and the client cannot tell the two apart. The map emits every reachable 521 // shape per segment instead (_client_url_variants). 522 var title = titles[norm(location.pathname)]; 523 if (title === undefined) return; 524 document.title = title; 525 // Re-read: the setter stores verbatim but the getter strips and collapses whitespace, so 526 // keeping the assigned string would make the next sync() read our own write as the app's. 527 written = document.title; 528 }; 529 var wrap = function (fn) { 530 // The history entry is already pushed by the time sync() runs, so a throw here would 531 // surface inside the host app's router and break navigation. This is injected into apps 532 // we cannot test, so it swallows rather than trusting the trace. 533 return function () { 534 var r = fn.apply(this, arguments);
535 try { sync(); } catch (e) {} 536 return r; 537 }; 538 }; 539 history.pushState = wrap(history.pushState); 540 history.replaceState = wrap(history.replaceState); 541 window.addEventListener("popstate", sync); 542})(); 543 </script>
543 544 <link href="https://kingjamesbiblereader.com" rel="canonical"/> 545
545<script data-seo-source="builder" type="application/ld+json"> 546 {"name": "KJB Reader", "@context": "https://schema.org", "@type": "WebSite", "url": "https://kingjamesbiblereader.com"} 547 </script>
547 548
548<script data-seo-source="builder" type="application/ld+json"> 549 {"name": "KJB Reader", "logo": "https://media.base44.com/images/public/6a8011c360ff52dad38eb2f3/03be03ba6_8e738d108_cfb4bf781_Untitled.png", "@context": "https://schema.org", "@type": "Organization", "url": "https://kingjamesbiblereader.com"} 550 </script>
550 551 </head> 552 <body> 553 <div id="root"> 554 <div data-seo-source="builder" id="seo-snapshot" style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;"> 555 <main> 556 <header> 557 <h1> 558 KJB Reader 559 </h1> 560 <p> 561 Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word of God., 562 </p> 563 </header> 564 <nav aria-label="Pages"> 565 <h2> 566 Pages 567 </h2> 568 <ul> 569 <li> 570 <a href="/credits"> 571 Credits Page 572 </a> 573 â Credits for the sources and functions that are used for this website to work. 574 </li> 575 <li> 576 <a href="/changelog"> 577 Changelog Page 578 </a> 579 â KJB Reader - SidePanel extension change-logs. 580 </li> 581 <li> 582 <a href="/terms"> 583 Terms Of Service Page 584 </a> 585 â Terms Of Service Page for KJB Reader Website. 586 </li> 587 <li> 588 <a href="/privacy"> 589 Privacy Policy Page 590 </a> 591 â Privacy Policy for KJB Reader Website. 592 </li> 593 <li> 594 <a href="/contact"> 595 Contact Page 596 </a> 597 â Contact me for feedback or issues on the website. 598 </li> 599 <li> 600 <a href="/salvation"> 601 Salvation Page 602 </a> 603 â Salvation Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 604 </li> 605 <li> 606 <a href="/discord"> 607 Discord Invite Page 608 </a> 609 â Discord links to download the KJB Reader Bot and more. 610 </li> 611 <li> 612 <a href="/extension"> 613 Extension Page 614 </a> 615 â Get the KJB Reader - SidePanel extension for Edge, Chrome, FireFox, Brave and Opera! 616 </li> 617 <li> 618 <a href="/extension-privacy"> 619 Extension Privacy Page 620 </a> 621 â Privacy Policy for KJB Reader - SidePanel Extension, 622 </li> 623 <li> 624 <a href="/extension-terms"> 625 Extension Terms Page 626 </a> 627 â Extension Terms Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 628 </li> 629 <li> 630 <a href="/extension-license"> 631 Extension License Page 632 </a> 633 â License policy for KJB Reader - SidePanel Extension. 634 </li> 635 <li> 636 <a href="/espanol-evangelio"> 637 Spanish Gospel Page 638 </a> 639 â Spanish Gospel Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 640 </li> 641 <li> 642 <a href="/read"> 643 Bible Reader 644 </a> 645 â Bible Reader on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 646 </li> 647 <li> 648 <a href="/gospel"> 649 Gospel Page 650 </a> 651 â Learn about how to be saved from the damnation to come. 652 </li> 653 <li> 654 <a href="/resources"> 655 Resources Page 656 </a> 657 â Resources Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 658 </li> 659 <li> 660 <a href="/kjb-defence"> 661 Kjb Defence Page 662 </a> 663 â KJB Defence Page with many resources. 664 </li> 665 <li> 666 <a href="/about"> 667 About Page 668 </a> 669 â About KJB Reader. 670 </li> 671 <li> 672 <a href="/contents"> 673 Contents Page 674 </a> 675 â Search through the Table of Contents for the King James Bible. 676 </li> 677 <li> 678 <a href="/settings"> 679 Settings Page 680 </a> 681 â Adjust your KJB Reader App settings. 682 </li> 683 <li> 684 <a href="/search"> 685 Search Page 686 </a> 687 â Search through the King James Bible. 688 </li> 689 <li> 690 <a href="/advanced-search"> 691 Advanced Search Page 692 </a> 693 â Search through the KJB with advanced filters. 694 </li> 695 <li> 696 <a href="/saved"> 697 Saved Verses Page 698 </a> 699 â Find the verses you saved in the app. 700 </li> 701 <li> 702 <a href="/legacy"> 703 Legacy Reader 704 </a> 705 â Read the KJB on Legacy browsers via an downloadable html file. 706 707Tested on Internet Explorer 11 on Windows 8.1. 708 </li> 709 <li> 710 <a href="/espanol"> 711 Spanish Resources Page 712 </a> 713 â Spanish Resources Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 714 </li> 715 </ul> 716 </nav> 717 </main> 718 </div> 719 </div> 720 <!-- Static placeholder that EXACTLY matches the React SplashScreen's idle 721 look (same logo size, progress bar, uppercase text) so there is no 722 visual flash/jump when React mounts and takes over. Colors are theme 723 aware: dark bg in dark mode, light bg in light mode. The inline theme 724 script above adds the `dark` class before this renders, so we read it 725 and set the splash colors here before paint. --> 726 <div id="kjb-boot-splash" style="position:fixed;inset:0;z-index:999999;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#0f1117;opacity:0;transition:opacity .2s ease;"> 727 <div style="display:flex;flex-direction:column;align-items:center;margin-top:-4rem;gap:48px;"> 728 <img alt="KJB Reader Logo" id="kjb-boot-logo" style="width:11rem;height:11rem;object-fit:contain;border-radius:1rem;padding:0.75rem;background:#0f1117;"/> 729 <div style="display:flex;flex-direction:column;align-items:center;gap:20px;width:16rem;"> 730 <div id="kjb-boot-track" style="width:100%;height:6px;border-radius:9999px;overflow:hidden;position:relative;background:rgba(255,255,255,0.08);"> 731 <div style="position:absolute;top:0;height:100%;border-radius:9999px;width:40%;background:#4f6aff;animation:kjb-boot-indeterminate 1.2s ease-in-out infinite;"> 732 </div>
733 </div> 734 <span id="kjb-boot-text" style="font-family:Inter,system-ui,sans-serif;font-size:0.875rem;font-weight:300;letter-spacing:0.25em;text-transform:uppercase;text-align:center;color:#c8cdd8;"> 735 Welcome to KJB Reader. 736 </span> 737 </div> 738 </div> 739 </div> 740
740<script> 741 (function () { 742 try { 743 // If this fresh page load is landing directly on /search -- 744 // i.e. a "Look Up" from another app, or a shared/bookmarked 745 // search link -- "Welcome to KJB Reader." doesn't fit; match 746 // the React SplashScreen's own isLookup logic (App.jsx / 747 // SplashScreen.jsx) so this static PRE-React placeholder shows 748 // the same "LOOKING UP..." wording instead. Without this, the 749 // static placeholder (visible for a brief moment before React 750 // mounts and takes over) showed the generic welcome text, then 751 // visibly swapped to "LOOKING UP..." once React's own splash 752 // rendered -- a small but noticeable mismatched flash. 753 var bootText = document.getElementById('kjb-boot-text'); 754 // DIAGNOSTIC: gated behind Settings > App Info > "Startup 755 // Diagnostics" (kjb-debug-diagnostics). Off by default -- a 756 // small tappable button instead of a full-width box, since the 757 // box was covering the reader/splash on small screens. Tapping 758 // copies the diagnostic text to the clipboard rather than 759 // displaying it all on screen. 760 try { 761 if (localStorage.getItem('kjb-debug-diagnostics') === 'true') { 762 var buildDbgText = function(tag) { 763 var carryParam = (location.search || '').indexOf('__kjb_carry=') !== -1; 764 return '[' + tag + '] origin=' + location.origin + 765 ' | hasCarryParam=' + carryParam + 766 ' | search=' + location.search + 767 ' | mode=' + JSON.stringify(localStorage.getItem('kjb-theme-mode')) + 768 ' | colour=' + JSON.stringify(localStorage.getItem('kjb-colour')) + 769 ' | color-mode=' + JSON.stringify(localStorage.getItem('kjb-color-mode')) + 770 ' | font=' + JSON.stringify(localStorage.getItem('kjb-reader-font-family')) + 771 ' | hasVisited=' + JSON.stringify(localStorage.getItem('kjb-has-visited-app')) + 772 ' | mirror=' + JSON.stringify(localStorage.getItem('kjb-mirror-status')) + 773 ' | ls.length=' + localStorage.length + 774 ' | online=' + navigator.onLine; 775 }; 776 var dbgLines = [buildDbgText('pre-main')]; 777 // Pre-main.jsx is EXPECTED to show ls.length=0 even when the 778 // carry param is present -- main.jsx hasn't decoded/applied 779 // it yet at this point in the file. The post-mount line 780 // 800ms later is the one that actually proves whether the 781 // carry applied in time. 782 setTimeout(function () { dbgLines.push(buildDbgText('post-main+800ms')); }, 800); 783 784 var fallbackCopy = function (text) { 785 try { 786 var ta = document.createElement('textarea'); 787 ta.value = text; 788 ta.style.position = 'fixed'; 789 ta.style.opacity = '0'; 790 document.body.appendChild(ta); 791 ta.focus(); 792 ta.select(); 793 document.execCommand('copy'); 794 document.body.removeChild(ta); 795 } catch (e) {} 796 }; 797 798 var dbgBtn = document.createElement('button'); 799 dbgBtn.id = 'kjb-debug-btn'; 800 dbgBtn.textContent = 'DBG'; 801 dbgBtn.style.cssText = 'position:fixed;right:10px;bottom:10px;z-index:9999999;background:rgba(0,0,0,0.75);color:#0f0;font-family:monospace;font-size:11px;font-weight:bold;padding:8px 12px;border-radius:20px;border:1px solid #0f0;opacity:0.85;'; 802 dbgBtn.addEventListener('click', function () { 803 var text = dbgLines.join('\n'); 804 var showCopied = function () { 805 dbgBtn.textContent = 'Copied!'; 806 setTimeout(function () { dbgBtn.textContent = 'DBG'; }, 1200); 807 }; 808 if (navigator.clipboard && navigator.clipboard.writeText) { 809 navigator.clipboard.writeText(text).then(showCopied).catch(function () { 810 fallbackCopy(text); 811 showCopied(); 812 }); 813 } else { 814 fallbackCopy(text); 815 showCopied(); 816 } 817 }); 818 document.body.appendChild(dbgBtn); 819 } 820 } catch (e) {} 821 if (bootText) { 822 var hasVisited = localStorage.getItem('kjb-has-visited-app') === 'true'; 823 // Must match SplashScreen.jsx's lookupSplashLabel() exactly (the 824 // CSS uppercases this) so a lookup shows ONE label, not 825 // "Looking up..." swapping to "Looking up "Romans 3:25"...". 826 var lookupQ = '';
827 try { lookupQ = (new URLSearchParams(location.search).get('q') || '').trim(); } catch (e) {} 828 bootText.textContent = (location.pathname === '/search') 829 ? (lookupQ ? 'Looking up \u201C' + lookupQ + '\u201D\u2026' : 'Looking up\u2026') 830 : (hasVisited ? 'Welcome back to KJB Reader.' : 'Welcome to KJB Reader.'); 831 } 832 // Use the base64-cached logo from localStorage if available (works 833 // offline without SW). Falls back to the cross-origin URL online. 834 var logoEl = document.getElementById('kjb-boot-logo'); 835 var cachedLogo = localStorage.getItem('kjb-splash-logo-dataurl'); 836 logoEl.src = cachedLogo || 837 // On the native offline-fallback copy (capacitor: origin) the logo 838 // comes from the app bundle, so it shows with no connection. 839 (window.location && window.location.protocol === 'capacitor:' 840 ? '/__native/logo.png' 841 : 'https://media.base44.com/images/public/6a05d76723afe58d80c589e8/2279e016e_8e738d108_cfb4bf781_Untitled.png'); 842 // Paint the splash in the theme's own resolved canvas colour 843 // (set by the theme script above), so there's no colour shift 844 // between the splash and the app underneath it. 845 var bg = window.__kjbBootBg || '#0f1117'; 846 document.getElementById('kjb-boot-splash').style.background = bg; 847 logoEl.style.background = bg; 848 if (!document.documentElement.classList.contains('dark')) { 849 document.getElementById('kjb-boot-track').style.background = 'rgba(0,0,0,0.08)'; 850 document.getElementById('kjb-boot-text').style.color = '#5a6472'; 851 } 852 // Installed desktop PWAs open at a temporary default window size 853 // that Chrome/Edge resizes to the remembered size almost immediately 854 // after launch. If the splash paints before that resize settles, the 855 // logo/progress bar appear to "shrink" as the window snaps to size â 856 // that's the browser's window, not this content, changing. Hide the 857 // splash for a beat in standalone mode so it only becomes visible 858 // once the window has settled, and reveal instantly everywhere else 859 // (plain browser tabs aren't affected and shouldn't see extra delay). 860 // 861 // Native Android (Capacitor) is excluded from this delay entirely: 862 // it ALSO matches display-mode: standalone, so it was getting this 863 // same 150ms treatment despite the window-resize issue it exists 864 // for being purely a desktop-browser-chrome concern -- a native 865 // Activity is already a fixed, full-screen surface with nothing to 866 // "settle". During that unnecessary delay, this placeholder stayed 867 // invisible while React was often already bootstrapping underneath 868 // (e.g. a "Look Up" launch straight to /search), so whatever React 869 // had rendered so far -- typically the lazy-route Suspense 870 // fallback's spinner -- was briefly visible unmasked, before this 871 // placeholder (and shortly after, the real SplashScreen) finally 872 // appeared. window.androidBridge is Capacitor's own native JS 873 // interface, injected directly by MainActivity.java and 874 // synchronously available the instant any page JS runs in that 875 // WebView -- reliable regardless of online/offline state, unlike 876 // Capacitor's own bridge script injection (see MainActivity.java's 877 // injectNativeMarker()). 878 var boot = document.getElementById('kjb-boot-splash'); 879 var isNativeAndroid = !!window.androidBridge; 880 var isStandalone = !isNativeAndroid && ((window.matchMedia && ( 881 window.matchMedia('(display-mode: standalone)').matches || 882 window.matchMedia('(display-mode: window-controls-overlay)').matches || 883 window.matchMedia('(display-mode: minimal-ui)').matches 884 )) || navigator.standalone === true); 885 if (isStandalone) { 886 setTimeout(function () { boot.style.opacity = '1'; }, 150); 887 } else { 888 boot.style.opacity = '1'; 889 } 890 } catch (e) { 891 var fallback = document.getElementById('kjb-boot-splash'); 892 if (fallback) fallback.style.opacity = '1'; 893 } 894 })(); 895 </script>
895 896 <style> 897 @keyframes kjb-boot-indeterminate { 898 0% { left: -40%; } 899 100% { left: 100%; } 900 } 901 </style> 902 </body> 903</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.