For youth pastors & ministers

Paste your notes.
Walk away with a presentation.

Upload your sermon notes, outline, or manuscript in any format. Get back a full HTML slide deck with audience/presenter sync, complete speaker notes, a sermon outline, and a bibliography — ready to run.

Generate AI image prompts
Adds a prompts file optimized for Nano Banana (Gemini 2.5 Flash Image)
🔒 Enter access passcode (if required)
1
Parsing content
2
Extracting structure and notes
3
Building presentation
4
Generating artifacts
Your presentation is ready
How to present
  • Open presentation.html in any browser — it works fully offline with no dependencies
  • For Zoom: open presentation.html?mode=audience and share that window
  • Keep presentation.html?mode=presenter on your screen — it syncs automatically when you advance slides
  • Navigate with arrow keys, spacebar, or click the dots. Press F for fullscreen.
${fontLinks}
${slidesHTML}
${OPEN_LINKS_HTML} ${PRESENTER_HTML} ${NAV_HTML} ${THEME_PICKER_HTML} `; } // ── Markdown artifact generators ── function generateNotesMarkdown(s) { return ['# ' + s.title + ' \u2014 Speaker Notes\n', '> ' + (s.primaryScripture || '') + '\n'] .concat(s.slides.map((slide, i) => '## Slide ' + (i + 1) + ': ' + (slide.folio || slide.title || '') + '\n\n' + (slide.notes || '').replace(/<[^>]+>/g, '').replace(/•/g, '\u2022').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '\n\n---\n' )).join('\n'); } function generateOutlineMarkdown(s) { return ['# ' + s.title + '\n', '**Scripture:** ' + (s.primaryScripture || '') + '\n', s.subtitle ? '*' + s.subtitle + '*\n' : '', '\n## Outline\n'] .concat((s.slides || []).map((slide, i) => { const folio = slide.folio || ''; const summary = slide.summary || ''; return (i + 1) + '. **' + folio + '**' + (summary ? ' — ' + summary : ''); })).join('\n'); } function generateBibMarkdown(s) { return '# ' + s.title + ' \u2014 Sources\n\n' + (s.sources || []).map(src => '- ' + src).join('\n'); } function generateImageMarkdown(s) { return ['# ' + s.title + ' \u2014 Image Prompts (Nano Banana / Gemini)\n', 'Run each prompt through Nano Banana at gemini.google.com or via the Gemini API (gemini-2.5-flash image model).\n'] .concat((s.imagePrompts || []).map((p, i) => '## Image ' + (i + 1) + '\n```\n' + p + '\n```\n')).join('\n'); } // ── Show results ── const ARTIFACT_META = { presentation: { icon: '\u25B6', name: 'Presentation HTML', desc: 'Open in browser \u00B7 audience + presenter sync built in' }, notes: { icon: '\uD83D\uDCDD', name: 'Speaker Notes', desc: 'Complete notes, clean text, no AI commentary' }, outline: { icon: '\u2261', name: 'Sermon Outline', desc: 'Structured outline for bulletin or planning' }, bibliography: { icon: '\u275D', name: 'Bibliography', desc: 'All sources and citations' }, images: { icon: '\u25A3', name: 'Image Prompts', desc: 'Ready for Nano Banana or any image gen tool' } }; function showResults(structure) { const wrap = document.getElementById('resultsWrap'); wrap.classList.add('show'); document.getElementById('resultsSub').textContent = structure.slides.length + ' slides generated for \u201C' + structure.title + '\u201D \u2014 ' + (structure.primaryScripture || ''); // Publish panel — shareable URL + cross-device sync renderPublishPanel(structure); const grid = document.getElementById('artifactsGrid'); while (grid.firstChild) grid.removeChild(grid.firstChild); Object.entries(generatedArtifacts).forEach(([key, art]) => { const meta = ARTIFACT_META[key] || { icon: '\u2193', name: key, desc: '' }; const card = document.createElement('div'); card.className = 'artifact-card'; const icon = document.createElement('div'); icon.className = 'artifact-icon'; icon.textContent = meta.icon; const name = document.createElement('div'); name.className = 'artifact-name'; name.textContent = meta.name; const desc = document.createElement('div'); desc.className = 'artifact-desc'; desc.textContent = meta.desc; card.appendChild(icon); card.appendChild(name); card.appendChild(desc); card.onclick = () => { const blob = new Blob([art.content], { type: art.mime }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = art.filename; a.click(); URL.revokeObjectURL(url); }; grid.appendChild(card); }); wrap.scrollIntoView({ behavior: 'smooth', block: 'start' }); } let publishedId = null; function renderPublishPanel(structure) { let panel = document.getElementById('publishPanel'); if (!panel) { panel = document.createElement('div'); panel.id = 'publishPanel'; panel.className = 'publish-panel'; const grid = document.getElementById('artifactsGrid'); grid.parentNode.insertBefore(panel, grid); } while (panel.firstChild) panel.removeChild(panel.firstChild); const title = document.createElement('div'); title.className = 'publish-title'; title.textContent = '\u2301 Share online \u2014 presenter mode on phone controls the laptop'; panel.appendChild(title); const desc = document.createElement('div'); desc.className = 'publish-desc'; desc.textContent = 'Publish to a shareable URL. Open on your laptop in audience mode, on your phone in presenter mode — slide changes sync in real time across devices.'; panel.appendChild(desc); const row = document.createElement('div'); row.className = 'publish-row'; const publicLabel = document.createElement('label'); publicLabel.className = 'publish-check'; const publicCheck = document.createElement('input'); publicCheck.type = 'checkbox'; publicCheck.id = 'publishPublic'; publicLabel.appendChild(publicCheck); const publicText = document.createElement('span'); publicText.textContent = 'Share publicly in the showcase'; publicLabel.appendChild(publicText); row.appendChild(publicLabel); const btn = document.createElement('button'); btn.className = 'publish-btn'; btn.textContent = 'Publish to shareable URL \u2192'; btn.onclick = () => doPublish(structure, publicCheck.checked, btn, panel); row.appendChild(btn); panel.appendChild(row); } async function doPublish(structure, isPublic, btn, panel) { btn.disabled = true; btn.textContent = 'Publishing\u2026'; try { const passcode = document.getElementById('passcode').value.trim(); const headers = { 'Content-Type': 'application/json' }; if (passcode) headers['x-access-token'] = passcode; const resp = await fetch('/api/publish', { method: 'POST', headers, body: JSON.stringify({ html: generatedArtifacts.presentation.content, title: structure.title, author: structure.author || '', public: isPublic, }), }); const data = await resp.json(); if (!resp.ok) throw new Error(data.error || 'Publish failed (' + resp.status + ')'); publishedId = data.id; // Render the live shareable URL area while (panel.firstChild) panel.removeChild(panel.firstChild); const t = document.createElement('div'); t.className = 'publish-title'; t.textContent = '\u2713 Published'; panel.appendChild(t); const urlRow = document.createElement('div'); urlRow.className = 'publish-url-row'; const link = document.createElement('a'); link.href = data.url; link.target = '_blank'; link.rel = 'noopener'; link.textContent = data.url; link.className = 'publish-url'; urlRow.appendChild(link); const copy = document.createElement('button'); copy.className = 'publish-copy'; copy.textContent = 'Copy'; copy.onclick = async () => { try { await navigator.clipboard.writeText(data.url); copy.textContent = 'Copied \u2713'; setTimeout(() => copy.textContent = 'Copy', 1500); } catch {} }; urlRow.appendChild(copy); panel.appendChild(urlRow); const links = document.createElement('div'); links.className = 'publish-links'; const mk = (label, q) => { const a = document.createElement('a'); a.href = data.url + (q ? '?mode=' + q : ''); a.target = '_blank'; a.rel = 'noopener'; a.className = 'publish-modelink'; a.textContent = label; return a; }; links.appendChild(mk('Open', '')); links.appendChild(mk('Audience \u2197', 'audience')); links.appendChild(mk('Presenter \u2197', 'presenter')); panel.appendChild(links); const note = document.createElement('div'); note.className = 'publish-desc'; note.textContent = 'Tip: open audience mode on the laptop driving the projector, presenter mode on your phone. Slide changes sync live across both.'; panel.appendChild(note); } catch (e) { btn.disabled = false; btn.textContent = 'Publish to shareable URL \u2192'; showError('Publish failed: ' + e.message); } } function resetForm() { document.getElementById('resultsWrap').classList.remove('show'); document.getElementById('progressWrap').classList.remove('show'); ['st0','st1','st2','st3'].forEach((_, i) => setStep(i, 'idle')); window.scrollTo({ top: 0, behavior: 'smooth' }); } // ──────────────────────────────────────────────────────────── // PRESENTATION CSS (embedded in generated file) // ──────────────────────────────────────────────────────────── const PRESENTATION_CSS = ` html,body{width:100%;height:100%;overflow:hidden;background:var(--bg);color:var(--white);font-family:var(--sans);-webkit-font-smoothing:antialiased} #deck{width:100vw;height:100vh;position:relative} .slide{position:absolute;inset:0;opacity:0;pointer-events:none;overflow:hidden;padding-bottom:3.6rem;will-change:opacity,transform} .slide.active{opacity:1;pointer-events:auto} .slide-dark{background:var(--bg);color:var(--white)} .slide-paper{background:var(--paper);color:var(--ink)} .slide-exiting{animation:sOut .2s cubic-bezier(.4,0,1,1) forwards;pointer-events:none;z-index:2} .slide-entering{animation:sIn .3s cubic-bezier(0,0,.2,1) forwards;z-index:1} @keyframes sOut{to{opacity:0;transform:scale(.96) translateY(-6px);filter:blur(1px)}} @keyframes sIn{from{opacity:0;transform:scale(1.02);filter:blur(2px)}to{opacity:1;transform:none;filter:blur(0)}} .w{display:inline-block;opacity:0;transform:translateY(28px);filter:blur(3px)} .slide.active .w{animation:wIn .52s cubic-bezier(.16,1,.3,1) forwards} @keyframes wIn{60%{filter:blur(0)}100%{opacity:1;transform:none;filter:blur(0)}} .be{opacity:0;transform:translateY(16px);filter:blur(2px)} .slide.active .be{animation:bIn .55s cubic-bezier(.16,1,.3,1) forwards} @keyframes bIn{55%{filter:blur(0)}100%{opacity:1;transform:none;filter:blur(0)}} .pi{opacity:0;transform:scale(.85);filter:blur(3px)} .slide.active .pi{animation:piIn .5s cubic-bezier(.34,1.4,.64,1) forwards} @keyframes piIn{60%{filter:blur(0)}100%{opacity:1;transform:scale(1);filter:blur(0)}} .tw-cursor{display:inline-block;width:.06em;height:1em;background:currentColor;margin-left:.04em;vertical-align:text-bottom;animation:cb .65s step-end infinite} @keyframes cb{0%,100%{opacity:1}50%{opacity:0}} .tw-cursor.tw-done{animation:none;opacity:0;transition:opacity .3s} .folio{position:absolute;bottom:0;left:0;right:0;height:3.6rem;display:flex;align-items:center;padding:0 3.5vw;border-top:1px solid rgba(255,255,255,.06)} .slide-paper .folio{border-top:1px solid rgba(0,0,0,.1)} .folio-title{font-family:var(--sans);font-size:.75rem;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--ghost)} .slide-paper .folio-title{color:rgba(0,0,0,.3)} .ey{font-family:var(--sans);font-size:.8rem;font-weight:700;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);margin-bottom:.9rem} .hed{font-family:var(--serif);font-weight:700;line-height:1.05;color:var(--white)} .hed-lg{font-size:clamp(2rem,4.5vw,4rem)} .hed-md{font-size:clamp(1.6rem,3.5vw,3rem)} .sub{font-family:var(--serif);font-style:italic;color:rgba(255,255,255,.55);line-height:1.3} .body{font-family:var(--sans);font-size:clamp(.9rem,1.3vw,1rem);line-height:1.65;color:var(--ghost);margin-bottom:.9rem} .body strong{color:var(--white);font-weight:600} .body em{color:var(--gold);font-style:normal} .slide-paper .hed{color:var(--ink)}.slide-paper .ey{color:var(--accent)}.slide-paper .sub{color:rgba(0,0,0,.55)}.slide-paper .body{color:rgba(0,0,0,.6)}.slide-paper .body strong{color:var(--ink)} .s-cover{height:100%;display:flex;flex-direction:column;justify-content:flex-end;padding:0 3.5vw 5.5rem} .cover-title{font-family:var(--serif);font-weight:900;line-height:.9;letter-spacing:-.02em;font-size:clamp(5rem,13vw,12rem);color:var(--white)} .cover-sub{font-family:var(--sans);font-size:clamp(1rem,2vw,1.6rem);color:var(--ghost);margin-top:1rem;font-style:italic} .byline{display:flex;align-items:center;gap:1.2rem;margin-top:2rem} .byline-rule{width:2rem;height:2px;background:var(--accent)} .byline-text{font-family:var(--sans);font-size:.78rem;font-weight:500;letter-spacing:.12em;color:var(--ghost);text-transform:uppercase} .s-verse{height:100%;display:flex;flex-direction:column;align-items:flex-start;justify-content:center;padding:8vh 6vw} .drop-q{font-family:var(--serif);font-size:clamp(6rem,15vw,14rem);line-height:.65;color:var(--accent);opacity:.3;position:absolute;top:4vh;left:3.5vw;user-select:none} .verse-text{font-family:var(--serif);font-size:clamp(1.4rem,3vw,2.7rem);font-weight:400;font-style:italic;line-height:1.5;color:var(--white);max-width:28ch;position:relative;z-index:1;padding-top:3vh} .verse-text strong{font-style:normal;font-weight:700} .verse-ref{margin-top:2.5rem;font-family:var(--sans);font-size:.78rem;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:var(--gold)} .s-content{height:100%;display:flex;flex-direction:column;justify-content:center;padding:8vh 6vw} .s-split{height:100%;display:grid;grid-template-columns:1.05fr 1fr} .split-left{padding:6vh 4vw 3.6rem;display:flex;flex-direction:column;justify-content:center;border-right:1px solid rgba(255,255,255,.07)} .split-right{background:var(--charcoal);padding:6vh 4.5vw 3.6rem;display:flex;flex-direction:column;justify-content:center} .s-statement{height:100%;display:flex;flex-direction:column;justify-content:space-between;padding:7vh 3.5vw 3.6rem} .big-stmt{font-family:var(--serif);font-size:clamp(2.5rem,6vw,5.5rem);font-weight:900;line-height:.95;letter-spacing:-.02em;color:var(--white)} .stmt-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1vw} .stmt-card{background:var(--charcoal);padding:1.3rem 1.5vw;border-left:3px solid var(--accent)} .stmt-card-label{font-size:.68rem;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:var(--accent);margin-bottom:.5rem} .stmt-card-text{font-size:clamp(.82rem,1.1vw,.92rem);color:var(--ghost);line-height:1.55} .s-stat{height:100%;display:grid;grid-template-columns:auto 1fr} .stat-col{width:28vw;background:var(--charcoal);display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:4vw;border-right:1px solid rgba(255,255,255,.07)} .big-num{font-family:var(--serif);font-size:clamp(4rem,8vw,7rem);font-weight:900;color:var(--accent);line-height:.9} .stat-trans{font-family:var(--serif);font-size:clamp(1.1rem,1.9vw,1.6rem);color:rgba(255,255,255,.75);margin-top:.9rem} .stat-trans strong{color:var(--white);font-weight:700} .stat-plain{font-size:clamp(.78rem,1.1vw,.9rem);color:var(--ghost);margin-top:.8rem;line-height:1.6} .stat-plain strong{color:var(--white);font-weight:600} .stat-fine{font-size:.65rem;color:rgba(154,165,180,.5);margin-top:1rem;line-height:1.6} .stat-right{padding:6vh 4.5vw 3.6rem;display:flex;flex-direction:column;justify-content:center} .s-grid{height:100%;display:flex;flex-direction:column;padding:5vh 3.5vw 3.6rem} .grid-cards{flex:1;display:grid;gap:1vw;min-height:0} .grid-card{background:var(--charcoal);padding:1.5rem 1.8vw;display:flex;flex-direction:column} .slide-paper .grid-card{background:var(--paper2)} .grid-card-title{font-family:var(--serif);font-size:clamp(1rem,1.8vw,1.4rem);font-weight:700;color:var(--white);margin-bottom:.5rem;padding-bottom:.5rem;border-bottom:1px solid rgba(255,255,255,.08)} .slide-paper .grid-card-title{color:var(--ink);border-bottom-color:rgba(0,0,0,.1)} .grid-card-body{font-size:clamp(.82rem,1.05vw,.9rem);color:var(--ghost);line-height:1.55} .slide-paper .grid-card-body{color:rgba(0,0,0,.6)} .s-steps{height:100%;display:flex;flex-direction:column;padding:5vh 3.5vw 3.6rem} .step-list{flex:1;display:flex;flex-direction:column} .step-item{flex:1;display:grid;grid-template-columns:3.5rem 1fr;gap:1.8vw;align-items:center;border-bottom:1px solid rgba(0,0,0,.1);padding:.5vh 0} .step-item:last-child{border-bottom:none} .step-circle{width:2.8rem;height:2.8rem;border-radius:50%;background:var(--charcoal);display:flex;align-items:center;justify-content:center;font-family:var(--serif);font-size:1.2rem;font-weight:700;color:var(--white);flex-shrink:0} .slide-paper .step-circle{background:var(--ink)} .step-name{font-size:.72rem;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:var(--accent);margin-bottom:.2rem} .step-desc{font-size:clamp(.85rem,1.2vw,.95rem);color:var(--ghost);line-height:1.55} .slide-paper .step-desc{color:rgba(0,0,0,.6)} .step-desc strong{color:var(--white);font-weight:600} .slide-paper .step-desc strong{color:var(--ink)} .s-close{height:100%;display:flex;flex-direction:column;justify-content:space-between;padding:8vh 6vw 3.6rem} .close-quote{font-family:var(--serif);font-size:clamp(1.8rem,4.5vw,4rem);font-weight:400;font-style:italic;color:var(--white);line-height:1.15;max-width:22ch} .close-ref{font-family:var(--sans);font-size:.78rem;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:var(--gold);margin-top:1rem} .close-mid{display:flex;flex-direction:column;gap:.5rem} .close-line{font-size:clamp(.9rem,1.4vw,1.05rem);color:rgba(255,255,255,.5);line-height:1.55} .close-bold{font-size:clamp(.95rem,1.5vw,1.1rem);font-weight:600;color:var(--white);line-height:1.55} .close-final{font-family:var(--serif);font-size:clamp(1.4rem,2.8vw,2.5rem);font-weight:700;font-style:italic;color:var(--accent)} #nav-bar{position:fixed;bottom:0;left:0;right:0;height:3.6rem;display:flex;align-items:center;justify-content:center;gap:1rem;z-index:500;padding:0 2rem;background:var(--bg);border-top:1px solid rgba(255,255,255,.09)} .arr-btn{width:2.4rem;height:2.4rem;background:transparent;border:1px solid rgba(255,255,255,.2);color:rgba(255,255,255,.5);cursor:pointer;font-size:1rem;display:flex;align-items:center;justify-content:center;pointer-events:auto;transition:.2s;flex-shrink:0} .arr-btn:hover{border-color:var(--accent);color:var(--accent)} #dots-row{display:flex;align-items:center;gap:.55rem} .dot-btn{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.2);border:none;cursor:pointer;pointer-events:auto;transition:.25s;padding:0;flex-shrink:0} .dot-btn.active{background:var(--accent);transform:scale(1.5)} #slide-counter{font-family:var(--sans);font-size:.78rem;font-weight:600;letter-spacing:.1em;color:rgba(255,255,255,.35);min-width:3.5rem;text-align:left} #hint{position:fixed;bottom:4rem;left:50%;transform:translateX(-50%);font-family:var(--sans);font-size:.7rem;letter-spacing:.1em;color:rgba(255,255,255,.25);z-index:200;pointer-events:none;white-space:nowrap;transition:opacity 3s ease 3s} #hint.hidden{opacity:0} #pshell{display:none;position:fixed;inset:0;grid-template-columns:60fr 40fr;grid-template-rows:1fr 3.6rem;background:#080A0E;z-index:1000} body.mode-presenter #pshell{display:grid} #ppane{grid-column:1;grid-row:1;position:relative;overflow:hidden;background:#0A0B0F;border-right:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:center} #npane{grid-column:2;grid-row:1;display:flex;flex-direction:column;overflow:hidden;background:#0E1016} #ntop{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.5rem .8rem;border-bottom:1px solid rgba(255,255,255,.07);flex-shrink:0} #nslabel{font-family:var(--sans);font-size:.72rem;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:var(--accent)} #ptimer{font-family:var(--sans);font-size:1.1rem;font-weight:700;color:rgba(255,255,255,.6);letter-spacing:.08em} #ptimer.warn{color:#E8803A} #nbody{flex:1;overflow-y:auto;padding:1.2rem 1.5rem} #nbody p{font-family:var(--sans);font-size:.9rem;line-height:1.7;color:rgba(255,255,255,.72)} #nbody strong{color:white;font-weight:600} #nbody em{color:var(--gold);font-style:italic} #nnext{flex-shrink:0;padding:.9rem 1.5rem;border-top:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.02)} #nnext-lbl{font-size:.65rem;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:var(--ghost);margin-bottom:.3rem} #nnext-title{font-family:var(--serif);font-size:1rem;color:rgba(255,255,255,.5);font-style:italic} #pnav{grid-column:1/3;grid-row:2;display:flex;align-items:center;justify-content:center;gap:1rem;background:var(--bg);border-top:1px solid rgba(255,255,255,.1);padding:0 2rem} .pa-btn{width:2.8rem;height:2.2rem;background:transparent;border:1px solid rgba(255,255,255,.2);color:rgba(255,255,255,.5);cursor:pointer;font-size:1.1rem;display:flex;align-items:center;justify-content:center;transition:.2s} .pa-btn:hover{border-color:var(--accent);color:var(--accent)} #pctr{font-family:var(--sans);font-size:.82rem;font-weight:600;letter-spacing:.1em;color:rgba(255,255,255,.4);min-width:4rem;text-align:left} #pdots{display:flex;align-items:center;gap:.5rem} .p-dot{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.18);border:none;cursor:pointer;transition:.2s;padding:0} .p-dot.active{background:var(--accent);transform:scale(1.4)} body.mode-audience #nav-bar{display:none} #open-links{position:fixed;top:1rem;right:1rem;display:none;gap:.5rem;z-index:600} body:not(.mode-presenter):not(.mode-audience) #open-links{display:flex} .ol-btn{font-family:var(--sans);font-size:.62rem;font-weight:700;letter-spacing:.15em;text-transform:uppercase;padding:.5rem .9rem;border:1px solid rgba(255,255,255,.18);color:rgba(255,255,255,.45);background:transparent;cursor:pointer;text-decoration:none;display:inline-flex;transition:.2s} .ol-btn:hover{border-color:var(--accent);color:var(--accent)} /* ── THEME PICKER ── */ #themePanel{position:fixed;top:3.2rem;right:1rem;width:280px;background:var(--deep);border:1px solid rgba(255,255,255,.1);box-shadow:0 12px 32px rgba(0,0,0,.5);z-index:900;opacity:0;pointer-events:none;transform:translateY(-6px);transition:opacity .18s,transform .18s} #themePanel.open{opacity:1;pointer-events:auto;transform:none} #themePanelInner{padding:.9rem 1rem 1rem} #themePanelTitle{font-family:var(--sans);font-size:.65rem;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:var(--ghost);margin-bottom:.6rem} #themeList{display:grid;grid-template-columns:1fr 1fr;gap:.45rem} .tl-item{display:flex;align-items:center;gap:.55rem;padding:.55rem .65rem;background:transparent;border:1px solid rgba(255,255,255,.08);cursor:pointer;transition:.15s;text-align:left;color:var(--white);font-family:var(--sans)} .tl-item:hover{border-color:var(--accent)} .tl-item.active{border-color:var(--accent);background:rgba(255,255,255,.03)} .tl-swatch{width:18px;height:18px;border-radius:3px;position:relative;flex-shrink:0;border:1px solid rgba(255,255,255,.08)} .tl-swatch::after{content:'';position:absolute;right:-2px;top:-2px;width:8px;height:8px;border-radius:50%;background:var(--accent)} .tl-name{font-size:.78rem;font-weight:600;color:var(--white)} /* ── MOBILE PRESENTER ── */ #pmobiletap{display:none} @media (max-width:820px) and (orientation:portrait){ body.mode-presenter{position:fixed;inset:0;overscroll-behavior:none;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none} #pshell{grid-template-columns:1fr;grid-template-rows:38vh 1fr 4.5rem} #ppane{grid-column:1;grid-row:1;border-right:none;border-bottom:1px solid rgba(255,255,255,.08)} #npane{grid-column:1;grid-row:2} #nbody{padding:1rem 1.1rem;font-size:1.05rem} #nbody p{font-size:1.05rem;line-height:1.65} #ntop{padding:.8rem 1.1rem .6rem} #nslabel{font-size:.65rem} #ptimer{font-size:1rem} #nnext{padding:.7rem 1.1rem} #pnav{grid-column:1;grid-row:3;gap:0;padding:0;position:relative;overflow:hidden} #pnav .pa-btn{flex:1;height:100%;min-height:4.5rem;border:none;border-radius:0;font-size:1.6rem;background:rgba(255,255,255,.02)} #pnav #pp{border-right:1px solid rgba(255,255,255,.08)} #pnav #pn{border-left:1px solid rgba(255,255,255,.08)} #pdots{display:none} #pctr{position:absolute;top:.4rem;left:50%;transform:translateX(-50%);font-size:.68rem;color:var(--ghost);pointer-events:none} #pmobiletap{display:block;position:absolute;inset:0;grid-column:1;grid-row:1/3;pointer-events:none;z-index:5} .pmt-left,.pmt-right{position:absolute;top:0;bottom:0;width:38%;pointer-events:auto;-webkit-tap-highlight-color:transparent} .pmt-left{left:0} .pmt-right{right:0} #open-links{display:none !important} } @media (max-width:820px){ body.mode-audience #open-links{display:none !important} #hint{display:none} }`; const OPEN_LINKS_HTML = ``; const PRESENTER_HTML = `
Slide 10:00
Next
1
`; const NAV_HTML = `
← → or Space
`; const THEME_PICKER_HTML = ``; const PRESENTATION_JS = ` const slides=document.querySelectorAll('.slide'),N=N_SLIDES; let cur=0,trans=false; const MODE=new URLSearchParams(location.search).get('mode')||'single'; document.body.classList.add('mode-'+MODE); /* Sync layer: WebSocket if served from /p/:id, BroadcastChannel otherwise (local file). */ let syncSend=()=>{}; (function initSync(){ if(MODE==='single')return; const pMatch=location.pathname.match(/^\\/p\\/([a-z0-9]{6,16})\\/?$/i); if(pMatch){ const id=pMatch[1]; const wsUrl=(location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'/api/sync/'+id; let ws,retry=0; function connect(){ ws=new WebSocket(wsUrl); ws.onopen=()=>{retry=0;ws.send(JSON.stringify({type:'request-state'}))}; ws.onmessage=ev=>{let m;try{m=JSON.parse(ev.data)}catch{return}if((m.type==='goto'||m.type==='state')&&typeof m.slide==='number'&&MODE==='audience')go(m.slide,false)}; ws.onclose=()=>{retry=Math.min(retry+1,6);setTimeout(connect,Math.min(1000*retry,10000))}; ws.onerror=()=>{try{ws.close()}catch{}}; } connect(); syncSend=(payload)=>{try{if(ws&&ws.readyState===1)ws.send(JSON.stringify(payload))}catch{}}; } else { try{const bc=new BroadcastChannel('CHANNEL_NAME'); if(MODE==='audience'){bc.onmessage=({data})=>{if(data.type==='goto')go(data.slide,false);if(data.type==='request-state')bc.postMessage({type:'state',slide:cur})};bc.postMessage({type:'request-state'})} if(MODE==='presenter')bc.onmessage=({data})=>{if(data.type==='request-state')bc.postMessage({type:'state',slide:cur})}; syncSend=(p)=>{try{bc.postMessage(p)}catch{}}; }catch{} } })(); function typewrite(el,delay=0,speed=42){if(!el)return;const text=el.textContent.trim();el.textContent='';el.style.opacity='1';const cur2=document.createElement('span');cur2.className='tw-cursor';el.appendChild(cur2);let i=0;setTimeout(()=>{const tick=()=>{if(icur2.classList.add('tw-done'),400)};tick()},delay)} function countUp(el,target,dur=1400,delay=0){if(!el)return;const dec=(target.toString().split('.')[1]||'').length,start=performance.now()+delay;const f=now=>{const e=Math.max(0,now-start),p=Math.min(e/dur,1),v=(1-Math.pow(1-p,4))*parseFloat(target);el.textContent=v.toFixed(dec);if(p<1)requestAnimationFrame(f);else el.textContent=parseFloat(target).toFixed(dec)};requestAnimationFrame(f)} function triggerAnim(slide){const els=slide.querySelectorAll('.w,.be,.pi');els.forEach(el=>{el.style.animation='none';el.offsetHeight;el.style.animation=''});slide.querySelectorAll('[data-tw]').forEach(el=>{el.style.opacity='0';typewrite(el,parseInt(el.dataset.twDelay||0),parseInt(el.dataset.twSpeed||42))});const cu=slide.querySelector('[data-countup]');if(cu){cu.style.opacity='1';cu.textContent='0.00';countUp(cu,cu.dataset.countup,1600,400)}} const dotsRow=document.getElementById('dots-row'),ctr=document.getElementById('slide-counter'); slides.forEach((_,i)=>{const b=document.createElement('button');b.className='dot-btn'+(i===0?' active':'');b.setAttribute('aria-label','Slide '+(i+1));b.addEventListener('click',()=>go(i));dotsRow.appendChild(b)}); const dots=dotsRow.querySelectorAll('.dot-btn'); function go(n,broadcast=true){if(n<0||n>=N||trans)return;if(n===cur)return;trans=true;const leaving=slides[cur],entering=slides[n];leaving.classList.add('slide-exiting');setTimeout(()=>{leaving.classList.remove('active','slide-exiting');cur=n;entering.classList.add('active','slide-entering');dots.forEach((d,i)=>d.classList.toggle('active',i===cur));if(ctr)ctr.textContent=(cur+1)+'/'+N;updatePresenter();setTimeout(()=>{entering.classList.remove('slide-entering');trans=false;triggerAnim(entering)},50)},220);if(broadcast&&MODE==='presenter')syncSend({type:'goto',slide:n})} document.getElementById('next')?.addEventListener('click',()=>go(cur+1)); document.getElementById('prev')?.addEventListener('click',()=>go(cur-1)); document.addEventListener('keydown',e=>{if(['ArrowRight','ArrowDown',' ','PageDown'].includes(e.key)){e.preventDefault();go(cur+1)}else if(['ArrowLeft','ArrowUp','PageUp'].includes(e.key)){e.preventDefault();go(cur-1)}else if(e.key==='Home')go(0);else if(e.key==='End')go(N-1);else if(e.key.toLowerCase()==='f'){if(!document.fullscreenElement)document.documentElement.requestFullscreen?.();else document.exitFullscreen?.()}}); let tx=0;document.addEventListener('touchstart',e=>{tx=e.touches[0].clientX},{passive:true});document.addEventListener('touchend',e=>{const d=e.changedTouches[0].clientX-tx;if(Math.abs(d)>45)go(d<0?cur+1:cur-1)}); setTimeout(()=>document.getElementById('hint')?.classList.add('hidden'),5000); triggerAnim(slides[0]); function updatePresenter(){if(MODE!=='presenter')return;const lbl=document.getElementById('nslabel');if(lbl)lbl.textContent='Slide '+(cur+1)+' / '+N;const body=document.getElementById('nbody');if(body)body.innerHTML='

'+(NOTES[cur]||'')+'

';const nt=document.getElementById('nnext-title');if(nt)nt.textContent=curd.classList.toggle('active',i===cur));const pc=document.getElementById('pctr');if(pc)pc.textContent=(cur+1)+'/'+N} let timerSec=0,timerStarted=false; function startTimer(){if(timerStarted)return;timerStarted=true;setInterval(()=>{timerSec++;const el=document.getElementById('ptimer');if(el){const m=Math.floor(timerSec/60),s=timerSec%60;el.textContent=m+':'+(s<10?'0':'')+s;el.classList.toggle('warn',timerSec>35*60)}},1000)} if(MODE==='presenter'){const ppane=document.getElementById('ppane'),deck=document.getElementById('deck');ppane.appendChild(deck);function scaleP(){const pw=ppane.offsetWidth,ph=ppane.offsetHeight,scale=Math.min(pw/window.innerWidth,ph/window.innerHeight)*.95,sw=window.innerWidth*scale,sh=window.innerHeight*scale;deck.style.transform='scale('+scale+')';deck.style.transformOrigin='top left';deck.style.position='absolute';deck.style.left=Math.round((pw-sw)/2)+'px';deck.style.top=Math.round((ph-sh)/2)+'px'}scaleP();window.addEventListener('resize',scaleP);const pdots=document.getElementById('pdots');slides.forEach((_,i)=>{const b=document.createElement('button');b.className='p-dot'+(i===0?' active':'');b.setAttribute('aria-label','Slide '+(i+1));b.addEventListener('click',()=>go(i));pdots.appendChild(b)});document.getElementById('pn')?.addEventListener('click',()=>{startTimer();go(cur+1)});document.getElementById('pp')?.addEventListener('click',()=>{startTimer();go(cur-1)});updatePresenter(); document.querySelector('.pmt-left')?.addEventListener('click',()=>{startTimer();go(cur-1)}); document.querySelector('.pmt-right')?.addEventListener('click',()=>{startTimer();go(cur+1)}); let wakeLock=null;async function requestWake(){try{if('wakeLock'in navigator){wakeLock=await navigator.wakeLock.request('screen');wakeLock.addEventListener('release',()=>{wakeLock=null})}}catch{}} requestWake();document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible'&&!wakeLock)requestWake()}); } (function(){ const panel=document.getElementById('themePanel'),list=document.getElementById('themeList'),toggle=document.getElementById('themeToggle'); if(!panel||!list||!toggle||typeof THEMES==='undefined')return; const current=()=>document.documentElement.getAttribute('data-theme')||THEMES[0].key; function render(){ while(list.firstChild)list.removeChild(list.firstChild); THEMES.forEach(t=>{ const b=document.createElement('button'); b.className='tl-item'+(t.key===current()?' active':''); const sw=document.createElement('span');sw.className='tl-swatch';sw.style.background=t.swatch; const nm=document.createElement('span');nm.className='tl-name';nm.textContent=t.name; b.appendChild(sw);b.appendChild(nm); b.addEventListener('click',()=>{document.documentElement.setAttribute('data-theme',t.key);try{localStorage.setItem('sermon-theme',t.key)}catch{}render();panel.classList.remove('open')}); list.appendChild(b); }); } try{const saved=localStorage.getItem('sermon-theme');if(saved&&THEMES.find(t=>t.key===saved))document.documentElement.setAttribute('data-theme',saved)}catch{} render(); toggle.addEventListener('click',e=>{e.stopPropagation();panel.classList.toggle('open')}); document.addEventListener('click',e=>{if(!panel.contains(e.target)&&e.target!==toggle)panel.classList.remove('open')}); })();`;