{% comment %}
  Daily Word  v2.2
  Lava Application: daily-word   Page block: Lava Application Content

  v2.2  AUDIO FIX. Two bugs meant the dock could never appear:
        1. ensureAudioBible() was never called - the prototype runs it from
           componentDidMount, a hook this build does not have, so the audio
           edition was never discovered and audioBibleId stayed null.
        2. renderDock() gated on state.audio, which only exists after
           startAudio(), which could only be reached from the play button
           inside the dock. The dock now gates on the same condition the
           prototype uses: in the reader, day loaded, edition discovered,
           and at least one chapter of the day has audio.
  v2.1  button glyph centring; hero spacing
  v2.0  day reader and glass audio dock, ported from the v5 prototype.
        DW_BIBLE_KEY below is inline and IS readable via View Source.
  v1.x  setup and today; scoped styles; save endpoint

  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);
  text-align: center;
}
/* icon buttons centre their glyph optically, not just by text-align */
#dw-root .dw-ghost, #dw-root .dw-round, #dw-root .dw-x, #dw-root .dw-fs,
#dw-root .dw-play, #dw-root .dw-qglyph {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; text-align: center;
}
#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-hero { margin-bottom: 48px; }
#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; }
}

/* ---------- day reader ---------- */
#dw-root .dw-wrap-read { max-width: 680px; margin: 0 auto; padding-bottom: 150px; }
#dw-root .dw-back { color: var(--text-muted); font-size: var(--text-body-sm);
  padding: 8px 12px 8px 0; min-height: 44px;
  transition: transform 140ms var(--ease), color 140ms ease; }
#dw-root .dw-back:hover { color: var(--text-strong); }
#dw-root .dw-back:active { transform: scale(.97); }
#dw-root .dw-read-head { display: flex; align-items: flex-start;
  justify-content: space-between; gap: 18px; margin-top: 6px; }
#dw-root .dw-read-title { font-family: var(--font-display);
  font-weight: var(--weight-regular); font-size: clamp(34px, 7vw, 54px);
  line-height: 1.03; letter-spacing: -0.028em; color: var(--text-strong);
  margin: 12px 0 0; }
#dw-root .dw-read-sub { margin-top: 10px; }
#dw-root .dw-fontctl { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
#dw-root .dw-fs { font-family: var(--font-serif); color: var(--text-faint);
  min-width: 40px; min-height: 40px;
  transition: transform 140ms var(--ease), color 140ms ease; }
#dw-root .dw-fs:hover { color: var(--text-strong); }
#dw-root .dw-fs:active { transform: scale(.97); }
#dw-root .dw-fs-sm { font-size: 14px; }
#dw-root .dw-fs-lg { font-size: 21px; }
#dw-root .dw-sagerule { width: 34px; height: 2px; background: var(--accent-soft);
  margin: 26px 0 8px; }
#dw-root .dw-loading { color: var(--text-muted); font-style: italic;
  font-family: var(--font-display); font-size: var(--text-h3); padding: 30px 0; }
#dw-root .dw-err { color: var(--coral-600); font-size: var(--text-body-sm); padding: 14px 0; }

#dw-root .dw-block { margin-top: 44px; animation: dw-rise 300ms var(--ease) both; }
#dw-root .dw-block-head { display: flex; align-items: flex-end;
  justify-content: space-between; gap: 16px; padding-bottom: 14px;
  border-bottom: 1px solid var(--border-subtle); margin-bottom: 24px; }
#dw-root .dw-block-l { min-width: 0; }
#dw-root .dw-block-ref { font-family: var(--font-display);
  font-size: clamp(22px, 4.6vw, 28px); letter-spacing: -0.026em; line-height: 1.1;
  color: var(--text-strong); }
#dw-root .dw-mark { flex-shrink: 0; border-radius: 999px; padding: 9px 16px;
  min-height: 40px; font-size: 13px; font-weight: var(--weight-book);
  border: 1px solid var(--border-default); color: var(--text-muted);
  transition: transform 140ms var(--ease), background-color 160ms var(--ease),
    border-color 160ms var(--ease), color 160ms var(--ease); }
#dw-root .dw-mark.is-on { border-color: var(--forest-500);
  background: var(--forest-500); color: #fff; }
#dw-root .dw-mark:active { transform: scale(.97); }

#dw-root .dw-passages { font-family: var(--font-display); letter-spacing: 0; }
#dw-root .dw-para { line-height: 1.8; color: var(--text-strong);
  margin: 0 0 0.35em; text-wrap: pretty; }
#dw-root .dw-para span { cursor: pointer; border-radius: 2px;
  transition: background-color 420ms var(--ease), box-shadow 420ms var(--ease); }
#dw-root .dw-para span.dw-verse-on { background: var(--forest-50);
  box-shadow: 0 0 0 4px var(--forest-50);
  transition: background-color 160ms var(--ease), box-shadow 160ms var(--ease); }
#dw-root .dw-note { font-size: var(--text-caption); color: var(--text-faint);
  margin-top: 6px; }
#dw-root .dw-note:first-of-type { margin-top: 34px; padding-top: 16px;
  border-top: 1px solid var(--border-subtle); }
#dw-root .dw-markday { margin-top: 30px; }
#dw-root .dw-readpill { flex-shrink: 0; font-size: var(--text-body-sm);
  font-weight: var(--weight-book); color: var(--forest-500);
  border: 1px solid var(--forest-200); border-radius: 999px; padding: 10px 18px;
  min-height: 44px; transition: transform 140ms var(--ease), background-color 140ms ease; }
#dw-root .dw-readpill:hover { background: var(--forest-50); }
#dw-root .dw-readpill:active { transform: scale(.97); }

/* ---------- glass audio dock (lives on body, outside #dw-root) ---------- */
#dw-dock-host { position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
  display: flex; justify-content: center; pointer-events: none;
  padding: 0 16px calc(24px + env(safe-area-inset-bottom));
  font-family: "Work Sans", -apple-system, sans-serif; letter-spacing: -0.006em; }
#dw-dock-host .dw-dockwrap { pointer-events: none; max-width: 560px; width: 100%; }
#dw-dock-host .dw-dock { pointer-events: auto; display: flex; align-items: center;
  gap: 12px; padding: 8px 14px 8px 8px; border-radius: 999px;
  background: rgba(255,255,255,.46); backdrop-filter: blur(20px) saturate(150%);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
  border: 1px solid rgba(255,255,255,.55); box-shadow: 0 12px 30px rgba(9,27,52,.2);
  color: #101C26; animation: dw-dock-in 260ms cubic-bezier(.23,1,.32,1) both; }
@keyframes dw-dock-in { from { transform: translateY(100%); } to { transform: none; } }
#dw-dock-host .dw-play { width: 52px; height: 52px; border-radius: 999px; flex: none;
  border: 0; background: #165B48; color: #fff; font-size: 16px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: transform 140ms cubic-bezier(.23,1,.32,1); }
#dw-dock-host .dw-play:active { transform: scale(.94); }
#dw-dock-host .dw-dock-l { flex: 1; min-width: 0; }
#dw-dock-host .dw-dock-ref { font-size: 13px; font-weight: 600; color: #101C26;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#dw-dock-host .dw-dock-status { font-size: 11px; color: #33414D;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#dw-dock-host .dw-chips-row { display: flex; align-items: center; gap: 5px; flex: none; }
#dw-dock-host .dw-dchip { width: 38px; height: 38px; border-radius: 999px; border: 0;
  background: rgba(255,255,255,.6); color: #101C26; font-size: 12px; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-family: inherit; padding: 0;
  transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 160ms ease; }
#dw-dock-host .dw-dchip:active { transform: scale(.97); }
#dw-dock-host .dw-dchip.is-on { background: #091B34; color: #fff; }
#dw-dock-host .dw-queue { pointer-events: auto; margin: 0 auto 10px; max-width: 520px;
  max-height: 40vh; overflow-y: auto; border-radius: 22px; padding: 14px;
  background: rgba(255,255,255,.46); backdrop-filter: blur(20px) saturate(150%);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
  border: 1px solid rgba(255,255,255,.55); box-shadow: 0 12px 30px rgba(9,27,52,.2);
  animation: dw-rise 220ms cubic-bezier(.23,1,.32,1) both; }
#dw-dock-host .dw-qhead { display: flex; justify-content: space-between; gap: 12px;
  font-size: 12px; font-weight: 600; color: #101C26; padding: 2px 8px 12px; }
#dw-dock-host .dw-qcount { font-weight: 400; color: #33414D; }
#dw-dock-host .dw-qrow { display: flex; align-items: center; gap: 12px; height: 46px;
  padding: 0 12px; border-radius: 14px; cursor: pointer;
  background: rgba(255,255,255,.26); margin-bottom: 4px; }
#dw-dock-host .dw-qrow.is-on { background: rgba(255,255,255,.7); }
#dw-dock-host .dw-qglyph { width: 20px; flex: none; font-size: 12px; color: #33414D; }
#dw-dock-host .dw-qrow.is-on .dw-qglyph { color: #091B34; }
#dw-dock-host .dw-qref { flex: 1; min-width: 0; font-family: "Newsreader", Georgia, serif;
  font-size: 17px; color: #101C26; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
#dw-dock-host .dw-qstream { font-size: 11px; color: #33414D; flex: none; }
</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;
  var DW_BIBLE_KEY = "ihqFe6Ulyut2QBDL8qqXX";

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;
    this.API = "https://api.scripture.api.bible/v1";
    this.TEXT_BIBLE = "78a9f6124f344018-01";
    this.audioQueue = []; this.audioIdx = 0; this.audioEl = null;
    this.audioObjectUrl = null; this.timecodes = null; this.chapterVerses = {};
    this.syncOffset = 0; this.audioChapters = null;
    this.audioBibleId = null; this.audioBibleName = null; this.audioCopyright = "";
    this.timecodesAreSynthetic = false;
    const st = this.state;
    if (st.readerFont == null) st.readerFont = 18;
    if (st.follow == null) st.follow = true;
    if (st.speed == null) st.speed = 1;
  }

  /* ---------- 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,
      readerFont: s.readerFont, follow: s.follow, speed: s.speed
    };
    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";
    if (s.view === "read") s.view = "today";
    this.bind();
    this.render();
    // discover the licensed audio Bible once, then repaint so the dock can
    // appear if the reader is open. Failure is non-fatal: no dock, no audio.
    this.ensureAudioBible().then(() => {
      if (this.paintDock) this.paintDock();
      if (this.state.view === "read") this.render();
    }).catch(() => {});
  }

  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) {
    if (this.readerDispatch && this.readerDispatch(act, i, el)) return;
    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 === "read" ? "dw-wrap-read"
      : view === "today" ? "dw-wrap-narrow" : "dw-wrap";
    const inner = view === "read" ? this.renderRead()
      : view === "today" ? this.renderToday() : this.renderSetup();
    this.root.innerHTML =
      this.renderHeader(view) +
      '<main class="dw-main"><div class="' + wrap + '">' + inner + "</div></main>";
    if (this.paintDock) this.paintDock();
    if (view === "read" && this.paintActiveVerse) this.paintActiveVerse();
    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>"
        + '<button class="dw-readpill" data-act="readone" data-i="' + si + '">Read</button>'
        + "</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>"
      + (rows ? '<button class="dw-chip" data-act="openday">Read today\u2019s passages</button>' : "")
      + "</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;
  }

  /* ---------- ported verbatim from the v5 prototype ---------- */

  apiHeaders() { return { headers: { "api-key": DW_BIBLE_KEY } }; }


  osis(book) {
    if (!this.OSIS) this.OSIS = { "Genesis":"GEN","Exodus":"EXO","Leviticus":"LEV","Numbers":"NUM","Deuteronomy":"DEU","Joshua":"JOS","Judges":"JDG","Ruth":"RUT","1 Samuel":"1SA","2 Samuel":"2SA","1 Kings":"1KI","2 Kings":"2KI","1 Chronicles":"1CH","2 Chronicles":"2CH","Ezra":"EZR","Nehemiah":"NEH","Esther":"EST","Job":"JOB","Psalms":"PSA","Proverbs":"PRO","Ecclesiastes":"ECC","Song of Songs":"SNG","Isaiah":"ISA","Jeremiah":"JER","Lamentations":"LAM","Ezekiel":"EZK","Daniel":"DAN","Hosea":"HOS","Joel":"JOL","Amos":"AMO","Obadiah":"OBA","Jonah":"JON","Micah":"MIC","Nahum":"NAM","Habakkuk":"HAB","Zephaniah":"ZEP","Haggai":"HAG","Zechariah":"ZEC","Malachi":"MAL","Matthew":"MAT","Mark":"MRK","Luke":"LUK","John":"JHN","Acts":"ACT","Romans":"ROM","1 Corinthians":"1CO","2 Corinthians":"2CO","Galatians":"GAL","Ephesians":"EPH","Philippians":"PHP","Colossians":"COL","1 Thessalonians":"1TH","2 Thessalonians":"2TH","1 Timothy":"1TI","2 Timothy":"2TI","Titus":"TIT","Philemon":"PHM","Hebrews":"HEB","James":"JAS","1 Peter":"1PE","2 Peter":"2PE","1 John":"1JN","2 John":"2JN","3 John":"3JN","Jude":"JUD","Revelation":"REV" };
    return this.OSIS[book];
  }

  labelForCid(cid) {
    const [osis, ch] = String(cid).split(".");
    const book = Object.keys(this.OSIS || {}).find(k => this.OSIS[k] === osis) || osis;
    const single = (this.BOOKS.find(b => b[0] === book) || [])[1] === 1;
    const name = book === "Psalms" ? "Psalm" : book;
    return single ? name : name + " " + ch;
  }

  chapterIds(chs) {
    const seen = new Set(), out = [];
    (chs || []).forEach(c => { const id = this.osis(c.book) + "." + c.ch; if (!seen.has(id)) { seen.add(id); out.push(id); } });
    return out;
  }

  async openDay(dayIdx, focusBlock) {
    this.stopAudio();
    const st = this.state;
    const blocks = st.selections.map((sel, si) => {
      const chs = this.dayChapters(sel, dayIdx);
      if (!chs.length) return null;
      return { si, key: "d" + dayIdx + "s" + si, name: sel.name, ref: this.formatChapters(chs), chapterIds: this.chapterIds(chs), paras: [] };
    }).filter(Boolean);
    const prev = st.view === "read" ? (st.prevView || "today") : st.view;
    this.focusBlock = focusBlock;
    this.chapterVerses = {};
    this.setSt({ prevView: prev, view: "read", viewDay: dayIdx, day: { idx: dayIdx, blocks, loading: true, error: null, copyright: "" }, activeVid: null });
    this.loadDayText();
  }

  async loadDayText() {
    const day = this.state.day;
    if (!day) return;
    try {
      const blocks = day.blocks.map(b => ({ ...b, paras: [] }));
      let copyright = "";
      for (const b of blocks) {
        for (const cid of b.chapterIds) {
          const r = await fetch(this.API + "/bibles/" + this.TEXT_BIBLE + "/chapters/" + cid + "?content-type=json&include-verse-numbers=false&include-titles=false&include-chapter-numbers=false", this.apiHeaders());
          const j = await r.json();
          if (!j.data) continue;
          const parsed = this.parseContent(j.data.content);
          // keep verse order + length per chapter so audio can follow along even
          // when the audio edition publishes no verse timecodes
          const vids = [], lens = {};
          parsed.forEach(p => p.spans.forEach(sp => {
            if (!sp.vid) return;
            if (lens[sp.vid] === undefined) { vids.push(sp.vid); lens[sp.vid] = 0; }
            lens[sp.vid] += sp.t.length;
          }));
          this.chapterVerses[cid] = { vids, lens };
          b.paras = b.paras.concat(parsed);
          copyright = j.data.copyright || copyright;
        }
      }
      if (this.state.view !== "read") return;
      this.setState({ day: { ...day, blocks, loading: false, copyright: (copyright || "").trim() } }, () => {
        if (this.focusBlock != null) { this.scrollToEl('[data-block="' + this.focusBlock + '"]', 0.12); this.focusBlock = null; }
      });
    } catch (e) {
      this.setState({ day: { ...day, loading: false, error: "Couldn't reach the Bible API. Check your connection and try again." } });
    }
  }

  parseContent(content) {
    const paras = [];
    const collect = (items, out) => {
      (items || []).forEach(it => {
        if (it.type === "text") { if (it.text) out.push({ vid: (it.attrs && it.attrs.verseId) || "", t: it.text }); }
        else if (it.items) collect(it.items, out);
      });
    };
    (content || []).forEach(node => {
      const out = [];
      collect(node.items, out);
      if (out.length) paras.push({ spans: out });
    });
    return paras;
  }

  scrollParentOf(el) {
    let n = el.parentElement;
    while (n) {
      const o = getComputedStyle(n).overflowY;
      if ((o === "auto" || o === "scroll" || o === "overlay") && n.scrollHeight > n.clientHeight + 1) return n;
      n = n.parentElement;
    }
    const de = document.scrollingElement || document.documentElement;
    return de.scrollHeight > de.clientHeight + 1 ? de : null;
  }

  scrollToEl(sel, ratio) {
    const el = document.querySelector(sel);
    if (!el) return;
    const box = this.scrollParentOf(el);
    if (!box) return;
    const isDoc = box === document.scrollingElement || box === document.documentElement || box === document.body;
    const viewH = isDoc ? window.innerHeight : box.clientHeight;
    const cur = isDoc ? (window.scrollY || box.scrollTop) : box.scrollTop;
    const r = el.getBoundingClientRect();
    const boxTop = isDoc ? 0 : box.getBoundingClientRect().top;
    // centre the verse itself; a ratio pins its top that far down instead
    const offset = ratio == null ? (viewH - r.height) / 2 : viewH * ratio;
    const to = Math.max(0, cur + (r.top - boxTop) - offset);
    this.tweenScroll(box, isDoc, to);
  }

  tweenScroll(box, isDoc, to) {
    const from = isDoc ? (window.scrollY || box.scrollTop) : box.scrollTop;
    if (Math.abs(to - from) < 2) return;
    const set = (y) => { if (isDoc) window.scrollTo(0, y); box.scrollTop = y; };
    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) { set(to); return; }
    if (this.scrollRaf) cancelAnimationFrame(this.scrollRaf);
    const ease = (t) => 1 - Math.pow(1 - t, 3);
    const dur = 420, t0 = performance.now();
    const step = (now) => {
      const p = Math.min(1, (now - t0) / dur);
      set(from + (to - from) * ease(p));
      if (p < 1) this.scrollRaf = requestAnimationFrame(step); else this.scrollRaf = null;
    };
    this.scrollRaf = requestAnimationFrame(step);
  }

  async ensureAudioBible() {
    if (this.audioReady) return this.audioBibleId;
    try {
      const r = await fetch(this.API + "/audio-bibles?language=eng&include-full-details=true", this.apiHeaders());
      const j = await r.json();
      const list = j.data || [];
      const pick = list.find(b => /niv/i.test(b.abbreviation || "") || /niv/i.test(b.abbreviationLocal || "")) || list[0];
      this.audioBibleId = pick ? pick.id : null;
      this.audioBibleName = pick ? (pick.abbreviationLocal || pick.abbreviation || pick.name || "") : "";
      this.audioBibleFull = pick ? (pick.name || "") : "";
      this.audioBibleCopyright = pick ? (pick.copyright || "") : "";
      if (this.audioBibleId) {
        const rb = await fetch(this.API + "/audio-bibles/" + this.audioBibleId + "/books?include-chapters=true", this.apiHeaders());
        const jb = await rb.json();
        const set = new Set();
        (jb.data || []).forEach(bk => (bk.chapters || []).forEach(c => set.add(c.id)));
        this.audioChapters = set;
      }
    } catch (e) { this.audioBibleId = null; }
    this.audioReady = true;
    return this.audioBibleId;
  }

  audioIdsFor(ids) {
    if (!this.audioChapters) return ids || [];
    return (ids || []).filter(id => this.audioChapters.has(id));
  }

  dayAudioQueue() {
    const day = this.state.day;
    if (!day) return [];
    const q = [];
    day.blocks.forEach((b, bi) => this.audioIdsFor(b.chapterIds).forEach(cid => q.push({ cid, bi })));
    return q;
  }

  async startAudio() {
    if (!this.state.day) return;
    this.setState({ audio: { state: "loading", i: 0, n: 0 } });
    const id = await this.ensureAudioBible();
    if (!id) return this.setState({ audio: { state: "error", error: "No audio Bible is licensed on this key yet." } });
    const q = this.dayAudioQueue();
    if (!q.length) return this.setState({ audio: { state: "none", i: 0, n: 0 } });
    this.audioQueue = q;
    this.playAt(0);
  }

  tcSeconds(v) {
    if (v == null) return 0;
    if (typeof v === "number") return v;
    const s = String(v);
    if (s.indexOf(":") > -1) return s.split(":").reduce((a, p) => a * 60 + parseFloat(p), 0);
    return parseFloat(s) || 0;
  }

  async playAt(i) {
    const q = this.audioQueue || [];
    if (i >= q.length) {
      if (q.length) this.markBlockRead(q[q.length - 1].bi);
      return this.setState({ audio: { state: "done", i: q.length - 1, n: q.length }, activeVid: null });
    }
    this.audioIdx = i;
    this.setState({ audio: { state: "loading", i, n: q.length } });
    try {
      const r = await fetch(this.API + "/audio-bibles/" + this.audioBibleId + "/chapters/" + q[i].cid, this.apiHeaders());
      const j = await r.json();
      const url = j.data && j.data.resourceUrl;
      if (!url) return this.playAt(i + 1);
      if (j.data.copyright) this.audioBibleCopyright = j.data.copyright;
      this.timecodes = (j.data.timecodes || []).map(t => ({ a: this.tcSeconds(t.start), b: this.tcSeconds(t.end), vid: t.verseId }));
      this.estimated = false;
      this.currentCid = q[i].cid;
      this.syncOffset = 0;
      const blob = await (await fetch(url)).blob();
      if (this.audioObjectUrl) URL.revokeObjectURL(this.audioObjectUrl);
      this.audioObjectUrl = URL.createObjectURL(blob);
      if (!this.audioEl) {
        this.audioEl = new Audio();
        this.audioEl.onended = () => {
          const cur = (this.audioQueue || [])[this.audioIdx];
          const nxt = (this.audioQueue || [])[this.audioIdx + 1];
          if (cur && (!nxt || nxt.bi !== cur.bi)) this.markBlockRead(cur.bi);
          this.playAt(this.audioIdx + 1);
        };
        this.audioEl.onerror = () => this.playAt(this.audioIdx + 1);
        this.audioEl.onloadedmetadata = () => this.buildSynthTimecodes();
        this.audioEl.ontimeupdate = () => {
          const tcs = this.timecodes;
          if (!tcs || !tcs.length) return;
          // estimated timing runs behind the narrator (chapter titles, breaths), so nudge
          // the lookup forward — LEAD covers the average, syncOffset is the user's correction
          const t = this.audioEl.currentTime + this.LEAD + (this.syncOffset || 0);
          const hit = tcs.find(x => t >= x.a && t < x.b) || (t >= tcs[tcs.length - 1].b ? tcs[tcs.length - 1] : null);
          if (hit && hit.vid && hit.vid !== this.state.activeVid) this.setState({ activeVid: hit.vid });
        };
      }
      this.audioEl.src = this.audioObjectUrl;
      this.applySpeed(this.state.speed || 1);
      await this.audioEl.play();
      this.setState({ audio: { state: "playing", i, n: q.length } });
    } catch (e) {
      this.setState({ audio: { state: "error", error: "Couldn't play this chapter — press play to try again." } });
    }
  }

  introSeconds(cid) {
    if (!cid) return 3.4;
    const osis = String(cid).split(".")[0];
    const book = Object.keys(this.OSIS || {}).find(k => this.OSIS[k] === osis) || "";
    const words = (book.split(/\s+/).filter(Boolean).length || 1) + 2; // + "chapter" + number
    return 1.6 + words * 0.62;
  }

  buildSynthTimecodes() {
    if (this.timecodes && this.timecodes.length) return;
    const cv = this.chapterVerses && this.chapterVerses[this.currentCid];
    const D = this.audioEl && this.audioEl.duration;
    if (!cv || !cv.vids.length || !D || !isFinite(D)) return;
    const total = cv.vids.reduce((n, v) => n + (cv.lens[v] || 1), 0);
    // The narrator announces the book and chapter before verse 1 ("The Gospel
    // according to Matthew, chapter one"). Hold verse 1 through that intro, sized
    // from the spoken reference itself (~2.6 syllables/sec) and clamped so a short
    // chapter can't spend most of its runtime on the heading.
    let t = Math.min(this.introSeconds(this.currentCid), D * 0.14);
    this.timecodes = cv.vids.map(v => {
      const dur = D * (cv.lens[v] || 1) / total;
      const seg = { a: t, b: t + dur, vid: v };
      t += dur;
      return seg;
    });
    this.estimated = true;
  }

  markBlockRead(bi) {
    const day = this.state.day;
    if (!day || !day.blocks[bi]) return;
    const key = day.blocks[bi].key;
    if (this.state.checks[key]) return;
    this.setSt({ checks: { ...this.state.checks, [key]: true } });
  }

  applySpeed(rate) {
    if (!this.audioEl) return;
    // keep the narrator's pitch natural at speed
    this.audioEl.preservesPitch = true;
    this.audioEl.mozPreservesPitch = true;
    this.audioEl.webkitPreservesPitch = true;
    this.audioEl.playbackRate = rate || 1;
  }

  pauseAudio() { if (this.audioEl) this.audioEl.pause(); this.setState(s => ({ audio: { ...(s.audio || {}), state: "paused" } })); }

  resumeAudio() { if (this.audioEl) this.audioEl.play(); this.setState(s => ({ audio: { ...(s.audio || {}), state: "playing" } })); }

  stopAudio() {
    if (this.audioEl) { this.audioEl.pause(); this.audioEl.removeAttribute("src"); }
    if (this.audioObjectUrl) { URL.revokeObjectURL(this.audioObjectUrl); this.audioObjectUrl = null; }
    this.audioQueue = null;
    this.timecodes = null;
    this.setState({ audio: null, activeVid: null, queueOpen: false });
  }
}

/* ---------- reader + audio layer, appended to DailyWord ---------- */

Object.assign(DailyWord.prototype, {

  /* setState with surgical paths. A full innerHTML rebuild on every verse
     change would destroy scroll position and make follow-along unusable, so
     activeVid / audio / queueOpen updates patch the DOM in place instead. */
  setState(patch, cb) {
    const p = (typeof patch === "function") ? patch(this.state) : patch;
    if (!p) { if (cb) cb(); return; }
    const keys = Object.keys(p);
    Object.assign(this.state, p);
    const surgical = keys.every(k =>
      k === "activeVid" || k === "audio" || k === "queueOpen");
    if (surgical && this.state.view === "read") {
      if (keys.indexOf("activeVid") > -1) this.paintActiveVerse();
      if (keys.indexOf("audio") > -1 || keys.indexOf("queueOpen") > -1) this.paintDock();
      if (cb) cb();
      return;
    }
    this.render();
    if (cb) cb();
  },

  /* ---------- reader view ---------- */

  renderRead() {
    const s = this.state, day = s.day;
    if (!day) return "";
    const d = this.dateOf(day.idx);
    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 = day.idx === this.todayIndex(s.startDate);
    const eyebrow = (isToday ? "Today" : "Day " + (day.idx + 1)) + " \u00b7 " + DAYS[d.getDay()];
    const refs = day.blocks.map(b => b.ref).join(" \u00b7 ");

    let body = "";
    if (day.loading) {
      body = '<div class="dw-loading">Loading today\u2019s passages\u2026</div>';
    } else if (day.error) {
      body = '<div class="dw-err">' + this.esc(day.error) + "</div>";
    } else {
      body = day.blocks.map((b, bi) => {
        const done = !!s.checks[b.key];
        const paras = b.paras.map(p =>
          '<p class="dw-para">' + p.spans.map(sp =>
            '<span data-verse="' + this.esc(sp.vid) + '" data-act="resync"'
            + ' title="Tap to resync the audio here">' + this.esc(sp.t) + "</span>"
          ).join("") + "</p>").join("");
        return '<div class="dw-block" data-block="' + bi + '">'
          + '<div class="dw-block-head"><div class="dw-block-l">'
          + '<div class="dw-stream">' + this.esc(b.name) + "</div>"
          + '<div class="dw-block-ref">' + this.esc(b.ref) + "</div></div>"
          + '<button class="dw-mark' + (done ? " is-on" : "") + '" data-act="mark" data-i="'
          + bi + '">' + (done ? "\u2713 Read" : "Mark read") + "</button>"
          + "</div>" + paras + "</div>";
      }).join("");
    }

    const notes = [day.copyright, this.audioCopyright,
      this.timecodesAreSynthetic ? "Audio timing is estimated." : "",
      "If it drifts, tap the verse being read to resync."]
      .filter(Boolean).map(x => '<div class="dw-note">' + this.esc(x) + "</div>").join("");

    return '<button class="dw-back" data-act="back">\u2190 Back</button>'
      + '<div class="dw-read-head"><div class="dw-mast-l">'
      + '<div class="dw-eyebrow dw-sage">' + eyebrow + "</div>"
      + '<h1 class="dw-read-title">' + MON[d.getMonth()] + " " + d.getDate() + "</h1>"
      + '<div class="dw-cap dw-read-sub">' + this.esc(refs) + "</div></div>"
      + '<div class="dw-fontctl">'
      + '<button class="dw-fs dw-fs-sm" data-act="font" data-i="-1" title="Smaller text">A</button>'
      + '<button class="dw-fs dw-fs-lg" data-act="font" data-i="1" title="Larger text">A</button>'
      + "</div></div>"
      + '<div class="dw-sagerule"></div>'
      + '<div class="dw-passages" style="font-size:' + (s.readerFont || 18) + 'px">'
      + body + "</div>"
      + notes
      + '<div class="dw-markday"><button class="dw-primary" data-act="markday">Mark the day read</button></div>';
  },

  /* ---------- glass audio dock ---------- */

  renderDock() {
    const s = this.state;
    if (s.view !== "read" || !s.day || !this.audioBibleId) return "";
    const dayQ = this.dayAudioQueue();
    if (!dayQ.length) return "";
    const q = (this.audioQueue && this.audioQueue.length) ? this.audioQueue : dayQ;
    const a = s.audio || { state: "idle", i: 0, n: q.length };
    const cur = q[a.i] || {};
    const playing = a.state === "playing";
    const label = this.labelForCid(cur.cid || "") || "Today\u2019s reading";
    const dayChapters = s.day.blocks.reduce((n, b) => n + b.chapterIds.length, 0);
    const status = a.state === "loading" ? "Loading audio\u2026"
      : a.state === "error" ? (a.error || "Audio unavailable")
      : a.state === "none" ? "No audio for these chapters yet"
      : a.state === "done" ? "Finished \u00b7 " + (this.audioBibleName || "")
      : a.state === "idle"
        ? (dayQ.length < dayChapters
            ? "Audio for " + dayQ.length + " of " + dayChapters + " chapters"
              + (this.audioBibleName ? " \u00b7 " + this.audioBibleName : "")
            : (this.audioBibleName ? "Audio \u00b7 " + this.audioBibleName : "Ready"))
      : "Chapter " + (a.i + 1) + " of " + (a.n || q.length)
        + (this.audioBibleName ? " \u00b7 " + this.audioBibleName : "");

    const live = s.audio && s.audio.state !== "idle";
    const rows = q.map((c, k) => {
      const on = live && k === a.i;
      const blk = s.day.blocks[c.bi] || {};
      return '<div class="dw-qrow' + (on ? " is-on" : "") + '" data-act="jump" data-i="' + k + '">'
        + '<span class="dw-qglyph">' + (on ? (playing ? "\u275a\u275a" : "\u25b6") : (k + 1)) + "</span>"
        + '<span class="dw-qref">' + this.esc(this.labelForCid(c.cid)) + "</span>"
        + '<span class="dw-qstream">' + this.esc(blk.name || "") + "</span></div>";
    }).join("");

    const queue = s.queueOpen
      ? '<div class="dw-queue"><div class="dw-qhead">Today \u00b7 ' + q.length
        + ' chapters<span class="dw-qcount">Chapter ' + (a.i + 1) + " of " + q.length
        + "</span></div>" + rows + "</div>"
      : "";

    return '<div class="dw-dockwrap">' + queue + '<div class="dw-dock">'
      + '<button class="dw-play" data-act="playpause">'
      + (playing ? "\u275a\u275a" : "\u25b6") + "</button>"
      + '<div class="dw-dock-l"><div class="dw-dock-ref">' + this.esc(label) + "</div>"
      + '<div class="dw-dock-status">' + this.esc(status) + "</div></div>"
      + '<div class="dw-chips-row">'
      + '<button class="dw-dchip" data-act="prevch" title="Previous chapter">\u2039</button>'
      + '<button class="dw-dchip" data-act="nextch" title="Next chapter">\u203a</button>'
      + '<button class="dw-dchip' + (s.speed !== 1 ? " is-on" : "") + '" data-act="speed">'
      + (s.speed || 1) + "\u00d7</button>"
      + '<button class="dw-dchip' + (s.follow ? " is-on" : "") + '" data-act="follow">Follow</button>'
      + '<button class="dw-dchip' + (s.queueOpen ? " is-on" : "") + '" data-act="queue" title="Queue">'
      + (s.queueOpen ? "\u2304" : "\u2303") + "</button>"
      + '<button class="dw-dchip" data-act="stopaudio" title="Stop">\u00d7</button>'
      + "</div></div></div>";
  },

  paintDock() {
    let host = document.getElementById("dw-dock-host");
    if (!host) {
      host = document.createElement("div");
      host.id = "dw-dock-host";
      document.body.appendChild(host);
    }
    host.innerHTML = this.state.view === "read" ? this.renderDock() : "";
    if (!host._bound) {
      host.addEventListener("click", (ev) => {
        const el = ev.target.closest("[data-act]");
        if (!el) return;
        this.dispatch(el.getAttribute("data-act"),
          parseInt(el.getAttribute("data-i"), 10), el);
      });
      host._bound = true;
    }
  },

  paintActiveVerse() {
    const vid = this.state.activeVid;
    const prev = this.root.querySelectorAll(".dw-verse-on");
    for (let k = 0; k < prev.length; k++) prev[k].classList.remove("dw-verse-on");
    if (!vid) return;
    const el = this.root.querySelector('[data-verse="' + vid + '"]');
    if (!el) return;
    el.classList.add("dw-verse-on");
    if (this.state.follow) {
      requestAnimationFrame(() => this.scrollToEl('[data-verse="' + vid + '"]'));
    }
  },

  /* ---------- reader dispatch ---------- */

  readerDispatch(act, i, el) {
    const s = this.state;
    if (act === "openday") { this.openDay(s.viewDay, null); this.paintDock(); return true; }
    if (act === "readone") { this.openDay(s.viewDay, i); this.paintDock(); return true; }
    if (act === "back") {
      this.stopAudio();
      this.setSt({ view: s.prevView || "today" });
      this.paintDock();
      return true;
    }
    if (act === "font") {
      const n = Math.max(14, Math.min(28, (s.readerFont || 18) + i * 2));
      this.setSt({ readerFont: n });
      return true;
    }
    if (act === "mark") {
      this.markBlockRead(i);
      return true;
    }
    if (act === "markday") {
      const checks = Object.assign({}, s.checks);
      (s.day ? s.day.blocks : []).forEach(b => { checks[b.key] = true; });
      this.stopAudio();
      this.setSt({ checks: checks, view: s.prevView || "today" });
      this.paintDock();
      return true;
    }
    if (act === "resync") {
      const vid = el.getAttribute("data-verse");
      if (!vid || !this.audioEl) return true;
      const seg = (this.timecodes || []).find(x => x.vid === vid);
      if (seg) this.syncOffset = seg.start - this.audioEl.currentTime;
      this.setState({ activeVid: vid });
      return true;
    }
    if (act === "playpause") {
      const a = s.audio;
      if (!a || a.state === "none") this.startAudio();
      else if (a.state === "playing") this.pauseAudio();
      else this.resumeAudio();
      return true;
    }
    if (act === "prevch") {
      if (this.audioQueue && this.audioIdx > 0) this.playAt(this.audioIdx - 1);
      return true;
    }
    if (act === "nextch") {
      const qn = (this.audioQueue || []).length;
      if (qn && this.audioIdx < qn - 1) this.playAt(this.audioIdx + 1);
      return true;
    }
    if (act === "jump") {
      if (this.audioQueue && this.audioQueue.length) this.playAt(i);
      else this.startAudio().then(() => this.playAt(i));
      return true;
    }
    if (act === "speed") {
      const ladder = [1, 1.25, 1.5, 1.75, 2, 0.75];
      const at = ladder.indexOf(s.speed || 1);
      const next = ladder[(at + 1) % ladder.length];
      this.state.speed = next;
      this.applySpeed(next);
      this.queueSave();
      this.paintDock();
      return true;
    }
    if (act === "follow") {
      this.state.follow = !s.follow;
      this.queueSave();
      this.paintDock();
      if (this.state.follow && s.activeVid) this.paintActiveVerse();
      return true;
    }
    if (act === "queue") { this.setState({ queueOpen: !s.queueOpen }); return true; }
    if (act === "stopaudio") { this.stopAudio(); this.paintDock(); return true; }
    return false;
  }
});

  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 v2.2 ready", saved ? "plan loaded" : "no plan");
  app.start();
  window.addEventListener("beforeunload", function () { app.stopAudio(); });
})();
</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 %}
