{% comment %}
  Daily Word  v1.0
  Lava Application: daily-word   Page block: Lava Application Content
  Views in this pass: Setup + Today. Day reader and audio are not included;
  they are blocked on the API.Bible proxy decision.

  Enabled Lava Commands on the BLOCK: none required.
  Writes go to the save-plan endpoint, which owns its own Sql permission.

  All CSS and JS live inside a raw block so Lava never parses them. Verified
  zero occurrences of double-brace and Lava tag sequences in that region.
{% endcomment %}

{% if CurrentPerson %}

<script type="application/json" id="dw-plan-data">
{{ CurrentPerson | Attribute:'DailyWordPlan','RawValue' | Default:'null' }}
</script>

{% raw %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Newsreader:ital,wght@0,400;1,400&family=Work+Sans:wght@300;400;600;700&display=swap">
<style>
#dw-root {
  --forest: #165B48; --forest-700: #0F4536; --forest-200: #A6C6BC; --forest-50: #EEF5F2;
  --navy: #091B34; --sage: #5F9B8C; --coral: #E6633F; --taupe: #B0958D;
  --page: #FBF6F2; --card: #FFFFFF; --bd: #E7E0D9; --bd-soft: #EFEAE5;
  --g50: #F7F8F8; --g100: #EDEFF0; --g400: #C9CFD5; --muted: #6E7883;
  --disp: "Newsreader", Georgia, serif;
  --ui: "Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-family: var(--ui); color: var(--navy); background: var(--page);
  letter-spacing: -0.006em; display: block; min-height: 100vh;
}
#dw-root *, #dw-root *::before, #dw-root *::after { box-sizing: border-box; }
#dw-root button { font: inherit; color: inherit; cursor: pointer; }

.dw-head { position: sticky; top: 0; z-index: 20; background: rgba(251,246,242,.86);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--bd); }
.dw-head-in, .dw-foot-in, .dw-main { max-width: 1140px; margin: 0 auto; padding: 0 22px; }
.dw-head-in { display: flex; align-items: center; justify-content: space-between;
  gap: 18px; min-height: 62px; }
.dw-brand { display: flex; align-items: center; gap: 12px; }
.dw-wordmark { font-family: var(--disp); font-size: 22px; letter-spacing: -0.028em; }
.dw-rule { width: 18px; height: 1px; background: var(--sage); }
.dw-tag { font-weight: 300; font-size: 13px; color: var(--muted); }
.dw-nav { display: flex; gap: 22px; }
.dw-tab { background: none; border: 0; border-bottom: 2px solid transparent;
  padding: 4px 0; font-size: 14px; color: var(--muted); }
.dw-tab.is-on { color: var(--forest); font-weight: 600; border-bottom-color: var(--forest); }

.dw-main { padding-top: 44px; padding-bottom: 130px; }
.dw-hero, .dw-masthead { max-width: 720px; }
.dw-eyebrow { font-size: 12px; font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; margin: 34px 0 13px; }
.dw-sage { color: var(--sage); } .dw-taupe { color: var(--taupe); }
.dw-hero .dw-eyebrow { margin-top: 0; }
.dw-h1 { font-family: var(--disp); font-weight: 400; letter-spacing: -0.028em;
  font-size: clamp(40px, 7vw, 62px); line-height: 1.02; margin: 0 0 20px; }
.dw-h1 em { font-style: italic; }
.dw-lede { font-weight: 300; font-size: 19px; line-height: 1.65; color: var(--muted);
  max-width: 46ch; margin: 0; text-wrap: pretty; }

.dw-card { display: flex; flex-direction: column; gap: 1px; background: var(--bd);
  border: 1px solid var(--bd); border-radius: 18px; overflow: hidden;
  box-shadow: 0 2px 6px rgba(9,27,52,.05); margin-bottom: 34px; }
.dw-row { background: var(--card); padding: 20px 22px; display: flex;
  align-items: center; gap: 18px; flex-wrap: wrap; }
.dw-row-main { flex: 1; min-width: 190px; }
.dw-secname { font-family: var(--disp); font-size: 21px; letter-spacing: -0.028em; }
.dw-cap { font-size: 13px; color: var(--g400); margin-top: 4px; display: block; }
.dw-step { display: flex; align-items: center; gap: 4px; }
.dw-ghost { width: 44px; height: 44px; border-radius: 999px; border: 0;
  background: none; color: var(--muted); font-size: 17px; line-height: 1; }
.dw-ghost:hover { background: var(--g100); color: var(--navy); }
.dw-freq { min-width: 124px; text-align: center; }
.dw-freq-n { font-weight: 700; font-size: 15px; color: var(--forest); }
.dw-x { border: 0; background: none; color: var(--g400); font-size: 19px; padding: 6px 4px; }
.dw-x:hover { color: var(--coral); }
.dw-empty, .dw-rest { font-family: var(--disp); font-style: italic; font-size: 18px;
  color: var(--muted); }

.dw-twoup { margin-bottom: 10px; }
.dw-select { font: inherit; font-size: 15px; padding: 12px 14px; border: 1px solid var(--bd);
  border-radius: 10px; background: var(--card); color: var(--navy); min-width: 300px;
  max-width: 100%; }
.dw-chips { display: flex; flex-wrap: wrap; gap: 9px; }
.dw-chip { border: 1px solid var(--forest-200); background: var(--card);
  color: var(--forest); border-radius: 999px; padding: 9px 15px; font-size: 14px; }
.dw-chip:hover { background: var(--forest-50); }
.dw-chip.is-used { background: transparent; border-color: var(--bd-soft); color: var(--g400); }

.dw-actionbar { position: sticky; bottom: 18px; margin-top: 40px; display: flex;
  align-items: center; justify-content: space-between; gap: 18px; flex-wrap: wrap;
  background: rgba(251,246,242,.9); backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px); border: 1px solid var(--bd);
  border-radius: 16px; padding: 14px 16px; box-shadow: 0 6px 22px rgba(9,27,52,.08); }
.dw-summary { font-size: 14px; color: var(--muted); }
.dw-primary { background: var(--forest); color: #fff; border: 0; border-radius: 10px;
  padding: 13px 22px; font-weight: 600; font-size: 15px; min-height: 44px; }
.dw-primary:hover { background: var(--forest-700); }

.dw-mast-row { display: flex; align-items: flex-end; justify-content: space-between;
  gap: 18px; border-bottom: 1px solid var(--bd); padding-bottom: 10px; }
.dw-date { font-family: var(--disp); font-weight: 400; letter-spacing: -0.028em;
  font-size: clamp(36px, 8vw, 58px); line-height: 1.02; margin: 0; }
.dw-daynav { display: flex; gap: 8px; padding-bottom: 6px; }
.dw-round { width: 44px; height: 44px; border-radius: 999px; border: 1px solid var(--bd);
  background: transparent; color: var(--muted); }
.dw-round:hover { background: var(--g50); color: var(--navy); }
.dw-dayof { margin: 10px 0 0; }

.dw-read { background: var(--card); padding: 18px 22px; display: flex;
  align-items: flex-start; gap: 15px; cursor: pointer; }
.dw-read:hover { filter: brightness(.985); }
.dw-read.is-done { background: var(--g50); }
.dw-check { flex: none; width: 24px; height: 24px; border-radius: 999px;
  border: 1.5px solid var(--forest-200); display: inline-flex; align-items: center;
  justify-content: center; color: #fff; font-size: 13px; margin-top: 4px; }
.dw-check.is-on { background: var(--forest); border-color: var(--forest); }
.dw-check.dw-small { width: 18px; height: 18px; margin-top: 2px; }
.dw-read-main { display: block; }
.dw-stream { display: block; font-size: 11px; font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; color: var(--taupe); margin-bottom: 4px; }
.dw-passage { display: block; font-family: var(--disp); letter-spacing: -0.028em;
  font-size: clamp(21px, 4.4vw, 27px); line-height: 1.15; }
.dw-read.is-done .dw-passage { text-decoration: line-through; color: var(--g400); }

.dw-stats { display: grid; grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--bd); border-bottom: 1px solid var(--bd-soft); }
.dw-stat { padding: 20px 10px; text-align: center; border-left: 1px solid var(--bd-soft); }
.dw-stat:first-child { border-left: 0; }
.dw-num { font-family: var(--disp); font-size: 34px; line-height: 1; }
.dw-slab { font-size: 11px; font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; color: var(--taupe); margin-top: 7px; }
.dw-bar { height: 3px; background: var(--g100); }
.dw-bar span { display: block; height: 100%; background: var(--forest);
  transition: width 280ms cubic-bezier(.23,1,.32,1); }

.dw-catch { border-top: 1px solid var(--bd-soft); }
.dw-cu { display: flex; gap: 12px; align-items: flex-start; padding: 13px 2px;
  border-bottom: 1px solid var(--bd-soft); cursor: pointer; }
.dw-cu:hover { filter: brightness(.985); }
.dw-cu-ref { font-family: var(--disp); font-size: 17px; display: block; }
.dw-more { padding: 12px 2px; }

.dw-foot { background: var(--navy); color: #fff; margin-top: 60px; }
.dw-foot-in { display: flex; align-items: center; justify-content: space-between;
  gap: 18px; min-height: 64px; font-size: 13px; }
.dw-save { color: rgba(255,255,255,.62); }
.dw-credit { color: rgba(255,255,255,.62); }

.dw-signin { max-width: 560px; margin: 90px auto; text-align: left; padding: 0 22px; }

@media (max-width: 640px) {
  .dw-main { padding-top: 30px; }
  .dw-row, .dw-read { padding: 16px 16px; }
  .dw-step { width: 100%; justify-content: space-between; }
}
@media (prefers-reduced-motion: reduce) {
  #dw-root * { transition-duration: 0.01ms !important; }
}
</style>

<div id="dw-root"></div>

<script>
(function () {
  "use strict";
  var DW_SAVE_URL = "/api/v2/lava-app/1/daily-word/save-plan";
  var DW_MIN_PER_CHAPTER = 4;

class DWCore {
  constructor(props) {
    this.props = props || {};
    this.API = "https://rest.api.bible/v1";
    this.TEXT_BIBLE = "78a9f6124f344018-01"; // NIV 2011
    this.BOOKS = [
      ["Genesis",50],["Exodus",40],["Leviticus",27],["Numbers",36],["Deuteronomy",34],
      ["Joshua",24],["Judges",21],["Ruth",4],["1 Samuel",31],["2 Samuel",24],
      ["1 Kings",22],["2 Kings",25],["1 Chronicles",29],["2 Chronicles",36],["Ezra",10],
      ["Nehemiah",13],["Esther",10],["Job",42],["Psalms",150],["Proverbs",31],
      ["Ecclesiastes",12],["Song of Songs",8],["Isaiah",66],["Jeremiah",52],["Lamentations",5],
      ["Ezekiel",48],["Daniel",12],["Hosea",14],["Joel",3],["Amos",9],
      ["Obadiah",1],["Jonah",4],["Micah",7],["Nahum",3],["Habakkuk",3],
      ["Zephaniah",3],["Haggai",2],["Zechariah",14],["Malachi",4],
      ["Matthew",28],["Mark",16],["Luke",24],["John",21],["Acts",28],
      ["Romans",16],["1 Corinthians",16],["2 Corinthians",13],["Galatians",6],["Ephesians",6],
      ["Philippians",4],["Colossians",4],["1 Thessalonians",5],["2 Thessalonians",3],["1 Timothy",6],
      ["2 Timothy",4],["Titus",3],["Philemon",1],["Hebrews",13],["James",5],
      ["1 Peter",5],["2 Peter",3],["1 John",5],["2 John",1],["3 John",1],
      ["Jude",1],["Revelation",22]
    ];
    const names = this.BOOKS.map(b => b[0]);
    const range = (a, b) => names.slice(names.indexOf(a), names.indexOf(b) + 1);
    this.PRESETS = [
      { name: "Whole Bible", books: names },
      { name: "Old Testament", books: range("Genesis","Malachi") },
      { name: "New Testament", books: range("Matthew","Revelation") },
      { name: "Pentateuch", books: range("Genesis","Deuteronomy") },
      { name: "OT history", books: range("Joshua","Esther") },
      { name: "Wisdom & poetry", books: range("Job","Song of Songs") },
      { name: "Psalms", books: ["Psalms"] },
      { name: "Proverbs", books: ["Proverbs"] },
      { name: "Prophets", books: range("Isaiah","Malachi") },
      { name: "Major prophets", books: range("Isaiah","Daniel") },
      { name: "Minor prophets", books: range("Hosea","Malachi") },
      { name: "Gospels", books: range("Matthew","John") },
      { name: "Acts", books: ["Acts"] },
      { name: "Paul's letters", books: range("Romans","Philemon") },
      { name: "General letters", books: range("Hebrews","Jude") },
      { name: "Epistles", books: range("Romans","Jude") },
      { name: "Revelation", books: ["Revelation"] }
    ];
    this.FREQ_STEPS = [0.5, 1, 2, 3, 4, 6, 12, 24, 52];
    this.TEMPLATES = [
      { name: "Whole Bible in a year", desc: "Cover to cover — about 3¼ chapters a day.", sels: [
        { name: "Whole Bible", baseName: "Whole Bible", books: names, freq: 1 }] },
      // M'Cheyne's 1842 calendar is four parallel bookmarks, not three streams
      // weighted by frequency. Each track is ~365-410 chapters read once, which
      // is why the original has a reading in every track every day. Modelling it
      // as "Psalms x2" instead left 65 days with no psalm. Book ORDER inside a
      // track is load-bearing here: dayChapters walks sel.books as given, so
      // placing Psalms mid-track is what makes the tracks open on Gen 1 / Mt 1 /
      // Ezra 1 / Acts 1 and close on 2 Chr 36 / Rev 22 / Mal 4 / Jn 21, exactly
      // as the printed calendar does.
      { name: "M'Cheyne", desc: "The classic 1842 Scottish calendar: four bookmarks — OT once, NT and Psalms twice — about 4 chapters a day.", sels: [
        { name: "Family · Genesis onward", baseName: "M'Cheyne family 1",
          books: range("Genesis","2 Chronicles"), freq: 1 },
        { name: "Family · Matthew onward", baseName: "M'Cheyne family 2",
          books: [...range("Matthew","John"), "Psalms", ...range("Acts","Revelation")], freq: 1 },
        { name: "Secret · Ezra onward", baseName: "M'Cheyne secret 1",
          books: range("Ezra","Malachi").filter(n => n !== "Psalms"), freq: 1 },
        { name: "Secret · Acts onward", baseName: "M'Cheyne secret 2",
          books: [...range("Acts","Revelation"), "Psalms", ...range("Matthew","John")], freq: 1 }] },
      { name: "New Testament in a year", desc: "A gentle pace — one chapter most days.", sels: [
        { name: "New Testament", baseName: "New Testament", books: range("Matthew","Revelation"), freq: 1 }] },
      { name: "Psalms & Proverbs monthly", desc: "The historic wisdom habit: five psalms and a proverb a day, twelve times a year.", sels: [
        { name: "Psalms", baseName: "Psalms", books: ["Psalms"], freq: 12 },
        { name: "Proverbs", baseName: "Proverbs", books: ["Proverbs"], freq: 12 }] },
      { name: "Gospels every season", desc: "Live in the life of Jesus — all four Gospels, four times a year.", sels: [
        { name: "Gospels", baseName: "Gospels", books: range("Matthew","John"), freq: 4 }] },
      { name: "Whole Bible in 90 days", desc: "Intensive: about 13 chapters a day, four passes in a year.", sels: [
        { name: "Whole Bible", baseName: "Whole Bible", books: names, freq: 4 }] }
    ];
    const saved = (props && props.saved) || null;
    this.state = saved || {
      view: "setup",
      selections: [
        { name: "Psalms", books: ["Psalms"], freq: 3 },
        { name: "Gospels", books: range("Matthew","John"), freq: 4 },
        { name: "Acts", books: ["Acts"], freq: 12 },
        { name: "Rest of OT", baseName: "Old Testament", books: range("Genesis","Malachi").filter(n => n !== "Psalms"), freq: 0.5 }
      ],
      startDate: null, checks: {}, viewDay: 0
    };
    if (this.state.follow === undefined) this.state.follow = true;
  }

  chaptersOf(books) { return books.reduce((s, n) => s + this.BOOKS.find(b => b[0] === n)[1], 0); }


  addSelection(name, books, freq) {
    const short = { "Old Testament": "OT", "New Testament": "NT" };
    const restName = (base) => "Rest of " + (short[base] || base);
    let newBooks = [...books];
    const sels = this.state.selections.map(s => ({ ...s }));
    sels.forEach(sel => {
      if (!sel.books.some(b => newBooks.includes(b))) return;
      if (this.chaptersOf(sel.books) > this.chaptersOf(newBooks)) {
        const base = sel.baseName || sel.name;
        sel.books = sel.books.filter(b => !newBooks.includes(b));
        sel.name = restName(base);
        sel.baseName = base;
      } else {
        newBooks = newBooks.filter(b => !sel.books.includes(b));
      }
    });
    if (!newBooks.length) return;
    const label = newBooks.length < books.length ? restName(name) : name;
    this.editSt({ selections: [...sels.filter(s => s.books.length), { name: label, baseName: name, books: newBooks, freq }] });
  }

  todayIndex(startDate) {
    const ms = Date.now() - new Date(startDate + "T00:00:00").getTime();
    return Math.max(0, Math.min(364, Math.floor(ms / 86400000)));
  }

  dayChapters(sel, i) {
    const seq = [];
    sel.books.forEach(n => {
      const c = this.BOOKS.find(b => b[0] === n)[1];
      for (let k = 1; k <= c; k++) seq.push({ book: n, ch: k });
    });
    const C = seq.length, total = Math.round(C * sel.freq);
    if (!total) return [];
    const a = Math.floor(i * total / 365), b = Math.floor((i + 1) * total / 365);
    const out = [];
    for (let k = a; k < b; k++) out.push(seq[k % C]);
    return out;
  }

  abbrev(book) {
    if (!this.ABBR) this.ABBR = { "Genesis":"Gn","Exodus":"Ex","Leviticus":"Lv","Numbers":"Nm","Deuteronomy":"Dt","Joshua":"Jos","Judges":"Jdg","Ruth":"Ru","1 Samuel":"1Sm","2 Samuel":"2Sm","1 Kings":"1Kg","2 Kings":"2Kg","1 Chronicles":"1Ch","2 Chronicles":"2Ch","Ezra":"Ezr","Nehemiah":"Neh","Esther":"Est","Job":"Job","Psalms":"Ps","Proverbs":"Prv","Ecclesiastes":"Ecc","Song of Songs":"Sng","Isaiah":"Isa","Jeremiah":"Jer","Lamentations":"Lam","Ezekiel":"Ezk","Daniel":"Dan","Hosea":"Hos","Joel":"Joel","Amos":"Am","Obadiah":"Ob","Jonah":"Jon","Micah":"Mic","Nahum":"Nah","Habakkuk":"Hab","Zephaniah":"Zep","Haggai":"Hag","Zechariah":"Zec","Malachi":"Mal","Matthew":"Mt","Mark":"Mk","Luke":"Lk","John":"Jn","Acts":"Ac","Romans":"Rom","1 Corinthians":"1Co","2 Corinthians":"2Co","Galatians":"Gal","Ephesians":"Eph","Philippians":"Php","Colossians":"Col","1 Thessalonians":"1Th","2 Thessalonians":"2Th","1 Timothy":"1Tm","2 Timothy":"2Tm","Titus":"Ti","Philemon":"Phm","Hebrews":"Heb","James":"Jas","1 Peter":"1Pt","2 Peter":"2Pt","1 John":"1Jn","2 John":"2Jn","3 John":"3Jn","Jude":"Jud","Revelation":"Rev" };
    return this.ABBR[book] || book;
  }

  formatChapters(chs, abbr) {
    if (!chs.length) return "";
    const parts = [];
    let start = chs[0], prev = chs[0];
    const flush = () => {
      const bk = abbr ? this.abbrev(start.book) : (start.book === "Psalms" ? "Psalm" : start.book);
      const one = this.BOOKS.find(b => b[0] === start.book)[1] === 1;
      if (one) parts.push(bk);
      else if (start.ch === prev.ch) parts.push(bk + " " + start.ch);
      else parts.push(bk + " " + start.ch + "–" + prev.ch);
    };
    for (let i = 1; i < chs.length; i++) {
      const c = chs[i];
      if (c.book === prev.book && c.ch === prev.ch + 1) { prev = c; continue; }
      flush(); start = c; prev = c;
    }
    flush();
    return parts.join("; ");
  }

  freqLabel(f) {
    if (f <= 0.5) return "every 2 years";
    if (f === 1) return "once a year";
    if (f === 12) return "once a month";
    if (f === 52) return "once a week";
    return f + "× a year";
  }

  streak(todayIdx) {
    const dayDone = (i) => {
      let sched = 0, chk = 0;
      this.state.selections.forEach((sel, si) => { if (this.dayChapters(sel, i).length) { sched++; if (this.state.checks["d" + i + "s" + si]) chk++; } });
      return sched === 0 ? null : chk === sched;
    };
    let n = 0, i = todayIdx;
    if (dayDone(i) === false) i--;
    for (; i >= 0; i--) { const d = dayDone(i); if (d === false) break; if (d === true) n++; }
    return n;
  }

  dateOf(i) {
    const d = new Date(this.state.startDate + "T00:00:00");
    d.setDate(d.getDate() + i);
    return d;
  }

}

/* ------------------------------------------------------------------ *
 * Daily Word - app layer (Rock)
 * Wraps the verified scheduling core. Render-to-HTML-string functions
 * replace the DC template and renderVals(). No framework.
 * ------------------------------------------------------------------ */

class DailyWord extends DWCore {
  constructor(props) {
    super(props);
    this.saveTimer = null;
    this.saveState = "idle";
    this.root = document.getElementById("dw-root");
  }

  /* ---------- persistence (the only Rock-facing surface) ---------- */

  setSt(patch) { Object.assign(this.state, patch); this.render(); this.queueSave(); }
  setLocal(patch) { Object.assign(this.state, patch); this.render(); }

  queueSave() {
    clearTimeout(this.saveTimer);
    this.saveState = "pending";
    this.paintSaveNotice();
    this.saveTimer = setTimeout(() => this.save(), 1500);
  }

  save() {
    const s = this.state;
    const payload = {
      selections: s.selections, startDate: s.startDate,
      checks: s.checks, viewDay: s.viewDay, view: s.view
    };
    this.saveState = "saving";
    this.paintSaveNotice();
    fetch(DW_SAVE_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json",
                 "X-Helix-CSRF-Protection": "true" },
      body: JSON.stringify({ plan: JSON.stringify(payload) })
    })
      .then(r => r.text())
      .then(txt => {
        let body = null;
        try { body = JSON.parse(txt); } catch (e) { body = null; }
        this.saveState = (body && body.ok) ? "saved" : "error";
        this.paintSaveNotice();
      })
      .catch(() => { this.saveState = "error"; this.paintSaveNotice(); });
  }

  paintSaveNotice() {
    const el = document.getElementById("dw-save-notice");
    if (!el) return;
    const map = {
      idle: "", pending: "Saving\u2026", saving: "Saving\u2026",
      saved: "All changes saved", error: "Could not save \u2014 check your connection"
    };
    el.textContent = map[this.saveState] || "";
    el.style.color = this.saveState === "error" ? "#E6633F" : "rgba(255,255,255,.62)";
  }

  /* ---------- helpers ---------- */

  esc(s) {
    return String(s == null ? "" : s)
      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  pace(sel) {
    const per = this.chaptersOf(sel.books) * sel.freq / 365;
    return per >= 1
      ? "\u2248" + (Math.round(per * 10) / 10) + " ch/day"
      : "\u2248" + (Math.round(per * 70) / 10) + " ch/week";
  }

  dayLoad(i) {
    return this.state.selections.reduce((a, s) => a + this.dayChapters(s, i).length, 0);
  }

  summaryLine() {
    const total = this.state.selections.reduce(
      (a, s) => a + Math.round(this.chaptersOf(s.books) * s.freq), 0);
    const perDay = total / 365;
    const mins = Math.round(perDay * DW_MIN_PER_CHAPTER);
    if (!total) return "Nothing selected yet.";
    return "About " + (Math.round(perDay * 10) / 10) + " chapters a day ("
      + mins + " min)";
  }

  /* ---------- boot ---------- */

  start() {
    const s = this.state;
    if (s.startDate) s.viewDay = this.todayIndex(s.startDate);
    if (!s.startDate) s.view = "setup";
    this.bind();
    this.render();
  }

  bind() {
    this.root.addEventListener("click", (ev) => {
      const el = ev.target.closest("[data-act]");
      if (!el) return;
      const act = el.getAttribute("data-act");
      const i = parseInt(el.getAttribute("data-i"), 10);
      this.dispatch(act, i, el);
    });
    this.root.addEventListener("change", (ev) => {
      const el = ev.target.closest("[data-act]");
      if (!el) return;
      this.dispatch(el.getAttribute("data-act"), -1, el);
    });
  }

  dispatch(act, i, el) {
    const s = this.state;
    const steps = this.FREQ_STEPS;

    if (act === "nav") { this.setLocal({ view: el.getAttribute("data-view") }); return; }

    if (act === "inc" || act === "dec") {
      const sel = s.selections[i];
      const at = Math.max(0, steps.indexOf(sel.freq));
      const next = act === "inc" ? Math.min(steps.length - 1, at + 1) : Math.max(0, at - 1);
      const sels = s.selections.slice();
      sels[i] = Object.assign({}, sel, { freq: steps[next] });
      this.setLocal({ selections: sels });
      return;
    }
    if (act === "remove") {
      const sels = s.selections.slice(); sels.splice(i, 1);
      this.setLocal({ selections: sels });
      return;
    }
    if (act === "preset") {
      const p = this.PRESETS[i];
      this.addSelection(p.name, p.books, 1);
      this.render();
      return;
    }
    if (act === "template") {
      const t = this.TEMPLATES[parseInt(el.value, 10)];
      if (!t) return;
      this.setLocal({ selections: t.sels.map(x => Object.assign({}, x, { books: x.books.slice() })) });
      return;
    }
    if (act === "startplan") {
      if (!s.selections.length) return;
      const today = new Date();
      const iso = today.getFullYear() + "-"
        + String(today.getMonth() + 1).padStart(2, "0") + "-"
        + String(today.getDate()).padStart(2, "0");
      this.setSt({ startDate: s.startDate || iso, view: "today",
                   viewDay: s.startDate ? s.viewDay : 0 });
      return;
    }
    if (act === "day") {
      const d = Math.max(0, Math.min(364, s.viewDay + i));
      this.setLocal({ viewDay: d });
      return;
    }
    if (act === "check") {
      const key = "d" + s.viewDay + "s" + i;
      const checks = Object.assign({}, s.checks);
      if (checks[key]) delete checks[key]; else checks[key] = true;
      this.setSt({ checks: checks });
      return;
    }
    if (act === "catchup") {
      const key = el.getAttribute("data-key");
      const checks = Object.assign({}, s.checks);
      checks[key] = true;
      this.setSt({ checks: checks });
      return;
    }
    if (act === "editplan") { this.setLocal({ view: "setup" }); return; }
  }

  /* ---------- render ---------- */

  render() {
    const s = this.state;
    const view = s.startDate ? s.view : "setup";
    this.root.innerHTML =
      this.renderHeader(view) +
      '<main class="dw-main">' +
      (view === "today" ? this.renderToday() : this.renderSetup()) +
      "</main>" +
      this.renderFooter();
    this.paintSaveNotice();
  }

  renderHeader(view) {
    const tabs = this.state.startDate
      ? [["today", "Today"], ["setup", "Edit plan"]]
      : [];
    return '<header class="dw-head"><div class="dw-head-in">'
      + '<div class="dw-brand"><span class="dw-wordmark">Daily Word</span>'
      + '<span class="dw-rule"></span>'
      + '<span class="dw-tag">a year in Scripture</span></div>'
      + '<nav class="dw-nav">'
      + tabs.map(t =>
          '<button data-act="nav" data-view="' + t[0] + '" class="dw-tab'
          + (view === t[0] ? " is-on" : "") + '">' + t[1] + "</button>").join("")
      + "</nav></div></header>";
  }

  renderFooter() {
    return '<footer class="dw-foot"><div class="dw-foot-in">'
      + '<span id="dw-save-notice" class="dw-save"></span>'
      + '<span class="dw-credit">Good News Church</span>'
      + "</div></footer>";
  }

  /* ---------- setup ---------- */

  renderSetup() {
    const s = this.state;
    const editing = !!s.startDate;
    const rows = s.selections.map((sel, i) => {
      const C = this.chaptersOf(sel.books);
      const caption = (sel.books.length === 1 ? C + " chapters"
        : sel.books.length + " books \u00b7 " + C + " chapters");
      return '<div class="dw-row">'
        + '<div class="dw-row-main"><div class="dw-secname">' + this.esc(sel.name) + "</div>"
        + '<div class="dw-cap">' + caption + "</div></div>"
        + '<div class="dw-step">'
        + '<button class="dw-ghost" data-act="dec" data-i="' + i + '" aria-label="Less often">\u2212</button>'
        + '<div class="dw-freq"><div class="dw-freq-n">' + this.freqLabel(sel.freq) + "</div>"
        + '<div class="dw-cap">' + this.pace(sel) + "</div></div>"
        + '<button class="dw-ghost" data-act="inc" data-i="' + i + '" aria-label="More often">+</button>'
        + "</div>"
        + '<button class="dw-x" data-act="remove" data-i="' + i + '" aria-label="Remove">\u00d7</button>'
        + "</div>";
    }).join("");

    const used = {};
    s.selections.forEach(x => { used[x.baseName || x.name] = true; });
    const chips = this.PRESETS.map((p, i) =>
      '<button class="dw-chip' + (used[p.name] ? " is-used" : "") + '" data-act="preset" data-i="'
      + i + '">' + this.esc(p.name) + "</button>").join("");

    const tmpl = '<select class="dw-select" data-act="template">'
      + '<option value="">Start from a proven plan\u2026</option>'
      + this.TEMPLATES.map((t, i) =>
          '<option value="' + i + '">' + this.esc(t.name) + "</option>").join("")
      + "</select>";

    return '<section class="dw-hero">'
      + '<div class="dw-eyebrow dw-sage">Build your plan</div>'
      + '<h1 class="dw-h1">Deep in the <em>Word</em></h1>'
      + '<p class="dw-lede">Choose the parts of the Bible you want to read and how '
      + "often. We\u2019ll turn it into a simple day-by-day plan for the next year.</p>"
      + "</section>"
      + '<div class="dw-eyebrow dw-taupe">Your sections</div>'
      + '<div class="dw-card">' + (rows || '<div class="dw-row"><div class="dw-empty">'
        + "Nothing chosen yet \u2014 pick a starting point below.</div></div>") + "</div>"
      + '<div class="dw-twoup">' + tmpl + "</div>"
      + '<div class="dw-eyebrow dw-taupe">Add a section</div>'
      + '<div class="dw-chips">' + chips + "</div>"
      + '<div class="dw-actionbar"><span class="dw-summary">' + this.summaryLine() + "</span>"
      + '<button class="dw-primary" data-act="startplan">'
      + (editing ? "Save my changes" : "Start my plan") + "</button></div>";
  }

  /* ---------- today ---------- */

  renderToday() {
    const s = this.state;
    const i = s.viewDay;
    const d = this.dateOf(i);
    const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
    const MON = ["January", "February", "March", "April", "May", "June", "July",
                 "August", "September", "October", "November", "December"];
    const isToday = i === this.todayIndex(s.startDate);
    const eyebrow = (isToday ? "Today" : "Day " + (i + 1)) + " \u00b7 " + DAYS[d.getDay()];

    const rows = s.selections.map((sel, si) => {
      const chs = this.dayChapters(sel, i);
      if (!chs.length) return "";
      const done = !!s.checks["d" + i + "s" + si];
      return '<div class="dw-read' + (done ? " is-done" : "") + '" data-act="check" data-i="' + si + '">'
        + '<span class="dw-check' + (done ? " is-on" : "") + '">' + (done ? "\u2713" : "") + "</span>"
        + '<span class="dw-read-main"><span class="dw-stream">' + this.esc(sel.name) + "</span>"
        + '<span class="dw-passage">' + this.esc(this.formatChapters(chs)) + "</span>"
        + '<span class="dw-cap">' + chs.length + (chs.length === 1 ? " chapter" : " chapters")
        + "</span></span></div>";
    }).join("");

    const sched = s.selections.filter(sel => this.dayChapters(sel, i).length).length;
    const donen = s.selections.filter((sel, si) =>
      this.dayChapters(sel, i).length && s.checks["d" + i + "s" + si]).length;
    const pct = sched ? Math.round(donen / sched * 100) : 100;

    const todayIdx = this.todayIndex(s.startDate);
    const catchup = [];
    for (let k = 0; k < todayIdx; k++) {
      s.selections.forEach((sel, si) => {
        const chs = this.dayChapters(sel, k);
        if (chs.length && !s.checks["d" + k + "s" + si])
          catchup.push({ k: k, si: si, ref: this.formatChapters(chs), name: sel.name });
      });
    }

    let missed = "";
    if (catchup.length) {
      missed = '<div class="dw-eyebrow dw-taupe">Catch up \u00b7 ' + catchup.length + "</div>"
        + '<div class="dw-catch">'
        + catchup.slice(0, 8).map(c => {
            const dt = this.dateOf(c.k);
            return '<div class="dw-cu" data-act="catchup" data-key="d' + c.k + "s" + c.si + '">'
              + '<span class="dw-check dw-small"></span>'
              + '<span><span class="dw-cu-ref">' + this.esc(c.ref) + "</span>"
              + '<span class="dw-cap">' + this.esc(c.name) + " \u00b7 "
              + MON[dt.getMonth()].slice(0, 3) + " " + dt.getDate() + "</span></span></div>";
          }).join("")
        + (catchup.length > 8
            ? '<div class="dw-cap dw-more">\u2026and ' + (catchup.length - 8) + " more</div>" : "")
        + "</div>";
    }

    return '<section class="dw-masthead">'
      + '<div class="dw-eyebrow dw-sage">' + eyebrow + "</div>"
      + '<div class="dw-mast-row"><h1 class="dw-date">' + MON[d.getMonth()] + " " + d.getDate() + "</h1>"
      + '<div class="dw-daynav">'
      + '<button class="dw-round" data-act="day" data-i="-1" aria-label="Previous day">\u2190</button>'
      + '<button class="dw-round" data-act="day" data-i="1" aria-label="Next day">\u2192</button>'
      + "</div></div>"
      + '<div class="dw-cap dw-dayof">Day ' + (i + 1) + " of 365</div>"
      + "</section>"
      + '<div class="dw-card">' + (rows
        || '<div class="dw-row"><div class="dw-rest">A day of rest \u2014 nothing scheduled today.</div></div>')
      + "</div>"
      + '<div class="dw-stats">'
      + '<div class="dw-stat"><div class="dw-num">' + this.streak(todayIdx) + '</div><div class="dw-slab">day streak</div></div>'
      + '<div class="dw-stat"><div class="dw-num">' + pct + '%</div><div class="dw-slab">done today</div></div>'
      + '<div class="dw-stat"><div class="dw-num">' + this.dayLoad(i) + '</div><div class="dw-slab">chapters</div></div>'
      + "</div>"
      + '<div class="dw-bar"><span style="width:' + pct + '%"></span></div>'
      + missed;
  }
}

  var saved = null;
  try {
    var raw = document.getElementById("dw-plan-data").textContent.trim();
    if (raw && raw !== "null") saved = JSON.parse(raw);
    if (saved && !saved.selections) saved = null;
  } catch (e) { saved = null; }

  var app = new DailyWord({ saved: saved });
  window.DW = app;
  console.log("Daily Word v1.0 ready", saved ? "plan loaded" : "no plan");
  app.start();
})();
</script>
{% endraw %}

{% else %}

<div class="dw-signin" style="font-family:Georgia,serif;max-width:560px;margin:90px auto;padding:0 22px;">
  <h1 style="font-size:38px;font-weight:400;letter-spacing:-0.028em;line-height:1.05;margin:0 0 14px;">Daily Word</h1>
  <p style="font-family:sans-serif;font-weight:300;font-size:18px;line-height:1.6;color:#6E7883;margin:0 0 22px;">
    Sign in to build your reading plan and keep your progress.</p>
  <a href="/Login?returnurl={{ 'Global' | PageParameter:'returnurl' | Default:CurrentPageUrl }}"
     style="display:inline-block;background:#165B48;color:#fff;text-decoration:none;border-radius:10px;padding:13px 22px;font-family:sans-serif;font-weight:600;font-size:15px;">Sign in</a>
</div>

{% endif %}
