/* ═══ TOKENS — Pentecost International brand (gold on dark) ═══ */
.brp-app{
/* Backgrounds — clean daylight palette */
--bg:#F8FAFC;
--surface:#FFFFFF;
--surface2:#F8FAFC;
--surface3:#F1F5F9;
/* Borders */
--b1:#E2E8F0;
--b2:#E2E8F0;
--bgold:rgba(245,158,11,.35);
/* Brand gold — Pentecost International accent, kept as-is on light ground */
--gold:#F59E0B;
--gold2:#D97706;
--gold3:#FBBF24;
--gold-bg:rgba(245,158,11,.09);
--gold-glow:rgba(245,158,11,.22);
/* Legacy aliases so existing rules keep working */
--ink:#1E293B;
--ink-soft:#334155;
--paper:#FFFFFF;
--paper-raised:#F8FAFC;
--paper-line:#E2E8F0;
--gold-soft:rgba(245,158,11,.09);
--gold-dim:#D97706;
--primary:#F59E0B;
--primary-soft:rgba(245,158,11,.09);
--text:#1E293B;
--title:#0F172A;
--muted:#64748B;
--placeholder:#94A3B8;
--success:#15803D;
--warning:#B45309;
--error:#B91C1C;
--info:#2563EB;
/* Church blue — links, active nav states, sparing use */
--blue:#0F3B63;
--blue-hover:#174B7A;
/* Muted gold for emphasized text/values (more legible than --gold on white) */
--gold-text:#C9A227;
/* Semantic */
--ot:#059669;
--ot-soft:rgba(5,150,105,.08);
--nt:#DC2626;
--nt-soft:rgba(220,38,38,.08);
/* Typography */
--fd:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
--fb:-apple-system,"Segoe UI",Inter,system-ui,sans-serif;
--fm:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
/* Shape */
--r:14px;
--r-sm:8px;
--r-xs:6px;
/* Shadows — soft, for a clean daylight surface */
--sh-card:0 1px 2px rgba(15,23,42,.04),0 8px 24px -12px rgba(15,23,42,.10);
--sh-raised:0 2px 8px rgba(15,23,42,.06),0 12px 32px -8px rgba(15,23,42,.12);
}
/* ═══ BASE ═══════════════════════════════════════════════ */
.brp-app *,.brp-app *::before,.brp-app *::after{box-sizing:border-box}
.brp-app{scroll-behavior:smooth}
.brp-app{
margin:0;background:var(--bg);color:var(--text);
font-family:var(--fb);-webkit-font-smoothing:antialiased;
font-size:15px;line-height:1.6;
}
/* ═══ TOPBAR ════════════════════════════════════════════ */
.brp-app .topbar{
position:relative;z-index:10;
background:linear-gradient(135deg,#0F172A 0%,#1E293B 100%);
padding:clamp(1.8rem,5vw,2.8rem) 1.5rem clamp(1.7rem,4.5vw,2.2rem);
text-align:center;
}
.brp-app .topbar::after{
content:'';position:absolute;bottom:0;left:50%;transform:translateX(-50%);
width:140px;height:3px;border-radius:2px;
background:linear-gradient(to right,transparent,var(--gold3),transparent);
}
.brp-app .topbar-inner{position:relative;max-width:640px;margin:0 auto}
.brp-app .topbar .eyebrow{
display:inline-block;font-family:var(--fm);
font-size:.64rem;letter-spacing:.28em;text-transform:uppercase;
color:var(--gold3);margin-bottom:.65rem;opacity:.95;font-weight:700;
}
.brp-app .topbar h1{
font-family:var(--fd);font-size:clamp(1.6rem,5vw,2.5rem);
font-weight:700;margin:0 0 .5rem;color:#FFFFFF;
letter-spacing:-.01em;line-height:1.2;
}
.brp-app .topbar .tagline{color:#94A3B8;font-size:clamp(.8rem,2.5vw,.92rem);margin:0;line-height:1.6}
/* ═══ LAYOUT ════════════════════════════════════════════ */
.brp-app main{
position:relative;z-index:1;
max-width:860px;
margin:1.8rem auto 5rem;
padding:0 clamp(.75rem,3vw,1.5rem);
}
/* ═══ CARD ══════════════════════════════════════════════ */
.brp-app .card{
background:var(--surface);
border-radius:var(--r);
border:1px solid var(--b1);
box-shadow:var(--sh-card);
overflow:hidden;position:relative;z-index:1;
}
.brp-app .card+.card{margin-top:1.25rem}
/* ═══ STICKY PROGRESS BAR ═══════════════════════════════ */
.brp-app .progress-bar{
position:relative;z-index:50;
background:rgba(255,251,235,.94);backdrop-filter:blur(8px);
border-bottom:1px solid var(--bgold);
padding:.7rem clamp(.75rem,3vw,1.5rem);
}
.brp-app .progress-inner{
max-width:860px;margin:0 auto;
display:flex;align-items:center;gap:0;overflow-x:auto;
scrollbar-width:none;
}
.brp-app .progress-inner::-webkit-scrollbar{display:none}
.brp-app .pstep{
display:flex;align-items:center;gap:.4rem;
padding:.35rem .6rem;border-radius:999px;
cursor:pointer;background:none;border:none;flex-shrink:0;
font-family:var(--fb);transition:background .2s;
}
.brp-app .pstep:hover{background:rgba(245,158,11,.14)}
.brp-app .pstep .pnum{
width:1.55rem;height:1.55rem;border-radius:50%;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
font-family:var(--fm);font-size:.68rem;font-weight:700;
background:#FFFFFF;color:var(--muted);
border:1.5px solid var(--b2);transition:all .2s;
}
.brp-app .pstep .plabel{
font-size:.72rem;font-weight:600;color:var(--ink-soft);
white-space:nowrap;transition:color .2s;
}
.brp-app .pstep.done .pnum{background:var(--gold2);border-color:var(--gold2);color:#fff}
.brp-app .pstep.done .plabel{color:var(--ink-soft)}
.brp-app .pstep.current .pnum{background:var(--gold);border-color:var(--gold);color:#000;box-shadow:0 0 0 5px var(--gold-glow)}
.brp-app .pstep.current .plabel{color:var(--blue);font-weight:700}
.brp-app .pconnector{width:clamp(.6rem,2vw,1.4rem);height:2px;background:var(--b2);flex-shrink:0;border-radius:1px}
.brp-app .pconnector.done{background:var(--gold)}
.brp-app .pstep:disabled .plabel,.brp-app .pstep:disabled .pnum{color:var(--placeholder)}
/* ═══ WIZARD STEPS ═══════════════════════════════════════ */
.brp-app .wiz-step{border-top:1px solid var(--b1);scroll-margin-top:4.5rem}
.brp-app .wiz-step:first-of-type{border-top:none}
.brp-app .step-head{
display:flex;align-items:center;gap:.75rem;width:100%;
padding:1rem 1.4rem;
border:none;border-left:4px solid transparent;
background:none;font-family:inherit;text-align:left;
cursor:pointer;user-select:none;
transition:background .2s,border-color .2s;
}
.brp-app .step-head:hover{background:var(--gold-bg)}
.brp-app .step-head:focus-visible{outline:2px solid var(--blue);outline-offset:-2px}
.brp-app .sec-icon{
font-size:1rem;flex-shrink:0;line-height:1;
width:2.2rem;height:2.2rem;
display:flex;align-items:center;justify-content:center;
background:var(--surface3);border-radius:var(--r-xs);
border:1px solid var(--b2);
}
.brp-app .sec-label{
font-family:var(--fb);font-size:.98rem;letter-spacing:0;
text-transform:none;color:var(--ink-soft);font-weight:700;flex:1;
transition:color .2s;
}
.brp-app .step-check{
display:none;align-items:center;justify-content:center;
width:1.3rem;height:1.3rem;border-radius:50%;
background:var(--gold2);color:#fff;font-size:.65rem;font-weight:700;flex-shrink:0;
}
.brp-app .wiz-step.completed .step-check{display:flex}
.brp-app .sec-caret{font-size:.7rem;color:var(--muted);transition:transform .2s,color .2s}
.brp-app .wiz-step.active .step-head{background:var(--gold-bg);border-left-color:var(--gold)}
.brp-app .wiz-step.active .sec-icon{background:var(--gold);border-color:var(--gold)}
.brp-app .wiz-step.active .sec-label{color:var(--blue)}
.brp-app .wiz-step.active .sec-caret{transform:rotate(90deg);color:var(--blue)}
.brp-app .wiz-step:not(.active) .step-body{display:none}
.brp-app .sec-body{padding:1.1rem 1.4rem 1.6rem;background:transparent}
.brp-app .wiz-step.locked .step-head{cursor:not-allowed;opacity:.5}
.brp-app .wiz-step.locked .step-head:hover{background:none}
.brp-app .pstep:disabled{cursor:not-allowed}
.brp-app .pstep:disabled:hover{background:none}
/* ═══ STEP NAV BUTTONS ═══════════════════════════════════ */
.brp-app .step-nav{display:flex;justify-content:space-between;gap:.7rem;margin-top:1.4rem}
.brp-app .btn-step{
font-family:var(--fb);font-size:.85rem;font-weight:600;
padding:.65rem 1.3rem;border-radius:999px;cursor:pointer;
border:1.5px solid var(--b2);background:var(--surface);color:var(--ink-soft);
transition:all .2s;
}
.brp-app .btn-step:hover{border-color:var(--gold);color:var(--gold2);background:var(--gold-bg)}
.brp-app .btn-step-next{
background:linear-gradient(135deg,var(--gold3) 0%,var(--gold) 100%);
color:#000;border-color:transparent;margin-left:auto;
}
.brp-app .btn-step-next:hover{color:#000;box-shadow:0 4px 16px -4px var(--gold-glow);transform:translateY(-1px)}
.brp-app .btn-step-back:first-child{margin-right:auto}
/* ═══ INPUTS ════════════════════════════════════════════ */
.brp-app .row{display:flex;flex-wrap:wrap;gap:.85rem 1.2rem;align-items:flex-end}
.brp-app .field{display:flex;flex-direction:column;gap:.35rem}
.brp-app .field label{font-size:.84rem;color:var(--ink-soft);font-weight:600;letter-spacing:0}
.brp-app input[type=date],.brp-app input[type=number],.brp-app input[type=text],.brp-app input[type=time],.brp-app select{
font-family:var(--fb);font-size:.9rem;padding:.58rem .8rem;
border:1.5px solid var(--b2);border-radius:var(--r-xs);
background:var(--surface2);color:var(--title);
min-width:0;transition:border-color .2s,box-shadow .2s;
-webkit-appearance:none;appearance:none;
}
.brp-app input:focus,.brp-app select:focus{
outline:none;border-color:var(--gold);
box-shadow:0 0 0 3px var(--gold-glow);
}
.brp-app select{
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%2394A3B8' d='M0 0l5 6 5-6z'/%3E%3C/svg%3E");
background-repeat:no-repeat;background-position:right .8rem center;
padding-right:2.3rem;
}
.brp-app .field-full input,.brp-app .field-full select{width:100%}
.brp-app input::placeholder{color:var(--placeholder)}
/* ═══ TIMEFRAME TOGGLE ══════════════════════════════════ */
.brp-app .tf-toggle{
display:flex;border:1.5px solid var(--b2);border-radius:var(--r-xs);
overflow:hidden;width:fit-content;margin-bottom:.9rem;background:var(--surface2);
}
.brp-app .tf-toggle label{
display:flex;align-items:center;gap:.3rem;
padding:.45rem .9rem;font-size:.8rem;cursor:pointer;
color:var(--muted);transition:all .2s;
}
.brp-app .tf-toggle input[type=radio]{display:none}
.brp-app .tf-toggle label:has(input:checked){
background:linear-gradient(135deg,rgba(245,158,11,.18),rgba(245,158,11,.08));
color:var(--gold);font-weight:600;
}
.brp-app .tf-toggle label:first-child{border-right:1.5px solid var(--b2)}
.brp-app .days-info{font-family:var(--fm);font-size:.74rem;color:var(--success);margin-top:.45rem;min-height:1.1em;font-weight:500}
.brp-app .help-text{font-size:.76rem;color:var(--muted);margin-top:.32rem;line-height:1.55}
.brp-app .step-lead{font-size:1rem;color:var(--ink-soft);font-weight:500;line-height:1.5;margin:0 0 1.1rem}
/* ═══ ACTIVE / DIM ══════════════════════════════════════ */
.brp-app .tf-active label{color:var(--success)!important;font-weight:600}
.brp-app .tf-active input{border-color:var(--success)!important}
.brp-app .tf-dim{opacity:.32;pointer-events:none}
.brp-app .tf-dim input,.brp-app .tf-dim select{background:var(--surface3)!important;cursor:not-allowed}
/* ═══ WEEKDAY PILLS ═════════════════════════════════════ */
.brp-app .wd-label{font-size:.92rem;color:var(--ink-soft);font-weight:600;margin-bottom:.55rem}
.brp-app .wd-grid{display:flex;flex-wrap:wrap;gap:.4rem}
.brp-app .wd-pill input[type=checkbox]{display:none}
.brp-app .wd-pill label{
display:flex;align-items:center;justify-content:center;
width:2.4rem;height:2.4rem;border-radius:50%;
font-size:.73rem;font-weight:600;
border:1.5px solid var(--b2);background:var(--surface2);
color:var(--muted);cursor:pointer;transition:all .2s;user-select:none;
}
.brp-app .wd-pill input:checked+label{
background:linear-gradient(135deg,var(--gold3),var(--gold));
border-color:transparent;color:#000;font-weight:700;
box-shadow:0 2px 14px var(--gold-glow);
}
/* ═══ PLAN TYPE CARDS ════════════════════════════════════ */
.brp-app .plan-type-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.8rem;margin-bottom:1.1rem}
.brp-app .plan-type-card{
display:flex;flex-direction:column;gap:.3rem;
border:1.5px solid var(--b2);border-radius:var(--r-sm);
padding:1rem 1.1rem;cursor:pointer;background:var(--surface);
transition:all .2s;position:relative;
}
.brp-app .plan-type-card:hover{border-color:var(--gold);background:var(--gold-bg)}
.brp-app .plan-type-card input{position:absolute;opacity:0;pointer-events:none}
.brp-app .plan-type-card:has(input:checked){
border-color:var(--gold);border-width:2px;background:var(--gold-bg);
box-shadow:0 0 0 3px var(--gold-glow),var(--sh-card);
}
.brp-app .plan-type-card:focus-within{outline:2px solid var(--blue);outline-offset:2px}
.brp-app .ptc-icon{font-size:1.4rem;line-height:1}
.brp-app .ptc-title{font-family:var(--fd);font-size:1.1rem;font-weight:700;color:var(--text)}
.brp-app .ptc-desc{font-size:.78rem;color:var(--muted);line-height:1.5}
/* ═══ READING PACE CARD ══════════════════════════════════ */
.brp-app .pace-card{
background:linear-gradient(160deg,#FFF6DC 0%,#FFFFFF 65%);
border:2px solid var(--bgold);border-radius:var(--r-sm);
padding:1.1rem 1.2rem;margin-top:1rem;
}
.brp-app .pace-head{display:flex;align-items:center;gap:.4rem;margin-bottom:.55rem}
.brp-app .pace-title{font-family:var(--fd);font-size:1.05rem;font-weight:700;color:var(--blue)}
.brp-app .pace-check-list{list-style:none;margin:0 0 .8rem;padding:0;display:flex;flex-wrap:wrap;gap:.4rem 1rem}
.brp-app .pace-check-list li{font-size:.82rem;color:var(--ink-soft)}
.brp-app .pace-check-list li:first-child{color:var(--blue);font-weight:700}
.brp-app .pace-stats{display:flex;gap:.7rem;margin-bottom:.7rem;flex-wrap:wrap}
.brp-app .pace-stat{
flex:1;min-width:110px;background:var(--surface);border:1px solid var(--bgold);
border-radius:var(--r-xs);padding:.6rem .8rem;text-align:center;
}
.brp-app .pace-stat .pace-n{display:block;font-family:var(--fm);font-size:1.35rem;font-weight:700;color:var(--gold-text)}
.brp-app .pace-stat .pace-l{display:block;font-size:.65rem;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-top:.15rem}
.brp-app .pace-summary{margin:0;font-size:.85rem;color:var(--ink-soft);line-height:1.55}
.brp-app .pace-summary strong{color:var(--title)}
/* ═══ CHECKBOXES ════════════════════════════════════════ */
.brp-app .check-row{display:flex;align-items:center;gap:.5rem;font-size:.84rem;cursor:pointer;color:var(--ink-soft)}
.brp-app .check-row input[type=checkbox]{accent-color:var(--gold);width:15px;height:15px;flex-shrink:0;cursor:pointer}
.brp-app .check-badge{font-size:.66rem;color:var(--muted);background:var(--b1);border-radius:999px;padding:.05rem .45rem}
.brp-app .check-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.5rem .9rem}
.brp-app .check-item{display:flex;flex-direction:column;gap:.12rem}
.brp-app .check-sub{font-size:.7rem;color:var(--muted);padding-left:1.5rem}
/* ═══ BOOK GRID ══════════════════════════════════════════ */
.brp-app .book-detail{margin-top:.65rem}
.brp-app .book-detail summary{cursor:pointer;font-size:.79rem;color:var(--muted);font-family:var(--fm);letter-spacing:.03em}
.brp-app .book-grid{
display:grid;grid-template-columns:repeat(auto-fill,minmax(138px,1fr));
gap:.18rem .55rem;max-height:230px;overflow-y:auto;
padding:.7rem;background:var(--surface3);
border-radius:var(--r-xs);border:1px solid var(--b1);margin-top:.5rem;
}
.brp-app .book-grid .glabel{
grid-column:1/-1;font-family:var(--fm);font-size:.6rem;
text-transform:uppercase;letter-spacing:.1em;color:var(--gold);
margin-top:.4rem;margin-bottom:.15rem;opacity:.7;
}
.brp-app .book-grid label{font-size:.78rem;display:flex;align-items:center;gap:.32rem;cursor:pointer;padding:.12rem 0;color:var(--ink-soft)}
.brp-app .book-grid input[type=checkbox]{accent-color:var(--gold);width:12px;height:12px}
/* ═══ GENERATE BUTTON ════════════════════════════════════ */
.brp-app .gen-wrap{padding:1.2rem 1.4rem 1.5rem;border-top:1px solid var(--b1)}
.brp-app .btn-gen{
width:100%;
background:var(--blue);
color:#fff;border:none;
font-family:var(--fb);font-size:1.05rem;font-weight:700;
padding:1rem 1.2rem;border-radius:var(--r-sm);cursor:pointer;
display:block;text-align:center;
transition:background .18s,transform .18s,box-shadow .18s;
letter-spacing:0;
box-shadow:0 2px 10px -2px rgba(15,59,99,.3);
}
.brp-app .btn-gen:hover{background:var(--blue-hover);transform:translateY(-1px);box-shadow:0 6px 18px -4px rgba(15,59,99,.4)}
.brp-app .btn-gen:active{transform:none}
/* ═══ PLAN ID STRIP ══════════════════════════════════════ */
.brp-app .plan-id-strip{
display:flex;align-items:center;flex-wrap:wrap;gap:.5rem .8rem;
padding:.7rem 1.4rem;
background:var(--gold-bg);border-bottom:1px solid var(--bgold);
}
.brp-app .plan-id-label{
font-family:var(--fm);font-size:.61rem;letter-spacing:.12em;
text-transform:uppercase;color:var(--muted);white-space:nowrap;
}
.brp-app .plan-id-code{
font-family:var(--fm);font-size:.95rem;font-weight:700;
color:var(--gold2);letter-spacing:.12em;
background:#FFFBEB;border:1.5px solid var(--bgold);
border-radius:var(--r-xs);padding:.15rem .55rem;user-select:all;cursor:text;
}
.brp-app .plan-id-tip{font-size:.7rem;color:var(--muted);flex:1;min-width:180px;line-height:1.4}
/* ═══ OUTPUT HEADER ══════════════════════════════════════ */
.brp-app .out-head{padding:1.25rem 1.4rem 1rem;border-bottom:1px solid var(--b1)}
.brp-app .out-title{
font-family:var(--fd);font-size:clamp(1.1rem,4vw,1.5rem);
margin:0 0 .25rem;color:var(--title);font-weight:700;
}
.brp-app .out-range{font-family:var(--fm);font-size:.7rem;color:var(--muted)}
/* ═══ TOOLBAR ════════════════════════════════════════════ */
.brp-app .toolbar{display:flex;gap:.55rem;flex-wrap:wrap;margin-top:.8rem}
.brp-app .btn{
font-family:var(--fb);font-size:.82rem;font-weight:600;padding:.55rem 1rem;
border-radius:var(--r-xs);border:1.5px solid var(--b2);
background:var(--surface);color:var(--ink-soft);
cursor:pointer;transition:all .2s;
display:inline-flex;align-items:center;gap:.45rem;white-space:nowrap;
}
.brp-app .btn-ic{font-size:.95rem;line-height:1}
.brp-app .btn:hover{border-color:var(--blue);color:var(--blue);background:var(--gold-bg)}
.brp-app .btn-pdf{border-color:var(--blue);color:var(--blue);background:#fff}
.brp-app .btn-pdf:hover{background:var(--blue);color:#fff}
.brp-app .btn-ics{border-color:var(--gold2);color:var(--gold2);background:#fff}
.brp-app .btn-ics:hover{background:var(--gold2);color:#fff}
.brp-app .btn:disabled{opacity:.45;cursor:wait}
.brp-app .btn-copied{border-color:var(--success)!important;color:var(--success)!important;background:#fff!important}
/* ═══ STATS ══════════════════════════════════════════════ */
.brp-app .stats-strip{
display:grid;grid-template-columns:repeat(auto-fit,minmax(95px,1fr));
gap:.6rem;padding:1rem 1.4rem;border-bottom:1px solid var(--b1);
}
.brp-app .stat{
background:var(--surface3);border:1px solid var(--b1);
border-radius:var(--r-sm);padding:.7rem .8rem;
transition:border-color .2s;
}
.brp-app .stat:hover{border-color:var(--bgold)}
.brp-app .stat .n{font-family:var(--fm);font-size:1.4rem;color:var(--title);display:block;line-height:1.1;font-weight:700}
.brp-app .stat .l{font-size:.61rem;color:var(--gold);text-transform:uppercase;letter-spacing:.08em;display:block;margin-top:.2rem;opacity:.75}
/* ═══ HEATMAP ════════════════════════════════════════════ */
/* ═══ PLAN BODY ══════════════════════════════════════════ */
.brp-app .plan-body{padding:1rem 1.4rem 2rem}
.brp-app .plan-body.cols-2{column-count:2;column-gap:2.4rem}
@media(max-width:640px){.brp-app .plan-body.cols-2{column-count:1}}
/* ═══ LIST ═══════════════════════════════════════════════ */
.brp-app .list-day{
display:grid;
grid-template-columns:auto 1fr auto auto;
gap:.35rem .55rem;align-items:start;
padding:.4rem 0;border-bottom:1px solid var(--b1);
break-inside:avoid;-webkit-column-break-inside:avoid;
}
.brp-app .list-day.has-cb{grid-template-columns:auto auto 1fr auto auto}
.brp-app .list-day.has-date{grid-template-columns:auto auto 1fr auto auto}
.brp-app .list-day.has-cb.has-date{grid-template-columns:auto auto auto 1fr auto auto}
.brp-app .d-cb{accent-color:var(--gold);width:13px;height:13px;margin-top:.2rem;cursor:pointer;flex-shrink:0}
.brp-app .d-num{
font-family:var(--fm);font-size:.63rem;
background:var(--gold-bg);color:var(--gold2);
border:1px solid var(--bgold);border-radius:999px;
min-width:1.8rem;text-align:center;
padding:.08rem .26rem;margin-top:.08rem;line-height:1.5;font-weight:700;
}
.brp-app .d-date{font-family:var(--fm);font-size:.64rem;color:var(--muted);padding-top:.12rem;white-space:nowrap}
.brp-app .d-read{font-family:var(--fd);font-size:clamp(.8rem,2.2vw,.9rem);line-height:1.4;color:var(--text)}
.brp-app .d-read .extra{display:block;font-family:var(--fb);font-size:.7rem;color:var(--muted)}
.brp-app .d-stat{font-size:.62rem;color:var(--muted);font-family:var(--fm);white-space:nowrap;padding-top:.1rem}
.brp-app .ot-bar{border-left:3px solid var(--ot);padding-left:.5rem}
.brp-app .nt-bar{border-left:3px solid var(--nt);padding-left:.5rem}
/* ═══ BG LINKS ═══════════════════════════════════════════ */
.brp-app .bg-link{
display:inline-flex;align-items:center;gap:.22rem;
font-size:.66rem;color:var(--gold);text-decoration:none;
border:1px solid var(--bgold);padding:.1rem .45rem;
border-radius:999px;white-space:nowrap;margin-top:.08rem;
transition:all .2s;background:var(--gold-bg);
}
.brp-app .bg-link:hover{background:var(--gold);color:#000}
/* ═══ WEEKLY ══════════════════════════════════════════════ */
.brp-app .week-block{margin-bottom:1.4rem;break-inside:avoid;-webkit-column-break-inside:avoid}
.brp-app .week-block h3{
font-family:var(--fm);font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;
color:var(--gold);margin:0 0 .4rem;padding-bottom:.3rem;
border-bottom:1px solid var(--b1);opacity:.8;
}
/* ═══ CALENDAR ════════════════════════════════════════════ */
.brp-app .cal-month{margin-bottom:1.8rem}
.brp-app .cal-month h3{font-family:var(--fd);font-size:1.05rem;margin:0 0 .5rem;color:var(--text)}
.brp-app .cal-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
.brp-app .cal-scroll-hint{display:none;font-size:.67rem;color:var(--muted);font-family:var(--fm);text-align:right;margin-bottom:.2rem}
.brp-app .cal-grid{display:grid;grid-template-columns:repeat(7,minmax(78px,1fr));gap:3px;min-width:546px}
.brp-app .cal-dow{font-family:var(--fm);font-size:.6rem;text-align:center;color:var(--muted);text-transform:uppercase;padding-bottom:.25rem}
.brp-app .cal-cell{
min-height:84px;max-height:84px;background:var(--surface3);
border:1px solid var(--b1);border-radius:var(--r-xs);
padding:.3rem .35rem;font-size:.67rem;position:relative;overflow:hidden;
transition:border-color .2s;
}
.brp-app .cal-cell:not(.empty):not(.rest):hover{border-color:var(--bgold);max-height:none;z-index:2;box-shadow:var(--sh-raised)}
.brp-app .cal-cell.empty{background:transparent;border-color:transparent}
.brp-app .cal-cell.rest{background:#FAFBFC;border-color:var(--b1);opacity:.7}
.brp-app .cal-rest-label{font-size:.56rem;color:var(--muted);font-style:italic;display:block;margin-top:.3rem}
.brp-app .cal-dnum{font-family:var(--fm);font-size:.62rem;color:var(--muted);line-height:1}
.brp-app .cal-daynum{font-size:.57rem;font-family:var(--fm);color:var(--gold2);display:block;line-height:1;margin-top:.1rem;opacity:.85}
.brp-app .cal-reading{
font-family:var(--fd);font-size:.68rem;line-height:1.3;margin-top:.12rem;color:var(--text);
display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;
overflow:hidden;text-overflow:ellipsis;
}
.brp-app .cal-extra{font-size:.59rem;color:var(--muted);display:block;margin-top:.08rem}
.brp-app .cal-link{display:inline-block;font-size:.58rem;color:var(--gold);text-decoration:none;margin-top:.2rem;border-bottom:1px solid var(--bgold);line-height:1;padding-bottom:1px;opacity:.8}
.brp-app .cal-link:hover{opacity:1}
.brp-app .cal-cb{position:absolute;top:3px;right:3px;width:11px;height:11px;accent-color:var(--gold)}
.brp-app .cal-cell.ot-tinted{box-shadow:inset 3px 0 0 var(--ot)}
.brp-app .cal-cell.nt-tinted{box-shadow:inset 3px 0 0 var(--nt)}
/* ═══ SITE FOOTER ════════════════════════════════════════ */
.brp-app .site-footer{text-align:center;color:var(--muted);font-size:.71rem;padding:1.2rem 1rem 3rem}
.brp-app .site-footer a{color:var(--blue);text-decoration:none;border-bottom:1px solid transparent}
.brp-app .site-footer a:hover{color:var(--blue-hover);border-bottom-color:var(--blue-hover)}
/* ═══ PRINT ══════════════════════════════════════════════ */
@media print{
@page{
margin:1.5cm 1.8cm 2cm;
}
.brp-app{background:#fff!important;color:#000!important;font-size:9.5pt}
.brp-app .topbar,.brp-app #planForm,.brp-app .toolbar,.brp-app .site-footer,.brp-app .gen-wrap,.brp-app .btn,.brp-app .progress-bar,.brp-app .plan-id-strip{display:none!important}
.brp-app .step-head{display:none!important}
.brp-app #output{
display:block!important;box-shadow:none!important;
margin:0!important;border:none!important;background:#fff!important;
}
.brp-app .out-head{border-bottom:2pt solid #1E293B;padding-bottom:.4rem;margin-bottom:.5rem}
.brp-app .out-title{font-size:14pt!important;color:#000!important}
.brp-app .out-range{font-size:8pt!important;color:#555!important}
.brp-app .stats-strip{-webkit-print-color-adjust:exact;print-color-adjust:exact}
.brp-app .stat{border:1px solid #ccc!important;background:#f9f9f9!important}
.brp-app .stat .n{color:#000!important}.brp-app .stat .l{color:#555!important}
.brp-app .list-day{break-inside:avoid}
.brp-app .d-read,.brp-app .cal-reading{color:#000!important}
.brp-app .d-date,.brp-app .cal-dnum{color:#555!important}
.brp-app .cal-scroll{overflow:visible!important}
.brp-app .cal-grid{min-width:0!important;grid-template-columns:repeat(7,1fr)!important}
.brp-app .cal-cell{min-height:55pt!important;font-size:6pt!important;break-inside:avoid;background:#fff!important;border:1px solid #ddd!important;color:#000!important}
.brp-app .cal-month,.brp-app .week-block{break-inside:avoid}
.brp-app .ot-bar,.brp-app .nt-bar,.brp-app .cal-cell.ot-tinted,.brp-app .cal-cell.nt-tinted,.brp-app .d-num{-webkit-print-color-adjust:exact;print-color-adjust:exact}
.brp-app a[href]::after{content:none!important}
.brp-app .bg-link,.brp-app .cal-link{display:none!important}
.brp-app .plan-body{padding:.5rem 0 0;color:#000}
.brp-app .plan-body.cols-2{column-count:2!important;column-gap:1.6rem!important}
.brp-app .print-footer{
display:block!important;position:fixed;bottom:.4cm;left:0;right:0;
text-align:center;font-size:7.5pt;color:#777;
}
.brp-app .print-footer::after{content:"Page " counter(page) " of " counter(pages)}
}
.brp-app .print-footer{display:none}
/* ═══ RESPONSIVE ═════════════════════════════════════════ */
@media(max-width:560px){
.brp-app .list-day{grid-template-columns:auto 1fr auto!important}
.brp-app .list-day .d-date{display:none}
.brp-app .d-stat{grid-column:2/-1}
.brp-app .stats-strip{grid-template-columns:repeat(3,1fr)}
.brp-app .out-head{flex-direction:column;gap:.6rem}
.brp-app .toolbar{width:100%}
.brp-app .toolbar .btn{flex:1;justify-content:center;font-size:.7rem;padding:.4rem .5rem}
.brp-app .cal-scroll-hint{display:block}
}
@media(max-width:400px){
.brp-app .stats-strip{grid-template-columns:repeat(2,1fr)}
.brp-app .stat .n{font-size:1.1rem}
}
1 Schedule
2 Reading Plan
3 Books
4 Reading Style
5 Output
6 Generate
📅
Schedule
✓
▶
Plan name
Number of days
End date
Start date
Reading days
End date
Tip: for a one-year whole-Bible plan, set 365 days. For a pace that fits your life, pick an end date and we'll calculate how many chapters per day.
Select the days you'll read
Sun
Mon
Tue
Wed
Thu
Fri
Sat
All 7 days selected. Tap a day to skip it — the plan moves to the next selected day instead.
Next: Reading Plan
📖
Reading Plan
✓
▶
Choose how much of the Bible to read. You'll pick specific books next.
📖
Bible Plan
Read straight through the whole Bible — every book, Old and New Testament, selected for you.
🗂️
Custom Selection
Start from a blank slate and hand-pick a testament or specific books to read.
Back
Next: Books
📚
Books
✓
▶
Old Testament 39 books
New Testament 27 books
Fine-tune individual books ▸
Add a daily Psalm
Add a daily Proverb
Psalms and/or Proverbs are read as a daily add-on instead, so they're left out of the main list above — no double-reading.
Back
Next: Reading Style
⚙️
Reading style
✓
▶
Set your reading order and daily pace.
Reading order
Traditional (canonical)
Chronological (approximate)
Daily pace
Same number of chapters each day
Same reading time each day
Chapters vary in length — some take a minute, some take ten. "Same reading time" balances that, so no single day is unexpectedly long.
Back
Next: Output
🎨
Output
✓
▶
Output format
Calendar
List
Weekly
Checkboxes to mark off each day
Color-code OT / NT
OT & NT side-by-side each day Requires both testaments selected
Reverse reading order
Hide dates (Day 1, Day 2…)
Show overall statistics
Show daily chapter/min counts
Add Bible Gateway links to each day's reading
Bible translation
ESVNIVKJVNKJV
GNTCSBMSG
📅 Calendar (.ics) export settings
Daily reminder time (applies to .ics only)
The .ics file can be imported into Google Calendar, Apple Calendar, or Outlook. Each day becomes a calendar event with the passage in the title and a Bible Gateway link you can tap to read.
Back
Next: Generate
✦
Generate
✓
▶
Generate my reading plan
You'll be able to download it as a PDF, plain text, or a calendar (.ics) file next.
Back
📄 PDF
📅 Calendar (.ics)
📝 Text (.txt)
📋 Copy
📅 Calendar Plan ID
This ID is included in every imported event's description — search for it in Google Calendar to find and delete all of them at once, if you ever need to.
/* ═══════════════════════════════════════════════════════
BIBLE DATA (chapters · KJV word counts)
═══════════════════════════════════════════════════════ */
const BOOKS=[
["Genesis","OT",50,38262],["Exodus","OT",40,32687],["Leviticus","OT",27,24541],
["Numbers","OT",36,32896],["Deuteronomy","OT",34,28352],["Joshua","OT",24,18854],
["Judges","OT",21,18966],["Ruth","OT",4,2574],["1 Samuel","OT",31,25048],
["2 Samuel","OT",24,20600],["1 Kings","OT",22,24513],["2 Kings","OT",25,23517],
["1 Chronicles","OT",29,20365],["2 Chronicles","OT",36,26069],["Ezra","OT",10,7440],
["Nehemiah","OT",13,10480],["Esther","OT",10,5633],["Job","OT",42,18098],
["Psalms","OT",150,43738],["Proverbs","OT",31,15038],["Ecclesiastes","OT",12,5579],
["Song of Solomon","OT",8,2658],["Isaiah","OT",66,37036],["Jeremiah","OT",52,42645],
["Lamentations","OT",5,3411],["Ezekiel","OT",48,39401],["Daniel","OT",12,11602],
["Hosea","OT",14,5174],["Joel","OT",3,2033],["Amos","OT",9,4216],["Obadiah","OT",1,669],
["Jonah","OT",4,1320],["Micah","OT",7,3152],["Nahum","OT",3,1284],["Habakkuk","OT",3,1475],
["Zephaniah","OT",3,1616],["Haggai","OT",2,1130],["Zechariah","OT",14,6443],["Malachi","OT",4,1781],
["Matthew","NT",28,23684],["Mark","NT",16,15166],["Luke","NT",24,25939],["John","NT",21,19094],
["Acts","NT",28,24245],["Romans","NT",16,9422],["1 Corinthians","NT",16,9462],["2 Corinthians","NT",13,6065],
["Galatians","NT",6,3084],["Ephesians","NT",6,3022],["Philippians","NT",4,2183],["Colossians","NT",4,1979],
["1 Thessalonians","NT",5,1837],["2 Thessalonians","NT",3,1022],["1 Timothy","NT",6,2244],["2 Timothy","NT",4,1666],
["Titus","NT",3,896],["Philemon","NT",1,430],["Hebrews","NT",13,6897],["James","NT",5,2304],
["1 Peter","NT",5,2476],["2 Peter","NT",3,1553],["1 John","NT",5,2517],["2 John","NT",1,298],
["3 John","NT",1,294],["Jude","NT",1,608],["Revelation","NT",22,11995]
].map(([n,t,c,w])=>({name:n,testament:t,chapters:c,words:w}));
const BOOK_MAP=new Map(BOOKS.map(b=>[b.name,b]));
const OT_CHRON=["Job","Genesis","Exodus","Leviticus","Numbers","Deuteronomy","Joshua","Judges","Ruth",
"1 Samuel","2 Samuel","Psalms","1 Kings","1 Chronicles","2 Chronicles","Proverbs","Ecclesiastes",
"Song of Solomon","Joel","Jonah","Amos","Hosea","Isaiah","Micah","Nahum","2 Kings","Zephaniah",
"Habakkuk","Jeremiah","Lamentations","Ezekiel","Daniel","Obadiah","Ezra","Haggai","Zechariah","Esther","Nehemiah","Malachi"];
const NT_CHRON=["James","Galatians","1 Thessalonians","2 Thessalonians","1 Corinthians","2 Corinthians",
"Romans","Mark","Ephesians","Philippians","Colossians","Philemon","Luke","Acts","1 Timothy","Titus",
"1 Peter","Hebrews","2 Timothy","Matthew","2 Peter","Jude","John","1 John","2 John","3 John","Revelation"];
const DOW_SHORT=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const MONTHS=['January','February','March','April','May','June','July','August','September','October','November','December'];
/* ═══════════════════════════════════════════════════════
CORE ALGORITHM
═══════════════════════════════════════════════════════ */
function buildUnits(books,logic){
const u=[];
for(const b of books){
const w=logic==='words'?b.words/b.chapters:1;
for(let c=1;c[]);
const total=units.reduce((s,u)=>s+u.weight,0);
const days=Array.from({length:numDays},()=>[]);
let cum=0;
for(const u of units){
let i=Math.floor((cum/total)*numDays);
if(i>=numDays) i=numDays-1;
days[i].push(u);
cum+=u.weight;
}
return days;
}
function groupRanges(units){
const r=[];
for(const u of units){
const last=r[r.length-1];
if(last&&last.book===u.book&&u.chapter===last.to+1) last.to=u.chapter;
else r.push({book:u.book,from:u.chapter,to:u.chapter});
}
return r;
}
function formatRanges(ranges){
if(!ranges.length) return '—';
return ranges.map(r=>r.from===r.to?`${r.book} ${r.from}`:`${r.book} ${r.from}–${r.to}`).join('; ');
}
function rangesWeight(ranges){
let ch=0,w=0;
for(const r of ranges){
const n=r.to-r.from+1; ch+=n;
const b=BOOK_MAP.get(r.book);
w+=b?b.words/b.chapters*n:0;
}
return{ch,w};
}
/* ═══════════════════════════════════════════════════════
DATES
═══════════════════════════════════════════════════════ */
function isoDate(d){return`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`}
function fmtDate(d){return`${DOW_SHORT[d.getDay()]} ${MONTHS[d.getMonth()].slice(0,3)} ${d.getDate()}`}
function fmtDateShort(d){return`${MONTHS[d.getMonth()].slice(0,3)} ${d.getDate()}, ${d.getFullYear()}`}
function fmtDateLong(d){return`${DOW_SHORT[d.getDay()]}, ${MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`}
function generateDates(startStr,numDays,wdSet){
const dates=[]; let d=new Date(startStr+'T00:00:00'); let guard=0;
while(dates.length<numDays&&guard<numDays*30+3650){
if(wdSet.has(d.getDay())) dates.push(new Date(d));
d.setDate(d.getDate()+1); guard++;
}
return dates;
}
function countReadingDays(startStr,endStr,wdSet){
let count=0;
const s=new Date(startStr+'T00:00:00'),e=new Date(endStr+'T00:00:00');
let d=new Date(s);
while(dBOOK_MAP.get(n))
:BOOKS).filter(b=>selected.has(b.name));
// Psalms/Proverbs read as a daily add-on come out of the main pool
// so a reader isn't served the same book twice on the same day.
if(cfg.psalm) ordered=ordered.filter(b=>b.name!=='Psalms');
if(cfg.proverb) ordered=ordered.filter(b=>b.name!=='Proverbs');
let perDay;
if(cfg.overlap){
const ot=ordered.filter(b=>b.testament==='OT'), nt=ordered.filter(b=>b.testament==='NT');
let ou=buildUnits(ot,cfg.logic), nu=buildUnits(nt,cfg.logic);
if(cfg.reverse){ou.reverse();nu.reverse();}
const od=distribute(ou,cfg.numDays), nd=distribute(nu,cfg.numDays);
perDay=od.map((o,i)=>({otRanges:groupRanges(o),ntRanges:groupRanges(nd[i])}));
} else {
let units=buildUnits(ordered,cfg.logic);
if(cfg.reverse) units.reverse();
const days=distribute(units,cfg.numDays);
perDay=days.map(d=>({ranges:groupRanges(d)}));
}
const wdSet=new Set(cfg.weekdays);
const dates=generateDates(cfg.startDate,cfg.numDays,wdSet);
const entries=perDay.map((p,i)=>({
dayNum:i+1,date:dates[i],
ranges:p.ranges||null,otRanges:p.otRanges||null,ntRanges:p.ntRanges||null,
psalm:cfg.psalm?(i%150)+1:null,
proverb:cfg.proverb?(i%31)+1:null
}));
let totalCh=ordered.reduce((s,b)=>s+b.chapters,0);
let totalW=ordered.reduce((s,b)=>s+b.words,0);
// Daily Psalm/Proverb add-ons happen every reading day on top of the main
// pool, so they belong in the overall pace/time totals too.
if(cfg.psalm){
const b=BOOK_MAP.get('Psalms');
totalCh+=cfg.numDays; totalW+=cfg.numDays*(b.words/b.chapters);
}
if(cfg.proverb){
const b=BOOK_MAP.get('Proverbs');
totalCh+=cfg.numDays; totalW+=cfg.numDays*(b.words/b.chapters);
}
const stats={
days:cfg.numDays,books:ordered.length,chapters:totalCh,words:totalW,
avgCh:totalCh/cfg.numDays,estHours:totalW/200/60,
start:dates[0],end:dates[dates.length-1]
};
return{entries,stats};
}
/* ═══════════════════════════════════════════════════════
BIBLE GATEWAY LINKS (comma separator = works reliably)
═══════════════════════════════════════════════════════ */
function bgUrl(ranges,version){
if(!ranges||!ranges.length) return null;
const q=ranges.map(r=>r.from===r.to?`${r.book} ${r.from}`:`${r.book} ${r.from}-${r.to}`).join(', ');
return`https://www.biblegateway.com/passage/?search=${encodeURIComponent(q)}&version=${encodeURIComponent(version)}`;
}
function bgUrlHtml(ranges,version){
const u=bgUrl(ranges,version);
return u?u.replace(/&/g,'&'):null;
}
function allRanges(e){return e.ranges?e.ranges:[...(e.otRanges||[]),...(e.ntRanges||[])]}
// Includes daily Psalm/Proverb in the BG link so the reader gets everything in one tap
function allRangesForLink(e){
const ar=[...allRanges(e)];
if(e.psalm) ar.push({book:'Psalms', from:e.psalm, to:e.psalm});
if(e.proverb) ar.push({book:'Proverbs', from:e.proverb, to:e.proverb});
return ar;
}
/* ═══════════════════════════════════════════════════════
RENDERING HELPERS
═══════════════════════════════════════════════════════ */
function readingHTML(e,cfg){
let h='';
if(e.ranges) h+=`
${formatRanges(e.ranges)} `;
else {
h+=`
OT — ${formatRanges(e.otRanges)} `;
h+=``;
}
if(e.psalm) h+=``;
if(e.proverb) h+=``;
return h;
}
function dayStatHTML(e){
const ar=allRanges(e);
if(!ar.length) return'';
const{ch,w}=rangesWeight(ar);
return`
${ch} ch · ~${Math.round(w/200)}min `;
}
function linkHTML(e,cfg,cls='bg-link'){
if(!cfg.links) return'';
const ar=allRangesForLink(e);
const u=bgUrlHtml(ar,cfg.version);
if(!u) return'';
return`
Read → `;
}
function tcls(e,cfg){
if(!cfg.colors) return'';
const ar=allRanges(e);
if(!ar.length) return'';
const b=BOOK_MAP.get(ar[0].book);
return b?b.testament==='OT'?'ot-bar':'nt-bar':'';
}
function calTcls(e,cfg){
if(!cfg.colors) return'';
const ar=allRanges(e);
if(!ar.length) return'';
const b=BOOK_MAP.get(ar[0].book);
return b?b.testament==='OT'?'ot-tinted':'nt-tinted':'';
}
/* ═══════════════════════════════════════════════════════
FORMAT RENDERERS
═══════════════════════════════════════════════════════ */
function renderList(entries,cfg){
const hasCB=cfg.checkbox,hasDate=!cfg.noDates;
let cls='list-day';
if(hasCB) cls+=' has-cb';
if(hasDate) cls+=' has-date';
let h='';
for(const e of entries){
const tc=tcls(e,cfg);
h+=`
`;
if(hasCB) h+=``;
h+=`${e.dayNum} `;
if(hasDate) h+=`${fmtDate(e.date)} `;
h+=`${readingHTML(e,cfg)} `;
if(cfg.dailyStat) h+=dayStatHTML(e);
h+=linkHTML(e,cfg);
h+=`
`;
}
return h;
}
function renderWeekly(entries,cfg){
const weeks=[];
let curKey=null,curW=null;
for(const e of entries){
const sun=new Date(e.date); sun.setDate(sun.getDate()-sun.getDay());
const key=isoDate(sun);
if(key!==curKey){curKey=key;curW={weekStart:sun,items:[]};weeks.push(curW);}
curW.items.push(e);
}
let h='';
weeks.forEach((w,i)=>{
const lbl=cfg.noDates?`Week ${i+1}`:`Week ${i+1} · ${MONTHS[w.weekStart.getMonth()].slice(0,3)} ${w.weekStart.getDate()}`;
h+=`
${lbl} ${renderList(w.items,cfg)}`;
});
return h;
}
function renderCalendar(entries,cfg){
const byDate=new Map(entries.map(e=>[isoDate(e.date),e]));
const first=entries[0].date,last=entries[entries.length-1].date;
const mStart=new Date(first.getFullYear(),first.getMonth(),1);
const mEnd=new Date(last.getFullYear(),last.getMonth(),1);
let h='';
let cur=new Date(mStart);
while(cur<=mEnd){
const y=cur.getFullYear(),m=cur.getMonth();
const mEnd2=new Date(y,m+1,0);
h+=`
${MONTHS[m]} ${y} `;
h+=`
← scroll sideways →
`;
h+=`
`;
cur.setMonth(cur.getMonth()+1);
}
return h;
}
function renderStats(s){
return`
${s.days} Reading days
${s.books} Books
${s.chapters} Chapters
${s.avgCh.toFixed(1)} Avg ch/day
${Math.round(s.estHours)} Est. hours
`;
}
/* ═══════════════════════════════════════════════════════
PLAN ID
Format: BRP-{YEAR}-{5CHARS}
BRP = Bible Reading Plan (always the same — safe to search as a prefix)
YEAR = start year of the plan (gives human context at a glance)
5CHARS = random uppercase alphanumeric, ambiguous chars excluded
(no 0/O, no 1/I/L) so it can be typed accurately from a printout
Example: BRP-2026-K4T9M
═══════════════════════════════════════════════════════ */
function generatePlanId(startDateStr){
const d=startDateStr?new Date(startDateStr+'T00:00:00'):new Date();
// YYMM encodes when the plan was generated — more compact and searchable than YYYY
const yy=String(d.getFullYear()).slice(2);
const mm=String(d.getMonth()+1).padStart(2,'0');
// Alphabet excludes visually ambiguous characters: 0 O 1 I L
const chars='ABCDEFGHJKMNPQRSTUVWXYZ23456789';
let suffix='';
for(let i=0;ic.dataset.book));
}
function renderBookGrid(){
const grid=document.getElementById('bookGrid');
let h='
Old Testament
';
BOOKS.filter(b=>b.testament==='OT').forEach(b=>{
h+=`
${b.name} `;
});
h+='
New Testament
';
BOOKS.filter(b=>b.testament==='NT').forEach(b=>{
h+=`
${b.name} `;
});
grid.innerHTML=h;
}
function syncTestament(t,checked){
document.querySelectorAll(`.book-cb[data-testament="${t}"]`).forEach(c=>c.checked=checked);
}
function syncMaster(t){
const boxes=[...document.querySelectorAll(`.book-cb[data-testament="${t}"]`)];
const n=boxes.filter(c=>c.checked).length;
const master=t==='OT'?document.getElementById('testOT'):document.getElementById('testNT');
master.checked=n>0;master.indeterminate=n>0&&nnames.has(b.name));
// Psalms/Proverbs read as a daily add-on come OUT of the main pool,
// so they aren't read twice on the same day.
if(psalmOn) books=books.filter(b=>b.name!=='Psalms');
if(proverbOn) books=books.filter(b=>b.name!=='Proverbs');
return{
count:books.length,
chapters:books.reduce((s,b)=>s+b.chapters,0),
words:books.reduce((s,b)=>s+b.words,0),
psalmOn,proverbOn,
psalmExcluded:psalmOn&&names.has('Psalms'),
proverbExcluded:proverbOn&&names.has('Proverbs')
};
}
function pacePerDay(stat,numDays){
let avgCh=stat.chapters/numDays;
let avgWordsPerDay=stat.words/numDays;
if(stat.psalmOn){
const b=BOOK_MAP.get('Psalms');
avgCh+=1; avgWordsPerDay+=b.words/b.chapters;
}
if(stat.proverbOn){
const b=BOOK_MAP.get('Proverbs');
avgCh+=1; avgWordsPerDay+=b.words/b.chapters;
}
return{avgCh,avgMin:Math.round(avgWordsPerDay/200)};
}
function updateReadingPace(){
const card=document.getElementById('paceCard');
const note=document.getElementById('psalmProverbNote');
const numDays=Math.max(1,parseInt(document.getElementById('numDays').value)||1);
const stat=selectedBookStats();
if(note) note.hidden=!(stat.psalmExcluded||stat.proverbExcluded);
if(!card) return;
const planType=(document.querySelector('[name=planType]:checked')||{}).value||'full';
if(!stat.count){
const msg=planType==='custom'
?'Pick a testament above, or fine-tune individual books, to see your chapters/day and reading time.'
:'Select at least one book to see your reading pace.';
card.innerHTML=`
${msg}
`;
const hint0=document.getElementById('booksHint');
if(hint0) hint0.textContent='Nothing selected yet — check Old Testament, New Testament, or expand "Fine-tune individual books" below.';
updateGenRecap();
return;
}
const{avgCh,avgMin}=pacePerDay(stat,numDays);
const isEntireBible=stat.count===BOOKS.length-(stat.psalmOn?1:0)-(stat.proverbOn?1:0);
const title=isEntireBible?'Bible Plan':'Custom Plan';
const scopeLabel=isEntireBible?'Entire Bible':`${stat.count} book${stat.count!==1?'s':''} selected`;
const addOns=[stat.psalmOn?'+ daily Psalm':'',stat.proverbOn?'+ daily Proverb':''].filter(Boolean).join(' ');
card.innerHTML=`
${title}
✓ ${scopeLabel}
${stat.chapters.toLocaleString()} chapters
${stat.count} book${stat.count!==1?'s':''}
${addOns?`${addOns} `:''}
${avgCh.toFixed(1)} Chapters / day
${avgMin} Minutes / day
📖 You'll read approximately ${avgCh.toFixed(1)} chapters per day . ⏱️ Estimated reading time: ${avgMin} minutes/day .
`;
const hint=document.getElementById('booksHint');
if(hint){
hint.textContent=planType==='full'
?'The Bible Plan reads through every book. Uncheck any below if you\u2019d like to leave a few out.'
:'Your custom selection so far — adjust testaments or fine-tune individual books below.';
}
updateGenRecap();
}
function updateGenRecap(){
const el=document.getElementById('genRecap');
if(!el) return;
const numDays=Math.max(1,parseInt(document.getElementById('numDays').value)||1);
const stat=selectedBookStats();
if(!stat.count){
el.textContent='Select at least one book to generate your plan.';
return;
}
const{avgCh,avgMin}=pacePerDay(stat,numDays);
const addOns=[stat.psalmOn?'a daily Psalm':'',stat.proverbOn?'a daily Proverb':''].filter(Boolean).join(' and ');
el.textContent=`${stat.count} book${stat.count!==1?'s':''} · about ${avgCh.toFixed(1)} chapters (${avgMin} min) a day`
+(addOns?`, plus ${addOns}`:'')+ '. Ready to generate.';
}
/* ═══════════════════════════════════════════════════════
TIMEFRAME LIVE CALC
═══════════════════════════════════════════════════════ */
function activeWdSet(){
return new Set([...document.querySelectorAll('.wd:checked')].map(c=>parseInt(c.value)));
}
const WD_NAMES=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
function updateWdSummary(){
const el=document.getElementById('wdSummary');
if(!el) return;
const wdSet=activeWdSet();
if(wdSet.size===7){
el.textContent='All 7 days selected. Tap a day to skip it — the plan moves to the next selected day instead.';
}else if(wdSet.size===0){
el.textContent='Select at least one day to read on.';
}else{
const skipped=WD_NAMES.filter((_,i)=>!wdSet.has(i));
el.textContent=`Reading ${wdSet.size} day${wdSet.size===1?'':'s'} a week. Skipping ${skipped.join(', ')}.`;
}
}
function recalcTimeframe(){
const mode=([...document.querySelectorAll('[name=tfMode]')].find(r=>r.checked)?.value)||'days';
const startVal=document.getElementById('startDate').value;
const info=document.getElementById('daysInfo');
updateReadingPace();
updateWdSummary();
if(!startVal){info.textContent='';return;}
if(mode==='enddate'){
const endVal=document.getElementById('endDate').value;
if(!endVal){info.textContent='';return;}
const s=new Date(startVal+'T00:00:00'),e=new Date(endVal+'T00:00:00');
if(e0?`${months} month${months!==1?'s':''}`:'';;
if(remDays>0) span+=(span?', ':'')+`${remDays} day${remDays!==1?'s':''}`;
info.textContent=`→ ${readDays} reading days (${span||'same day'} total span)`;
} else {
const n=parseInt(document.getElementById('numDays').value)||365;
const wdSet=activeWdSet();
if(!wdSet.size){info.textContent='';return;}
const dates=generateDates(startVal,n,wdSet);
if(dates.length){
const last=dates[dates.length-1];
const calDays=Math.round((last-new Date(startVal+'T00:00:00'))/864e5);
const months=Math.floor(calDays/30.44);
info.textContent=`→ finishes ${fmtDateShort(last)} (≈${months>0?months+' months':calDays+' days'} calendar span)`;
}
}
updateReadingPace();
}
/* ═══════════════════════════════════════════════════════
INIT
═══════════════════════════════════════════════════════ */
/* ═══════════════════════════════════════════════════════
LOCAL PERSISTENCE
Saves form inputs as they change and restores them on
reload, so refreshing the tab doesn't wipe your setup.
═══════════════════════════════════════════════════════ */
const STORAGE_KEY='bibleReadingPlan:formState:v1';
let storageAvailable=true;
try{
const t='__brp_test__'; localStorage.setItem(t,'1'); localStorage.removeItem(t);
}catch(e){ storageAvailable=false; }
function serializeFormState(){
const data={};
document.querySelectorAll('#planForm input[id], #planForm select[id]').forEach(el=>{
data[el.id]=(el.type==='checkbox'||el.type==='radio')?el.checked:el.value;
});
return data;
}
function saveFormState(){
if(!storageAvailable) return;
try{ localStorage.setItem(STORAGE_KEY,JSON.stringify(serializeFormState())); }
catch(e){ /* quota or private-mode failure — not critical, skip silently */ }
}
function restoreFormState(){
if(!storageAvailable) return false;
let saved;
try{
const raw=localStorage.getItem(STORAGE_KEY);
if(!raw) return false;
saved=JSON.parse(raw);
}catch(e){ return false; }
document.querySelectorAll('#planForm input[id], #planForm select[id]').forEach(el=>{
if(!(el.id in saved)) return;
if(el.type==='checkbox'||el.type==='radio') el.checked=!!saved[el.id];
else el.value=saved[el.id];
});
return true;
}
(function init(){
// Default start = tomorrow
const t=new Date(); t.setDate(t.getDate()+1);
document.getElementById('startDate').value=isoDate(t);
// Default end = 1 year later
const y=new Date(t); y.setFullYear(y.getFullYear()+1); y.setDate(y.getDate()-1);
document.getElementById('endDate').value=isoDate(y);
renderBookGrid();
const restored=restoreFormState();
recalcTimeframe();
updateReadingPace();
// Auto-save every change so a reload doesn't wipe the form
document.getElementById('planForm').addEventListener('change',saveFormState);
document.getElementById('planForm').addEventListener('input',saveFormState);
// Reading Plan type: "Bible Plan" selects every book; "Custom" leaves current selection
document.querySelectorAll('[name=planType]').forEach(r=>{
r.addEventListener('change',()=>{
const selectAll=r.value==='full'&&r.checked;
document.getElementById('testOT').checked=selectAll;
document.getElementById('testNT').checked=selectAll;
syncTestament('OT',selectAll);
syncTestament('NT',selectAll);
syncMaster('OT');syncMaster('NT');
refreshOverlap();
updateReadingPace();
});
});
// Keep the pace card current whenever book selection changes
// COMMENTED OUT TO PREVENT RACE CONDITION BUG:
// document.getElementById('bookGrid').addEventListener('change',updateReadingPace);
// document.getElementById('optPsalm').addEventListener('change',updateReadingPace);
// document.getElementById('optProverb').addEventListener('change',updateReadingPace);
// document.getElementById('testOT').addEventListener('change',updateReadingPace);
// document.getElementById('testNT').addEventListener('change',updateReadingPace);
// Timeframe mode toggle
function applyTfMode(isEnd){
const nf=document.getElementById('numDaysField');
const ef=document.getElementById('endDateField');
nf.className='field '+(isEnd?'tf-dim':'tf-active');
ef.className='field '+(isEnd?'tf-active':'tf-dim');
document.getElementById('numDays').disabled=isEnd;
document.getElementById('endDate').disabled=!isEnd;
}
applyTfMode(false); // initial state
document.querySelectorAll('[name=tfMode]').forEach(r=>{
r.addEventListener('change',()=>{
const isEnd=[...document.querySelectorAll('[name=tfMode]')].find(x=>x.checked)?.value==='enddate';
applyTfMode(isEnd);
recalcTimeframe();
});
});
// Live recalc triggers
['startDate','endDate','numDays'].forEach(id=>{
document.getElementById(id)?.addEventListener('input',recalcTimeframe);
});
document.querySelectorAll('.wd').forEach(c=>c.addEventListener('change',recalcTimeframe));
// Testament master toggles
// COMMENTED OUT TO PREVENT RACE CONDITION BUG:
// document.getElementById('testOT').addEventListener('change',e=>syncTestament('OT',e.target.checked));
// document.getElementById('testNT').addEventListener('change',e=>syncTestament('NT',e.target.checked));
// document.getElementById('bookGrid').addEventListener('change',e=>{
// if(e.target.classList.contains('book-cb')) syncMaster(e.target.dataset.testament);
// });
// Keep the pace card current whenever add-on selection changes
document.getElementById('optPsalm').addEventListener('change', updateReadingPace);
document.getElementById('optProverb').addEventListener('change', updateReadingPace);
// Testament master toggles (Sync DOM state first, THEN update the pace!)
document.getElementById('testOT').addEventListener('change', e => {
syncTestament('OT', e.target.checked);
updateReadingPace();
});
document.getElementById('testNT').addEventListener('change', e => {
syncTestament('NT', e.target.checked);
updateReadingPace();
});
document.getElementById('bookGrid').addEventListener('change', e => {
if (e.target.classList.contains('book-cb')) {
syncMaster(e.target.dataset.testament);
}
updateReadingPace();
});
// Overlap availability
function refreshOverlap(){
const names=selectedBookNames();
const hasOT=[...names].some(n=>BOOK_MAP.get(n)?.testament==='OT');
const hasNT=[...names].some(n=>BOOK_MAP.get(n)?.testament==='NT');
const el=document.getElementById('optOverlap');
el.disabled=!(hasOT&&hasNT);
if(el.disabled) el.checked=false;
}
document.getElementById('bookGrid').addEventListener('change',refreshOverlap);
document.getElementById('testOT').addEventListener('change',refreshOverlap);
document.getElementById('testNT').addEventListener('change',refreshOverlap);
// Calendar format disables noDates
document.getElementById('formatSel').addEventListener('change',e=>{
const el=document.getElementById('optNoDates');
el.disabled=(e.target.value==='calendar');
if(el.disabled) el.checked=false;
});
// If we just restored saved values, every derived/disabled UI state above
// was wired against the *default* markup — re-sync it all against what's
// actually checked/selected now.
if(restored){
const isEnd=[...document.querySelectorAll('[name=tfMode]')].find(x=>x.checked)?.value==='enddate';
applyTfMode(isEnd);
refreshOverlap();
document.getElementById('optNoDates').disabled=(document.getElementById('formatSel').value==='calendar');
recalcTimeframe();
updateReadingPace();
}
document.getElementById('resetFormLink').addEventListener('click',e=>{
e.preventDefault();
if(!confirm('Clear your saved selections and reload with defaults?')) return;
try{ localStorage.removeItem(STORAGE_KEY); }catch(err){}
location.reload();
});
})();
/* ═══════════════════════════════════════════════════════
WIZARD NAVIGATION
Single-open steps with a sticky progress bar. Moving to
a step opens it and collapses/marks the others complete.
═══════════════════════════════════════════════════════ */
(function wizard(){
const TOTAL=6;
let current=1;
let reached=1; // furthest step the user has actually advanced to via "Next"
function render(){
document.querySelectorAll('.wiz-step').forEach(el=>{
const n=parseInt(el.dataset.step,10);
const locked=n>reached;
el.classList.toggle('active',n===current);
el.classList.toggle('completed',n{
const n=parseInt(btn.dataset.goto,10);
btn.classList.toggle('current',n===current);
btn.classList.toggle('done',nreached);
btn.disabled=n>reached;
btn.setAttribute('aria-current',n===current?'step':'false');
});
document.querySelectorAll('.pconnector').forEach((c,i)=>{
c.classList.toggle('done',(i+1)reached) return; // can't skip ahead of steps you haven't reached via Next
current=n;
render();
if(typeof updateGenRecap==='function') updateGenRecap();
if(scroll){
const el=document.getElementById('step'+current);
if(el) el.scrollIntoView({behavior:'smooth',block:'start'});
}
const head=document.querySelector('#step'+current+' .step-head');
if(head) head.focus();
}
// Clicking a step header revisits an already-reached step; locked steps do nothing
document.querySelectorAll('.wiz-step .step-head').forEach(head=>{
head.addEventListener('click',()=>{
const n=parseInt(head.closest('.wiz-step').dataset.step,10);
goTo(n,false);
});
});
// Progress-bar pills work the same way — only reached steps are clickable
document.querySelectorAll('.pstep[data-goto]').forEach(btn=>{
btn.addEventListener('click',()=>goTo(parseInt(btn.dataset.goto,10),true));
});
// Next advances (and unlocks) one step further; Back only moves the cursor, nothing re-locks
document.querySelectorAll('[data-nav-next]').forEach(btn=>{
btn.addEventListener('click',()=>{
const n=parseInt(btn.dataset.navNext,10)+1;
reached=Math.max(reached,Math.min(TOTAL,n));
goTo(n,true);
});
});
document.querySelectorAll('[data-nav-back]').forEach(btn=>{
btn.addEventListener('click',()=>goTo(parseInt(btn.dataset.navBack,10)-1,true));
});
render();
})();
/* ═══════════════════════════════════════════════════════
FORM SUBMIT
═══════════════════════════════════════════════════════ */
document.getElementById('planForm').addEventListener('submit',e=>{
e.preventDefault();
const selected=selectedBookNames();
if(!selected.size){alert('Please select at least one book.');return;}
const wdays=[...document.querySelectorAll('.wd:checked')].map(c=>parseInt(c.value));
if(!wdays.length){alert('Please select at least one reading day.');return;}
const numDays=Math.max(1,Math.min(3650,parseInt(document.getElementById('numDays').value)||365));
const cfg={
startDate:document.getElementById('startDate').value,
numDays,weekdays:wdays,
order:document.getElementById('orderSel').value,
logic:document.getElementById('logicSel').value,
format:document.getElementById('formatSel').value,
checkbox:document.getElementById('optCB').checked,
colors:document.getElementById('optColors').checked,
overlap:document.getElementById('optOverlap').checked&&!document.getElementById('optOverlap').disabled,
psalm:document.getElementById('optPsalm').checked,
proverb:document.getElementById('optProverb').checked,
reverse:document.getElementById('optReverse').checked,
noDates:document.getElementById('optNoDates').checked,
showStats:document.getElementById('optStats').checked,
dailyStat:document.getElementById('optDailyStat').checked,
links:document.getElementById('optLinks').checked,
version:document.getElementById('versionSel').value,
};
const{entries,stats}=buildPlan(cfg);
// ── Plan ID (computed now, only surfaced once a .ics is actually downloaded) ──
const planId=generatePlanId(cfg.startDate);
document.getElementById('planIdCode').textContent=planId;
document.getElementById('planIdStrip').hidden=true;
// ── Header ──
const planName=document.getElementById('planName').value.trim()||'Bible Reading Plan';
document.getElementById('outTitle').textContent=planName;
const s=stats.start,en=stats.end;
document.getElementById('outRange').textContent=
s&&en?`${fmtDateLong(s)} → ${fmtDateLong(en)} · ${cfg.numDays} reading days`:'';
// ── Stats ──
const statsEl=document.getElementById('outStats');
if(cfg.showStats){statsEl.innerHTML=renderStats(stats);statsEl.hidden=false;}
else statsEl.hidden=true;
// ── Plan body ──
let planHTML='';
if(cfg.format==='calendar') planHTML=renderCalendar(entries,cfg);
else if(cfg.format==='weekly') planHTML=renderWeekly(entries,cfg);
else planHTML=renderList(entries,cfg);
const planBodyEl=document.getElementById('planBody');
planBodyEl.innerHTML=planHTML;
planBodyEl.classList.toggle('cols-2',cfg.format==='list'||cfg.format==='weekly');
// ── Show output ──
const out=document.getElementById('output');
out.hidden=false;
// ── Toolbar buttons ──
document.getElementById('btnPDF').onclick=()=>generatePDF(entries,cfg,stats);
document.getElementById('btnICS').onclick=()=>{
const fname=(planName.replace(/[^a-z0-9]/gi,'-').toLowerCase()||'bible-plan')+'.ics';
triggerDownload(fname,planToICS(entries,cfg,planId),'text/calendar;charset=utf-8');
document.getElementById('planIdStrip').hidden=false;
};
document.getElementById('btnTxt').onclick=()=>{
const fname=(planName.replace(/[^a-z0-9]/gi,'-').toLowerCase()||'bible-plan')+'.txt';
triggerDownload(fname,planToTxt(entries,cfg,planId),'text/plain;charset=utf-8');
};
document.getElementById('btnCopy').onclick=async function(){
try{
await navigator.clipboard.writeText(planToTxt(entries,cfg,planId));
const orig=this.textContent;
this.textContent='✓ Copied!';this.classList.add('btn-copied');
setTimeout(()=>{this.textContent=orig;this.classList.remove('btn-copied');},2000);
}catch{alert('Copy not supported — use the .txt download instead.');}
};
if(typeof out.scrollIntoView==='function'){
out.scrollIntoView({behavior:'smooth',block:'start'});
}
});
/* ═══════════════════════════════════════════════════════
PDF EXPORT (jsPDF only — no autotable — loaded on demand)
Renders three layouts to match the on-screen format:
calendar (real month grid), weekly, and list. Desktop
gets a standard A4 page with multiple columns to save
paper; phones get a narrow pocket-sized single column.
═══════════════════════════════════════════════════════ */
function loadScript(src){
return new Promise((resolve,reject)=>{
if(document.querySelector('script[src="'+src+'"]')){resolve();return;}
var s=document.createElement('script');
s.src=src;
s.onload=resolve;
s.onerror=function(){reject(new Error('Could not load '+src));};
document.head.appendChild(s);
});
}
async function generatePDF(entries,cfg,stats){
var btn=document.getElementById('btnPDF');
var origHTML=btn.innerHTML;
btn.disabled=true; btn.innerHTML='
⏳ Loading…';
try{
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js');
}catch(e){
btn.disabled=false; btn.innerHTML=origHTML;
alert('Could not load the PDF library — check your connection (a firewall or ad-blocker may be blocking cdnjs.cloudflare.com) and try again. The .txt and .ics downloads work fully offline in the meantime.');
return;
}
if(!window.jspdf){
btn.disabled=false; btn.innerHTML=origHTML;
alert('The PDF library did not start correctly. Please try again, or use the .txt / .ics downloads instead.');
return;
}
btn.innerHTML='
⏳ Building…';
await new Promise(function(r){setTimeout(r,10);});
try{
buildPdfDocument(entries,cfg,stats);
}catch(e){
console.error(e);
alert('Something went wrong building the PDF. Please try again, or use the .txt / .ics downloads instead.');
}
btn.innerHTML=origHTML;
btn.disabled=false;
}
function buildPdfDocument(entries,cfg,stats){
var isMobile=/Android|iPhone|iPad|iPod|Mobi/i.test(navigator.userAgent) ||
(window.matchMedia && window.matchMedia('(max-width:640px)').matches);
var W,H,ML,MR,MB,f;
if(isMobile){
W=108; H=192; ML=7; MR=7; MB=10;
f=function(pt){return pt;};
}else{
W=210; H=297; ML=16; MR=16; MB=14;
f=function(pt){return pt*1.35;};
}
var jsPDFLib=window.jspdf;
var doc=new jsPDFLib.jsPDF({orientation:'portrait',unit:'mm',format:[W,H]});
var Cink=[13,18,32], Cpaper=[248,250,252], Cgold=[201,162,39],
CgoldSoft=[254,243,199], Cot=[5,150,105], Cnt=[220,38,38],
Cmuted=[100,116,139], Cline=[226,232,240], Ctext=[15,23,42],
Csub=[200,210,225], Cgolddark=[150,110,10];
var planName=(document.getElementById('planName')?document.getElementById('planName').value:'').trim()||'Bible Reading Plan';
var pageNum=1;
function drawChrome(pg){
var headH=f(22);
doc.setFillColor(Cink[0],Cink[1],Cink[2]);
doc.rect(0,0,W,headH,'F');
doc.setFont('helvetica','normal');
doc.setFontSize(f(5));
doc.setTextColor(Cgold[0],Cgold[1],Cgold[2]);
doc.text('PENTECOST INTERNATIONAL · BIBLE READING PLAN',W/2,f(5),{align:'center'});
doc.setFont('helvetica','bold');
doc.setFontSize(f(11));
doc.setTextColor(Cpaper[0],Cpaper[1],Cpaper[2]);
var titleLine=doc.splitTextToSize(planName,W-f(10))[0];
doc.text(titleLine,W/2,f(12),{align:'center'});
if(stats.start&&stats.end){
doc.setFont('helvetica','normal');
doc.setFontSize(f(6));
doc.setTextColor(Csub[0],Csub[1],Csub[2]);
doc.text(
fmtDateShort(stats.start)+' – '+fmtDateShort(stats.end)+' · '+stats.days+' days',
W/2,f(18),{align:'center'}
);
}
doc.setDrawColor(Cgold[0],Cgold[1],Cgold[2]);
doc.setLineWidth(0.3);
doc.line(0,headH,W,headH);
var contentTop=headH+3;
if(pg===1){
var barH=f(11);
doc.setFillColor(CgoldSoft[0],CgoldSoft[1],CgoldSoft[2]);
doc.rect(0,headH,W,barH,'F');
var items=[
{v:String(stats.books),l:'Books'},
{v:String(stats.chapters),l:'Chap.'},
{v:stats.avgCh.toFixed(1),l:'Avg/day'},
{v:String(Math.round(stats.estHours)),l:'Hours'}
];
var sw=W/items.length;
items.forEach(function(it,i){
var x=sw*i+sw/2;
doc.setFont('helvetica','bold');
doc.setFontSize(f(9));
doc.setTextColor(Cink[0],Cink[1],Cink[2]);
doc.text(it.v,x,headH+f(6),{align:'center'});
doc.setFont('helvetica','normal');
doc.setFontSize(f(5));
doc.setTextColor(Cmuted[0],Cmuted[1],Cmuted[2]);
doc.text(it.l.toUpperCase(),x,headH+f(9.5),{align:'center'});
});
doc.setDrawColor(Cgold[0],Cgold[1],Cgold[2]);
doc.setLineWidth(0.2);
doc.line(0,headH+barH,W,headH+barH);
contentTop=headH+barH+4;
}
var fy=H-f(4.5);
doc.setDrawColor(Cgold[0],Cgold[1],Cgold[2]);
doc.setLineWidth(0.2);
doc.line(ML,fy-f(2.5),W-MR,fy-f(2.5));
doc.setFont('helvetica','normal');
doc.setFontSize(f(5.5));
doc.setTextColor(Cmuted[0],Cmuted[1],Cmuted[2]);
doc.text(planName,ML,fy);
doc.text('Page '+pg,W/2,fy,{align:'center'});
doc.text('pentecostinternational.se',W-MR,fy,{align:'right'});
return contentTop;
}
var contentTop=drawChrome(1);
var contentBottom=H-f(10);
function newPage(){
doc.addPage();
pageNum++;
return drawChrome(pageNum);
}
function readingLine(e){
var ar=allRanges(e);
var reading=formatRanges(ar).replace(/\u2013/g,'-');
if(e.psalm) reading+='; Ps '+e.psalm;
if(e.proverb) reading+='; Prov '+e.proverb;
return reading;
}
function groupEntries(groupBy){
var groups=[], curKey=null, curG=null;
entries.forEach(function(e){
var key,label;
if(groupBy==='week'){
var sun=new Date(e.date); sun.setDate(sun.getDate()-sun.getDay());
key=isoDate(sun);
label='Week of '+fmtDateShort(sun);
}else{
key=e.date.getFullYear()+'-'+e.date.getMonth();
label=MONTHS[e.date.getMonth()]+' '+e.date.getFullYear();
}
if(key!==curKey){curKey=key; curG={label:label,items:[]}; groups.push(curG);}
curG.items.push(e);
});
return groups;
}
function renderPdfList(groupBy){
var columns=isMobile?1:3;
var colGap=f(6);
var usableW=W-ML-MR;
var colW=(usableW-colGap*(columns-1))/columns;
var colX=[]; for(var c=0;ccontentBottom){
col++;
if(col>=columns){ contentTop=newPage(); col=0; y=contentTop; }
else y=contentTop;
}
}
groupEntries(groupBy).forEach(function(g){
ensure(f(8));
doc.setFont('helvetica','bold'); doc.setFontSize(f(8.5));
doc.setTextColor(Cink[0],Cink[1],Cink[2]);
doc.text(g.label,colX[col],y);
y+=f(3);
doc.setDrawColor(Cgold[0],Cgold[1],Cgold[2]); doc.setLineWidth(0.15);
doc.line(colX[col],y,colX[col]+colW,y);
y+=f(3.4);
g.items.forEach(function(e){
var reading=readingLine(e);
var dayLabel=cfg.noDates?('Day '+e.dayNum):fmtDateShort(e.date);
doc.setFont('helvetica','normal'); doc.setFontSize(rowFont);
var lines=doc.splitTextToSize(reading,colW-gutter);
ensure(lineH*lines.length+f(0.6));
if(cfg.colors){
var ar=allRanges(e);
var b=ar.length?BOOK_MAP.get(ar[0].book):null;
if(b){
var bc=b.testament==='OT'?Cot:Cnt;
doc.setFillColor(bc[0],bc[1],bc[2]);
doc.rect(colX[col]-f(1.6),y-f(2.6),f(0.9),lineH*lines.length,'F');
}
}
doc.setFont('helvetica','bold'); doc.setFontSize(rowFont);
doc.setTextColor(Cgolddark[0],Cgolddark[1],Cgolddark[2]);
doc.text(dayLabel,colX[col],y);
doc.setFont('helvetica','normal'); doc.setFontSize(rowFont);
doc.setTextColor(Ctext[0],Ctext[1],Ctext[2]);
doc.text(lines,colX[col]+gutter,y);
y+=lineH*lines.length+f(0.6);
});
y+=f(2.8);
});
}
function renderPdfCalendar(){
var byDate=new Map(entries.map(function(e){return[isoDate(e.date),e];}));
var first=entries[0].date, last=entries[entries.length-1].date;
var mStart=new Date(first.getFullYear(),first.getMonth(),1);
var mEnd=new Date(last.getFullYear(),last.getMonth(),1);
var usableW=W-ML-MR;
var cellW=usableW/7;
var cellH=f(20);
var y=contentTop;
var cur=new Date(mStart);
var firstMonth=true;
while(curcontentBottom){ contentTop=newPage(); y=contentTop; }
firstMonth=false;
doc.setFont('helvetica','bold'); doc.setFontSize(f(11));
doc.setTextColor(Cink[0],Cink[1],Cink[2]);
doc.text(MONTHS[m]+' '+yr,ML,y+f(4));
y+=f(7);
doc.setFont('helvetica','bold'); doc.setFontSize(f(6));
doc.setTextColor(Cmuted[0],Cmuted[1],Cmuted[2]);
DOW_SHORT.forEach(function(d,i){
doc.text(d.toUpperCase(),ML+i*cellW+cellW/2,y,{align:'center'});
});
y+=f(2.5);
var gridTop=y;
for(var r=0;r<rows;r++){
for(var c=0;c=1&&dayIdxmaxLines) lines=lines.slice(0,maxLines);
doc.text(lines,cx+f(1.3),cy+f(7));
}else if(dt>=first&&dt<=last){
doc.setFont('helvetica','italic'); doc.setFontSize(f(5.2));
doc.setTextColor(Cmuted[0],Cmuted[1],Cmuted[2]);
doc.text('Rest day',cx+f(1.3),cy+f(7));
}
}
}
}
y=gridTop+rows*cellH+f(6);
cur.setMonth(cur.getMonth()+1);
}
}
if(cfg.format==='calendar'&&!isMobile) renderPdfCalendar();
else if(cfg.format==='weekly') renderPdfList('week');
else renderPdfList('month');
var safeName=planName.replace(/[^a-z0-9]/gi,'-').toLowerCase()||'bible-plan';
doc.save(safeName+(isMobile?'-pocket':'-a4')+'.pdf');
}