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

  v1.3  stylesheet rebuilt from the Good News Church token sheets and the
        real inline styles in 'Reading Plan Builder v5 polish'. Token values
        are verbatim, not approximated.
  v1.2  all rules scoped under #dw-root plus an element reset, so the site
        theme cannot bleed in; 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,opsz,wght@0,6..72,300..700;1,6..72,300..700&family=Work+Sans:wght@300;400;450;500;600;700&display=swap">
<style>
/* Daily Word - styles ported from "Reading Plan Builder v5 polish" and the
   Good News Church token sheets. Every rule is scoped under #dw-root so the
   site theme cannot reach in. Token values are verbatim from tokens/*.css. */

#dw-root {
  --forest: #165B48; --sage: #5F9B8C; --navy: #091B34;
  --coral: #E6633F; --taupe: #B0958D; --cream: #EEDDD5;
  --forest-700: #0E3D30; --forest-600: #124A3A; --forest-500: #165B48;
  --forest-400: #2E725E; --forest-300: #5F9B8C; --forest-200: #A6C6BC;
  --forest-100: #D8E7E2; --forest-50: #EEF5F2;
  --navy-900: #050F1F; --navy-700: #091B34; --navy-500: #1B3151;
  --navy-300: #4A5E7C; --navy-200: #8C9AB0; --navy-100: #C9D1DC;
  --coral-600: #C84E2C; --coral-500: #E6633F; --coral-400: #EE8163;
  --cream-50: #FBF6F2; --cream-100: #F5EBE4; --cream-200: #EEDDD5;
  --taupe-300: #C9B4AD; --taupe-400: #B0958D; --taupe-500: #94786F;
  --white: #FFFFFF; --gray-50: #F7F8F8; --gray-100: #EDEFF0;
  --gray-200: #DCE0E2; --gray-400: #9AA3A8; --gray-600: #5C666C;
  --text-strong: var(--navy-700); --text-body: #243650;
  --text-muted: var(--navy-300); --text-faint: var(--navy-200);
  --text-on-accent: #FFFFFF; --text-link: var(--forest-500);
  --surface-page: var(--cream-50); --surface-card: var(--white);
  --border-subtle: #E7E0D9; --border-default: #D8CFC7;
  --accent-primary: var(--forest-500); --accent-soft: var(--sage);
  --accent-energy: var(--coral-500);
  --font-serif: "Newsreader", "Iowan Old Style", Georgia, serif;
  --font-sans: "Work Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-display: var(--font-serif); --font-body-f: var(--font-sans);
  --font-ui: var(--font-sans);
  --weight-light: 300; --weight-regular: 400; --weight-book: 450;
  --weight-medium: 500; --weight-semibold: 600; --weight-bold: 700;
  --track-display: -0.022em; --track-body: -0.006em; --track-eyebrow: 0.16em;
  --text-h3: 22px; --text-body-lg: 19px; --text-body-sm: 14px;
  --text-caption: 13px; --text-eyebrow: 12px;
  --ease: cubic-bezier(.23,1,.32,1);

  font-family: var(--font-body-f); color: var(--text-body);
  letter-spacing: var(--track-body); background: var(--surface-page);
  display: block; min-height: 100vh;
}

#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 *, #dw-root *::before, #dw-root *::after { box-sizing: border-box; }
#dw-root button, #dw-root select {
  -webkit-appearance: none; appearance: none; border-radius: 0;
  text-decoration: none; cursor: pointer; font-family: var(--font-ui);
}
#dw-root select { -webkit-appearance: menulist; appearance: menulist; }
#dw-root h1 { font-weight: var(--weight-regular); }

@keyframes dw-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

#dw-root .dw-head { position: sticky; top: 0; z-index: 10;
  background: rgba(251,246,242,.88); backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--border-subtle); }
#dw-root .dw-head-in { max-width: 1140px; margin: 0 auto; padding: 16px 24px;
  display: flex; align-items: center; justify-content: space-between; gap: 20px; }
#dw-root .dw-brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
#dw-root .dw-wordmark { font-family: var(--font-display); font-size: 22px;
  letter-spacing: -0.028em; color: var(--text-strong); }
#dw-root .dw-rule { width: 18px; height: 1px; background: var(--accent-soft); flex: none; }
#dw-root .dw-tag { font-family: var(--font-ui); font-weight: 300;
  font-size: var(--text-caption); color: var(--text-muted); }
#dw-root .dw-nav { display: flex; gap: 24px; flex: none; }
#dw-root .dw-tab { font-family: var(--font-ui); font-size: var(--text-body-sm);
  color: var(--text-muted); border-bottom: 2px solid transparent; padding: 4px 0;
  transition: color 160ms var(--ease), border-color 160ms var(--ease); }
#dw-root .dw-tab.is-on { color: var(--forest-500); font-weight: var(--weight-semibold);
  border-bottom-color: var(--forest-500); }
#dw-root .dw-main { padding: 56px 24px 40px; }
#dw-root .dw-wrap { max-width: 720px; margin: 0 auto; }
#dw-root .dw-wrap-narrow { max-width: 660px; margin: 0 auto; }

#dw-root .dw-eyebrow { font-family: var(--font-ui); font-size: var(--text-eyebrow);
  font-weight: var(--weight-semibold); letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; }
#dw-root .dw-sage { color: var(--accent-soft); margin-bottom: 18px; }
#dw-root .dw-taupe { color: var(--taupe-500); margin-bottom: 14px; }
#dw-root .dw-h1 { font-family: var(--font-display); font-weight: var(--weight-regular);
  font-size: clamp(40px, 8vw, 62px); line-height: 1.04; letter-spacing: -0.028em;
  color: var(--text-strong); margin: 0 0 20px; }
#dw-root .dw-h1 i, #dw-root .dw-h1 em { font-style: italic; }
#dw-root .dw-lede { font-size: var(--text-body-lg); font-weight: 300; line-height: 1.65;
  color: var(--text-muted); margin: 0; max-width: 46ch; text-wrap: pretty; }
#dw-root .dw-cap { font-size: var(--text-caption); color: var(--text-faint); }

#dw-root .dw-card { display: flex; flex-direction: column; gap: 1px;
  background: var(--border-subtle); border: 1px solid var(--border-subtle);
  border-radius: 18px; overflow: hidden; box-shadow: 0 2px 6px rgba(9,27,52,.05); }
#dw-root .dw-sections { margin-bottom: 36px; }
#dw-root .dw-row { background: var(--surface-card); padding: 20px 22px; display: flex;
  align-items: center; gap: 18px; flex-wrap: wrap;
  transition: background-color 180ms var(--ease); }
#dw-root .dw-row:hover { background: var(--gray-50); }
#dw-root .dw-row-main { flex: 1; min-width: 190px; }
#dw-root .dw-secname { font-family: var(--font-display); font-size: 21px;
  letter-spacing: var(--track-display); color: var(--text-strong); }
#dw-root .dw-row-main .dw-cap { margin-top: 4px; }

#dw-root .dw-step { display: flex; align-items: center; gap: 4px; flex: none; }
#dw-root .dw-ghost { width: 44px; height: 44px; border-radius: 999px;
  color: var(--text-muted); font-size: 17px; line-height: 1;
  transition: transform 140ms var(--ease), background-color 140ms ease, color 140ms ease; }
#dw-root .dw-ghost:hover { background: var(--gray-100); color: var(--text-strong); }
#dw-root .dw-ghost:active { transform: scale(.97); }
#dw-root .dw-freq { min-width: 124px; text-align: center; }
#dw-root .dw-freq-n { font-family: var(--font-ui); font-weight: var(--weight-bold);
  font-size: 15px; letter-spacing: -0.006em; color: var(--forest-500); }
#dw-root .dw-freq .dw-cap { margin-top: 2px; }
#dw-root .dw-x { color: var(--gray-400); font-size: 19px; padding: 6px 4px; flex: none;
  transition: transform 140ms var(--ease), color 140ms ease; }
#dw-root .dw-x:hover { color: var(--coral-500); }
#dw-root .dw-x:active { transform: scale(.97); }

#dw-root .dw-twoup { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 36px; }
#dw-root .dw-select { flex: 1 1 300px; font-family: var(--font-ui);
  font-size: var(--text-body-sm); font-weight: var(--weight-book);
  padding: 13px 16px; min-height: 48px; border: 1px solid var(--border-default);
  border-radius: 10px; background: var(--surface-card); color: var(--text-strong);
  max-width: 100%; }
#dw-root .dw-chips { display: flex; flex-wrap: wrap; gap: 9px; margin-bottom: 120px; }
#dw-root .dw-chip { border: 1px solid var(--forest-200); background: var(--surface-card);
  color: var(--forest-500); border-radius: 999px; padding: 10px 18px; min-height: 40px;
  font-family: var(--font-ui); font-size: var(--text-body-sm); font-weight: var(--weight-book);
  transition: transform 140ms var(--ease), background-color 160ms var(--ease); }
#dw-root .dw-chip:hover { background: var(--forest-50); }
#dw-root .dw-chip:active { transform: scale(.97); }
#dw-root .dw-chip.is-used { background: none; border-color: var(--border-subtle);
  color: var(--text-faint); }

#dw-root .dw-actionbar { position: sticky; bottom: 18px; 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(--border-subtle);
  border-radius: 16px; padding: 14px 16px; box-shadow: 0 6px 22px rgba(9,27,52,.08); }
#dw-root .dw-summary { font-size: var(--text-body-sm); color: var(--text-muted); }
#dw-root .dw-primary { background: var(--forest-500); color: var(--text-on-accent);
  border-radius: 10px; padding: 13px 26px; min-height: 48px;
  font-family: var(--font-ui); font-weight: var(--weight-semibold); font-size: 15px;
  transition: transform 140ms var(--ease), background-color 160ms var(--ease); }
#dw-root .dw-primary:hover { background: var(--forest-600); }
#dw-root .dw-primary:active { transform: scale(.97); }

#dw-root .dw-masthead { display: flex; align-items: flex-end;
  justify-content: space-between; gap: 20px; padding-bottom: 20px;
  border-bottom: 1px solid var(--border-default); margin-bottom: 8px; }
#dw-root .dw-mast-l { min-width: 0; }
#dw-root .dw-mast-l .dw-eyebrow { margin-bottom: 10px; }
#dw-root .dw-date { font-family: var(--font-display); font-weight: var(--weight-regular);
  font-size: clamp(36px, 8vw, 58px); line-height: 1; letter-spacing: -0.028em;
  color: var(--text-strong); margin: 0; }
#dw-root .dw-daynav { display: flex; gap: 6px; flex-shrink: 0; }
#dw-root .dw-round { width: 44px; height: 44px; border-radius: 999px;
  border: 1px solid var(--border-default); color: var(--text-strong); font-size: 15px;
  transition: transform 140ms var(--ease), background-color 140ms ease; }
#dw-root .dw-round:hover { background: var(--surface-card); }
#dw-root .dw-round:active { transform: scale(.97); }
#dw-root .dw-subrow { display: flex; align-items: center; justify-content: space-between;
  gap: 16px; margin-bottom: 30px; flex-wrap: wrap; }

#dw-root .dw-read { display: flex; align-items: center; gap: 18px; padding: 22px 24px;
  background: var(--surface-card); cursor: pointer;
  transition: filter 140ms var(--ease), background-color 180ms var(--ease);
  animation: dw-rise 260ms var(--ease) both; }
#dw-root .dw-read:hover { filter: brightness(.985); }
#dw-root .dw-read.is-done { background: var(--gray-50); }
#dw-root .dw-check { width: 24px; height: 24px; border-radius: 999px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; font-size: 14px;
  font-weight: 600; background: none; color: transparent;
  border: 1.5px solid var(--border-default);
  transition: background-color 140ms var(--ease), border-color 140ms var(--ease),
    color 140ms var(--ease); }
#dw-root .dw-check.is-on { background: var(--forest-500); color: #fff;
  border-color: var(--forest-500); }
#dw-root .dw-read-main { flex: 1; min-width: 0; }
#dw-root .dw-stream { display: block; font-size: var(--text-caption);
  font-weight: var(--weight-semibold); letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--taupe-500); margin-bottom: 6px; }
#dw-root .dw-passage { display: block; font-family: var(--font-display);
  font-size: clamp(21px, 4.4vw, 27px); letter-spacing: -0.026em; line-height: 1.15;
  color: var(--text-strong); transition: color 180ms var(--ease); }
#dw-root .dw-read.is-done .dw-passage { color: var(--text-faint);
  text-decoration: line-through; }
#dw-root .dw-read-main .dw-cap { margin-top: 6px; }
#dw-root .dw-rest { padding: 56px 0; color: var(--text-muted);
  font-family: var(--font-display); font-size: var(--text-h3); font-style: italic; }

#dw-root .dw-stats { display: grid; grid-template-columns: repeat(3, 1fr);
  margin-top: 36px; border-top: 1px solid var(--border-default);
  border-bottom: 1px solid var(--border-subtle); }
#dw-root .dw-stat { padding: 20px 4px 18px; }
#dw-root .dw-stat + .dw-stat { border-left: 1px solid var(--border-subtle);
  padding-left: 20px; }
#dw-root .dw-num { font-family: var(--font-display); font-size: 34px; line-height: 1;
  letter-spacing: -0.028em; color: var(--text-strong); }
#dw-root .dw-slab { font-family: var(--font-ui); font-size: var(--text-eyebrow);
  font-weight: var(--weight-semibold); letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--taupe-500); margin-top: 8px; }
#dw-root .dw-bar { height: 3px; background: var(--gray-100); border-radius: 999px;
  overflow: hidden; }
#dw-root .dw-bar span { display: block; height: 100%; border-radius: 999px;
  background: var(--forest-500); transition: width 280ms var(--ease); }

#dw-root .dw-catch { margin-top: 44px; }
#dw-root .dw-cu { display: flex; gap: 14px; align-items: center; padding: 14px 2px;
  border-bottom: 1px solid var(--border-subtle); cursor: pointer;
  transition: filter 140ms var(--ease); }
#dw-root .dw-cu:hover { filter: brightness(.985); }
#dw-root .dw-cu-ref { display: block; font-family: var(--font-display); font-size: 17px;
  letter-spacing: -0.02em; color: var(--text-strong); }
#dw-root .dw-cu .dw-check { width: 18px; height: 18px; }
#dw-root .dw-more { padding: 14px 2px; }

#dw-root .dw-signin { max-width: 560px; margin: 90px auto; padding: 0 24px; }

#dw-toast { position: fixed; left: 24px; bottom: 24px; z-index: 60;
  background: rgba(9,27,52,.92); color: #fff; font-family: "Work Sans", sans-serif;
  font-size: 13px; font-weight: 500; letter-spacing: -0.006em;
  padding: 10px 17px; border-radius: 999px; opacity: 0; transform: translateY(6px);
  pointer-events: none; box-shadow: 0 8px 24px rgba(9,27,52,.22);
  transition: opacity 200ms cubic-bezier(.23,1,.32,1),
    transform 200ms cubic-bezier(.23,1,.32,1); }
#dw-toast.is-on { opacity: 1; transform: translateY(0); }
#dw-toast.is-err { background: #E6633F; }

@media (max-width: 640px) {
  #dw-root .dw-main { padding: 34px 18px 34px; }
  #dw-root .dw-head-in { padding: 14px 18px; }
  #dw-root .dw-row, #dw-root .dw-read { padding: 18px 16px; }
  #dw-root .dw-step { width: 100%; justify-content: space-between; }
  #dw-root .dw-tag { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  #dw-root *, #dw-root *::before, #dw-root *::after {
    animation-duration: .01ms !important; transition-duration: .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: "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";
    const wrap = view === "today" ? "dw-wrap-narrow" : "dw-wrap";
    this.root.innerHTML =
      this.renderHeader(view) +
      '<main class="dw-main"><div class="' + wrap + '">' +
      (view === "today" ? this.renderToday() : this.renderSetup()) +
      "</div></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 <i>Word</i></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 dw-sections">' + (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];
      const delay = Math.min(si * 50, 200);
      return '<div class="dw-read' + (done ? " is-done" : "") + '" data-act="check" data-i="' + si
        + '" style="animation-delay:' + delay + 'ms">'
        + '<span class="dw-check' + (done ? " is-on" : "") + '">\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-mast-l">'
      + '<div class="dw-eyebrow dw-sage">' + eyebrow + "</div>"
      + '<h1 class="dw-date">' + MON[d.getMonth()] + " " + d.getDate() + "</h1>"
      + "</div>"
      + '<div class="dw-daynav">'
      + '<button class="dw-round" data-act="day" data-i="-1" title="Previous day">\u2190</button>'
      + '<button class="dw-round" data-act="day" data-i="1" title="Next day">\u2192</button>'
      + "</div></section>"
      + '<div class="dw-subrow"><span class="dw-cap">Day ' + (i + 1) + " of 365</span></div>"
      + '<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">Caught up</div></div>'
      + '<div class="dw-stat"><div class="dw-num">' + this.dayLoad(i) + '</div><div class="dw-slab">Chapters today</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.3 ready", saved ? "plan loaded" : "no plan");
  app.start();
})();
</script>
{% endraw %}

{% else %}

<div id="dw-root"><div class="dw-signin">
  <div class="dw-eyebrow dw-sage">A year in Scripture</div>
  <h1 class="dw-h1">Daily <i>Word</i></h1>
  <p class="dw-lede" style="margin-bottom:26px;">Sign in to build your reading plan and keep your progress.</p>
  <a class="dw-primary" href="/Login" style="display:inline-block;">Sign in</a>
</div></div>

{% endif %}
