{% comment %}
  Daily Word  v1.2
  Lava Application: daily-word   Page block: Lava Application Content
  Setup + Today. Day reader and audio are blocked on the API.Bible proxy.

  v1.2  every CSS rule is now scoped under #dw-root, plus an element reset,
        so the site theme cannot bleed into the block
        save toast moved to an id so no theme rule outranks it
        M'Cheyne tracks renamed to their anchor books
  v1.1  removed the navy footer band; transient save toast; sticky bar no
        longer covers the chips; single-book picker; restored editSt()

  Enabled Lava Commands on the BLOCK: none.
{% 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 h1, #dw-root p, #dw-root div, #dw-root span, #dw-root nav,
#dw-root header, #dw-root main, #dw-root section, #dw-root button, #dw-root select {
  margin: 0; padding: 0; border: 0; background: none; box-shadow: none;
  font-family: inherit; font-size: inherit; font-weight: inherit;
  line-height: inherit; color: inherit; text-align: left; text-transform: none;
  letter-spacing: inherit; float: none; width: auto; min-width: 0;
}
#dw-root button, #dw-root select {
  -webkit-appearance: none; appearance: none; border-radius: 0;
  text-decoration: none; outline-offset: 2px;
}
#dw-root select {
  -webkit-appearance: menulist; appearance: menulist;
}
#dw-root h1 { font-weight: 400; }
#dw-root::after { content: ""; display: table; clear: both; }
#dw-root button { font: inherit; color: inherit; cursor: pointer; }

#dw-root .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-root .dw-head-in, #dw-root .dw-main { max-width: 1140px; margin: 0 auto; padding: 0 22px; }
#dw-root .dw-head-in { display: flex; align-items: center; justify-content: space-between;
  gap: 18px; min-height: 62px; }
#dw-root .dw-brand { display: flex; align-items: center; gap: 12px; }
#dw-root .dw-wordmark { font-family: var(--disp); font-size: 22px; letter-spacing: -0.028em; }
#dw-root .dw-rule { width: 18px; height: 1px; background: var(--sage); }
#dw-root .dw-tag { font-weight: 300; font-size: 13px; color: var(--muted); }
#dw-root .dw-nav { display: flex; gap: 22px; }
#dw-root .dw-tab { background: none; border: 0; border-bottom: 2px solid transparent;
  padding: 4px 0; font-size: 14px; color: var(--muted); }
#dw-root .dw-tab.is-on { color: var(--forest); font-weight: 600; border-bottom-color: var(--forest); }

#dw-root .dw-main { padding-top: 44px; padding-bottom: 48px; }
#dw-root .dw-hero, #dw-root .dw-masthead { max-width: 720px; }
#dw-root .dw-eyebrow { font-size: 12px; font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; margin: 34px 0 13px; }
#dw-root .dw-sage { color: var(--sage); } .dw-taupe { color: var(--taupe); }
#dw-root .dw-hero .dw-eyebrow { margin-top: 0; }
#dw-root .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-root .dw-h1 em { font-style: italic; }
#dw-root .dw-lede { font-weight: 300; font-size: 19px; line-height: 1.65; color: var(--muted);
  max-width: 46ch; margin: 0; text-wrap: pretty; }

#dw-root .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-root .dw-row { background: var(--card); padding: 20px 22px; display: flex;
  align-items: center; gap: 18px; flex-wrap: wrap; }
#dw-root .dw-row-main { flex: 1; min-width: 190px; }
#dw-root .dw-secname { font-family: var(--disp); font-size: 21px; letter-spacing: -0.028em; }
#dw-root .dw-cap { font-size: 13px; color: var(--g400); margin-top: 4px; display: block; }
#dw-root .dw-step { display: flex; align-items: center; gap: 2px; flex: none;
  border: 1px solid var(--bd-soft); border-radius: 999px; padding: 2px; }
#dw-root .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-root .dw-freq { min-width: 116px; text-align: center; padding: 0 2px; }
#dw-root .dw-freq-n { font-weight: 700; font-size: 15px; color: var(--forest); }
#dw-root .dw-x { border: 0; background: none; color: var(--g400); font-size: 19px;
  padding: 6px 4px; margin-left: 6px; flex: none; }
.dw-x:hover { color: var(--coral); }
#dw-root .dw-empty, #dw-root .dw-rest { font-family: var(--disp); font-style: italic; font-size: 18px;
  color: var(--muted); }

#dw-root .dw-twoup { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 34px; }
#dw-root .dw-twoup .dw-select { flex: 1 1 280px; min-width: 0; }
#dw-root .dw-select { font: inherit; font-size: 15px; padding: 12px 14px; border: 1px solid var(--bd);
  border-radius: 10px; background: var(--card); color: var(--navy); max-width: 100%; }
#dw-root .dw-chips { display: flex; flex-wrap: wrap; gap: 9px; margin-bottom: 104px; }
#dw-root .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-root .dw-chip.is-used { background: transparent; border-color: var(--bd-soft); color: var(--g400); }

#dw-root .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-root .dw-summary { font-size: 14px; color: var(--muted); }
#dw-root .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-root .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-root .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-root .dw-daynav { display: flex; gap: 8px; padding-bottom: 6px; }
#dw-root .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-root .dw-dayof { margin: 10px 0 0; }

#dw-root .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-root .dw-read.is-done { background: var(--g50); }
#dw-root .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-root .dw-check.is-on { background: var(--forest); border-color: var(--forest); }
#dw-root .dw-check.dw-small { width: 18px; height: 18px; margin-top: 2px; }
#dw-root .dw-read-main { display: block; }
#dw-root .dw-stream { display: block; font-size: 11px; font-weight: 600; letter-spacing: .16em;
  text-transform: uppercase; color: var(--taupe); margin-bottom: 4px; }
#dw-root .dw-passage { display: block; font-family: var(--disp); letter-spacing: -0.028em;
  font-size: clamp(21px, 4.4vw, 27px); line-height: 1.15; }
#dw-root .dw-read.is-done .dw-passage { text-decoration: line-through; color: var(--g400); }

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

#dw-root .dw-catch { border-top: 1px solid var(--bd-soft); }
#dw-root .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-root .dw-cu-ref { font-family: var(--disp); font-size: 17px; display: block; }
#dw-root .dw-more { padding: 12px 2px; }


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

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

#dw-toast { position: fixed; left: 22px; bottom: 22px; z-index: 60;
  background: rgba(9,27,52,.92); color: #fff; font-size: 13px; font-weight: 500;
  padding: 10px 17px; border-radius: 999px; opacity: 0; transform: translateY(6px);
  pointer-events: none; transition: opacity 200ms cubic-bezier(.23,1,.32,1),
  transform 200ms cubic-bezier(.23,1,.32,1); box-shadow: 0 8px 24px rgba(9,27,52,.22); }
#dw-toast.is-on { opacity: 1; transform: translateY(0); }
#dw-toast.is-err { background: #E6633F; }
</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: "Genesis onward", baseName: "M'Cheyne 1",
          books: range("Genesis","2 Chronicles"), freq: 1 },
        { name: "Matthew onward", baseName: "M'Cheyne 2",
          books: [...range("Matthew","John"), "Psalms", ...range("Acts","Revelation")], freq: 1 },
        { name: "Ezra onward", baseName: "M'Cheyne 3",
          books: range("Ezra","Malachi").filter(n => n !== "Psalms"), freq: 1 },
        { name: "Acts onward", baseName: "M'Cheyne 4",
          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");
    this.toast = document.createElement("div");
    this.toast.id = "dw-toast";
    this.toast.className = "";
    document.body.appendChild(this.toast);
    this.toastTimer = null;
  }

  /* ---------- 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(); }

  // Core's addSelection() calls this. While editing an existing plan from the
  // setup view, changes are a draft held in memory until "Save my changes";
  // otherwise they persist immediately. Mirrors the prototype's editSt().
  editSt(patch) {
    if (this.state.startDate && this.state.view === "setup") this.setLocal(patch);
    else this.setSt(patch);
  }

  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 = this.toast;
    if (!el) return;
    const map = {
      idle: "", pending: "", saving: "Saving\u2026",
      saved: "Saved", error: "Could not save \u2014 check your connection"
    };
    const msg = map[this.saveState] || "";
    el.className = (msg ? "is-on" : "") + (this.saveState === "error" ? " is-err" : "");
    if (msg) el.textContent = msg;
    clearTimeout(this.toastTimer);
    if (this.saveState === "saved") {
      this.toastTimer = setTimeout(() => { el.className = ""; }, 1800);
    }
  }

  /* ---------- 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 === "book") {
      const b = this.BOOKS[parseInt(el.value, 10)];
      if (!b) return;
      this.addSelection(b[0], [b[0]], 1);
      el.value = "";
      this.render();
      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.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>";
  }

  /* ---------- 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 covered = {};
    s.selections.forEach(x => x.books.forEach(b => { covered[b] = true; }));
    const books = '<select class="dw-select" data-act="book">'
      + '<option value="">Or add a single book\u2026</option>'
      + this.BOOKS.map((b, k) =>
          '<option value="' + k + '">' + this.esc(b[0]) + " (" + b[1] + ")"
          + (covered[b[0]] ? " \u2014 in another section" : "") + "</option>").join("")
      + "</select>";

    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 + books + "</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.2 ready", saved ? "plan loaded" : "no plan");
  app.start();
})();
</script>
{% endraw %}

{% else %}

<div id="dw-root"><div class="dw-signin">
  <h1 class="dw-h1" style="font-size:38px;">Daily Word</h1>
  <p class="dw-lede" style="margin-bottom:22px;">Sign in to build your reading plan and keep your progress.</p>
  <a href="/Login" style="display:inline-block;background:#165B48;color:#fff;text-decoration:none;border-radius:10px;padding:13px 22px;font-weight:600;font-size:15px;">Sign in</a>
</div></div>

{% endif %}
