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 Contact Page | 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="Contact me for feedback or issues on the website." 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="Contact Page | KJB Reader" property="og:title"/> 279 <meta content="Contact me for feedback or issues on the website." 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="Contact Page | KJB Reader" name="twitter:title"/> 285 <meta content="Contact me for feedback or issues on the website." 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/contact" 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/contact" 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", "/homepage": "KJB Reader", "/home-page": "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/contact" 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
551<script data-seo-source="builder" type="application/ld+json"> 552 {"@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [{"@type": "ListItem", "position": 1, "name": "Home", "item": "https://kingjamesbiblereader.com/"}, {"@type": "ListItem", "position": 2, "name": "Contact Page | KJB Reader", "item": "https://kingjamesbiblereader.com/contact"}]} 553 </script>
553 554 </head> 555 <body> 556 <div id="root"> 557 <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;"> 558 <main> 559 <header> 560 <h1> 561 Contact Page | KJB Reader 562 </h1> 563 <p> 564 Contact me for feedback or issues on the website. 565 </p> 566 </header> 567 <nav aria-label="Pages"> 568 <h2> 569 Pages 570 </h2> 571 <ul> 572 <li> 573 <a href="/"> 574 Home Page 575 </a> 576 </li> 577 <li> 578 <a href="/credits"> 579 Credits Page 580 </a> 581 â Credits for the sources and functions that are used for this website to work. 582 </li> 583 <li> 584 <a href="/changelog"> 585 Changelog Page 586 </a> 587 â KJB Reader - SidePanel extension change-logs. 588 </li> 589 <li> 590 <a href="/terms"> 591 Terms Of Service Page 592 </a> 593 â Terms Of Service Page for KJB Reader Website. 594 </li> 595 <li> 596 <a href="/privacy"> 597 Privacy Policy Page 598 </a> 599 â Privacy Policy for KJB Reader Website. 600 </li> 601 <li> 602 <a href="/contact"> 603 Contact Page 604 </a> 605 â Contact me for feedback or issues on the website. 606 </li> 607 <li> 608 <a href="/salvation"> 609 Salvation Page 610 </a> 611 â Salvation Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 612 </li> 613 <li> 614 <a href="/discord"> 615 Discord Invite Page 616 </a> 617 â Discord links to download the KJB Reader Bot and more. 618 </li> 619 <li> 620 <a href="/extension"> 621 Extension Page 622 </a> 623 â Get the KJB Reader - SidePanel extension for Edge, Chrome, FireFox, Brave and Opera! 624 </li> 625 <li> 626 <a href="/extension-privacy"> 627 Extension Privacy Page 628 </a> 629 â Privacy Policy for KJB Reader - SidePanel Extension, 630 </li> 631 <li> 632 <a href="/extension-terms"> 633 Extension Terms Page 634 </a> 635 â Extension Terms Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 636 </li> 637 <li> 638 <a href="/extension-license"> 639 Extension License Page 640 </a> 641 â License policy for KJB Reader - SidePanel Extension. 642 </li> 643 <li> 644 <a href="/espanol-evangelio"> 645 Spanish Gospel Page 646 </a> 647 â Spanish Gospel Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 648 </li> 649 <li> 650 <a href="/read"> 651 Bible Reader 652 </a> 653 â Bible Reader on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 654 </li> 655 <li> 656 <a href="/gospel"> 657 Gospel Page 658 </a> 659 â Learn about how to be saved from the damnation to come. 660 </li> 661 <li> 662 <a href="/resources"> 663 Resources Page 664 </a> 665 â Resources Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 666 </li> 667 <li> 668 <a href="/kjb-defence"> 669 Kjb Defence Page 670 </a> 671 â KJB Defence Page with many resources. 672 </li> 673 <li> 674 <a href="/about"> 675 About Page 676 </a> 677 â About KJB Reader. 678 </li> 679 <li> 680 <a href="/contents"> 681 Contents Page 682 </a> 683 â Search through the Table of Contents for the King James Bible. 684 </li> 685 <li> 686 <a href="/settings"> 687 Settings Page 688 </a> 689 â Adjust your KJB Reader App settings. 690 </li> 691 <li> 692 <a href="/search"> 693 Search Page 694 </a> 695 â Search through the King James Bible. 696 </li> 697 <li> 698 <a href="/advanced-search"> 699 Advanced Search Page 700 </a> 701 â Search through the KJB with advanced filters. 702 </li> 703 <li> 704 <a href="/saved"> 705 Saved Verses Page 706 </a> 707 â Find the verses you saved in the app. 708 </li> 709 <li> 710 <a href="/legacy"> 711 Legacy Reader 712 </a> 713 â Read the KJB on Legacy browsers via an downloadable html file. 714 715Tested on Internet Explorer 11 on Windows 8.1. 716 </li> 717 <li> 718 <a href="/espanol"> 719 Spanish Resources Page 720 </a> 721 â Spanish Resources Page on KJB Reader. Read the King James Bible (Pure Cambridge Edition) â the pure, infallible Word. 722 </li> 723 </ul> 724 </nav> 725 </main> 726 </div> 727 </div> 728 <!-- Static placeholder that EXACTLY matches the React SplashScreen's idle 729 look (same logo size, progress bar, uppercase text) so there is no 730 visual flash/jump when React mounts and takes over. Colors are theme 731 aware: dark bg in dark mode, light bg in light mode. The inline theme 732 script above adds the `dark` class before this renders, so we read it 733 and set the splash colors here before paint. --> 734 <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;"> 735 <div style="display:flex;flex-direction:column;align-items:center;margin-top:-4rem;gap:48px;"> 736 <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;"/> 737 <div style="display:flex;flex-direction:column;align-items:center;gap:20px;width:16rem;"> 738 <div id="kjb-boot-track" style="width:100%;height:6px;border-radius:9999px;overflow:hidden;position:relative;background:rgba(255,255,255,0.08);"> 739 <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;"> 740 </div>
741 </div> 742 <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;"> 743 Welcome to KJB Reader. 744 </span> 745 </div> 746 </div> 747 </div> 748
748<script> 749 (function () { 750 try { 751 // If this fresh page load is landing directly on /search -- 752 // i.e. a "Look Up" from another app, or a shared/bookmarked 753 // search link -- "Welcome to KJB Reader." doesn't fit; match 754 // the React SplashScreen's own isLookup logic (App.jsx / 755 // SplashScreen.jsx) so this static PRE-React placeholder shows 756 // the same "LOOKING UP..." wording instead. Without this, the 757 // static placeholder (visible for a brief moment before React 758 // mounts and takes over) showed the generic welcome text, then 759 // visibly swapped to "LOOKING UP..." once React's own splash 760 // rendered -- a small but noticeable mismatched flash. 761 var bootText = document.getElementById('kjb-boot-text'); 762 // DIAGNOSTIC: gated behind Settings > App Info > "Startup 763 // Diagnostics" (kjb-debug-diagnostics). Off by default -- a 764 // small tappable button instead of a full-width box, since the 765 // box was covering the reader/splash on small screens. Tapping 766 // copies the diagnostic text to the clipboard rather than 767 // displaying it all on screen. 768 try { 769 if (localStorage.getItem('kjb-debug-diagnostics') === 'true') { 770 var buildDbgText = function(tag) { 771 var carryParam = (location.search || '').indexOf('__kjb_carry=') !== -1; 772 return '[' + tag + '] origin=' + location.origin + 773 ' | hasCarryParam=' + carryParam + 774 ' | search=' + location.search + 775 ' | mode=' + JSON.stringify(localStorage.getItem('kjb-theme-mode')) + 776 ' | colour=' + JSON.stringify(localStorage.getItem('kjb-colour')) + 777 ' | color-mode=' + JSON.stringify(localStorage.getItem('kjb-color-mode')) + 778 ' | font=' + JSON.stringify(localStorage.getItem('kjb-reader-font-family')) + 779 ' | hasVisited=' + JSON.stringify(localStorage.getItem('kjb-has-visited-app')) + 780 ' | mirror=' + JSON.stringify(localStorage.getItem('kjb-mirror-status')) + 781 ' | ls.length=' + localStorage.length + 782 ' | online=' + navigator.onLine; 783 }; 784 var dbgLines = [buildDbgText('pre-main')]; 785 // Pre-main.jsx is EXPECTED to show ls.length=0 even when the 786 // carry param is present -- main.jsx hasn't decoded/applied 787 // it yet at this point in the file. The post-mount line 788 // 800ms later is the one that actually proves whether the 789 // carry applied in time. 790 setTimeout(function () { dbgLines.push(buildDbgText('post-main+800ms')); }, 800); 791 792 var fallbackCopy = function (text) { 793 try { 794 var ta = document.createElement('textarea'); 795 ta.value = text; 796 ta.style.position = 'fixed'; 797 ta.style.opacity = '0'; 798 document.body.appendChild(ta); 799 ta.focus(); 800 ta.select(); 801 document.execCommand('copy'); 802 document.body.removeChild(ta); 803 } catch (e) {} 804 }; 805 806 var dbgBtn = document.createElement('button'); 807 dbgBtn.id = 'kjb-debug-btn'; 808 dbgBtn.textContent = 'DBG'; 809 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;'; 810 dbgBtn.addEventListener('click', function () { 811 var text = dbgLines.join('\n'); 812 var showCopied = function () { 813 dbgBtn.textContent = 'Copied!'; 814 setTimeout(function () { dbgBtn.textContent = 'DBG'; }, 1200); 815 }; 816 if (navigator.clipboard && navigator.clipboard.writeText) { 817 navigator.clipboard.writeText(text).then(showCopied).catch(function () { 818 fallbackCopy(text); 819 showCopied(); 820 }); 821 } else { 822 fallbackCopy(text); 823 showCopied(); 824 } 825 }); 826 document.body.appendChild(dbgBtn); 827 } 828 } catch (e) {} 829 if (bootText) { 830 var hasVisited = localStorage.getItem('kjb-has-visited-app') === 'true'; 831 // Must match SplashScreen.jsx's lookupSplashLabel() exactly (the 832 // CSS uppercases this) so a lookup shows ONE label, not 833 // "Looking up..." swapping to "Looking up "Romans 3:25"...". 834 var lookupQ = '';
835 try { lookupQ = (new URLSearchParams(location.search).get('q') || '').trim(); } catch (e) {} 836 bootText.textContent = (location.pathname === '/search') 837 ? (lookupQ ? 'Looking up \u201C' + lookupQ + '\u201D\u2026' : 'Looking up\u2026') 838 : (hasVisited ? 'Welcome back to KJB Reader.' : 'Welcome to KJB Reader.'); 839 } 840 // Use the base64-cached logo from localStorage if available (works 841 // offline without SW). Falls back to the cross-origin URL online. 842 var logoEl = document.getElementById('kjb-boot-logo'); 843 var cachedLogo = localStorage.getItem('kjb-splash-logo-dataurl'); 844 logoEl.src = cachedLogo || 845 // On the native offline-fallback copy (capacitor: origin) the logo 846 // comes from the app bundle, so it shows with no connection. 847 (window.location && window.location.protocol === 'capacitor:' 848 ? '/__native/logo.png' 849 : 'https://media.base44.com/images/public/6a05d76723afe58d80c589e8/2279e016e_8e738d108_cfb4bf781_Untitled.png'); 850 // Paint the splash in the theme's own resolved canvas colour 851 // (set by the theme script above), so there's no colour shift 852 // between the splash and the app underneath it. 853 var bg = window.__kjbBootBg || '#0f1117'; 854 document.getElementById('kjb-boot-splash').style.background = bg; 855 logoEl.style.background = bg; 856 if (!document.documentElement.classList.contains('dark')) { 857 document.getElementById('kjb-boot-track').style.background = 'rgba(0,0,0,0.08)'; 858 document.getElementById('kjb-boot-text').style.color = '#5a6472'; 859 } 860 // Installed desktop PWAs open at a temporary default window size 861 // that Chrome/Edge resizes to the remembered size almost immediately 862 // after launch. If the splash paints before that resize settles, the 863 // logo/progress bar appear to "shrink" as the window snaps to size â 864 // that's the browser's window, not this content, changing. Hide the 865 // splash for a beat in standalone mode so it only becomes visible 866 // once the window has settled, and reveal instantly everywhere else 867 // (plain browser tabs aren't affected and shouldn't see extra delay). 868 // 869 // Native Android (Capacitor) is excluded from this delay entirely: 870 // it ALSO matches display-mode: standalone, so it was getting this 871 // same 150ms treatment despite the window-resize issue it exists 872 // for being purely a desktop-browser-chrome concern -- a native 873 // Activity is already a fixed, full-screen surface with nothing to 874 // "settle". During that unnecessary delay, this placeholder stayed 875 // invisible while React was often already bootstrapping underneath 876 // (e.g. a "Look Up" launch straight to /search), so whatever React 877 // had rendered so far -- typically the lazy-route Suspense 878 // fallback's spinner -- was briefly visible unmasked, before this 879 // placeholder (and shortly after, the real SplashScreen) finally 880 // appeared. window.androidBridge is Capacitor's own native JS 881 // interface, injected directly by MainActivity.java and 882 // synchronously available the instant any page JS runs in that 883 // WebView -- reliable regardless of online/offline state, unlike 884 // Capacitor's own bridge script injection (see MainActivity.java's 885 // injectNativeMarker()). 886 var boot = document.getElementById('kjb-boot-splash'); 887 var isNativeAndroid = !!window.androidBridge; 888 var isStandalone = !isNativeAndroid && ((window.matchMedia && ( 889 window.matchMedia('(display-mode: standalone)').matches || 890 window.matchMedia('(display-mode: window-controls-overlay)').matches || 891 window.matchMedia('(display-mode: minimal-ui)').matches 892 )) || navigator.standalone === true); 893 if (isStandalone) { 894 setTimeout(function () { boot.style.opacity = '1'; }, 150); 895 } else { 896 boot.style.opacity = '1'; 897 } 898 } catch (e) { 899 var fallback = document.getElementById('kjb-boot-splash'); 900 if (fallback) fallback.style.opacity = '1'; 901 } 902 })(); 903 </script>
903 904 <style> 905 @keyframes kjb-boot-indeterminate { 906 0% { left: -40%; } 907 100% { left: 100%; } 908 } 909 </style> 910 </body> 911</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.