Bible Reading Plan Generator

Set your dates, choose your books, download your plan — including a calendar file you can import straight into your phone.

/* ═══ 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} }

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

All 7 days selected. Tap a day to skip it — the plan moves to the next selected day instead.

Choose how much of the Bible to read. You'll pick specific books next.

Fine-tune individual books ▸

Set your reading order and daily pace.

Traditional (canonical) Chronological (approximate)
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.

Calendar List Weekly
Requires both testaments selected

ESVNIVKJVNKJV GNTCSBMSG

📅 Calendar (.ics) export settings

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.

You'll be able to download it as a PDF, plain text, or a calendar (.ics) file next.

/* ═══════════════════════════════════════════════════════ 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+=`NT — ${formatRanges(e.ntRanges)}`; } if(e.psalm) h+=`Psalm ${e.psalm}`; if(e.proverb) h+=`Proverbs ${e.proverb}`; 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+=`
`; DOW_SHORT.forEach(d=>h+=`
${d}
`); const lead=new Date(y,m,1).getDay(); for(let i=0;i<lead;i++) h+=`
`; for(let day=1;day=new Date(first.getFullYear(),first.getMonth(),first.getDate())&& dt<=new Date(last.getFullYear(),last.getMonth(),last.getDate()); if(e){ const tc=calTcls(e,cfg); h+=`
`; if(cfg.checkbox) h+=``; h+=`${day}`; h+=`Day ${e.dayNum}`; // Reading let reading=''; if(e.ranges) reading=formatRanges(e.ranges); else reading=`OT ${formatRanges(e.otRanges)}
NT ${formatRanges(e.ntRanges)}`; if(e.psalm) reading+=`
Psalm ${e.psalm}`; if(e.proverb) reading+=`
Prov ${e.proverb}`; h+=`${reading}`; // Link in calendar cell (includes psalm/proverb) if(cfg.links){ const ar=allRangesForLink(e); const u=bgUrlHtml(ar,cfg.version); if(u) h+=`Read →`; } h+=`
`; } else { h+=`
`; h+=`${day}`; if(inRange) h+=`Rest day`; h+=`
`; } } const trail=(7-((lead+mEnd2.getDate())%7))%7; for(let i=0;i<trail;i++) h+=`
`; 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+=``; }); h+='
New Testament
'; BOOKS.filter(b=>b.testament==='NT').forEach(b=>{ h+=``; }); 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'); }

Your word is a lamp for my feet, a light on my path

Psalm 119:105