<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Daily Word v5.1 - M'Cheyne rebuilt on the four historical tracks. Core verified 43/43 by test-core-v2.js. -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
  <link rel="stylesheet" href="_ds/good-news-church-design-system-929ad584-7773-4846-a963-236afa2b46e3/tokens/fonts.css">
  <link rel="stylesheet" href="_ds/good-news-church-design-system-929ad584-7773-4846-a963-236afa2b46e3/tokens/colors.css">
  <link rel="stylesheet" href="_ds/good-news-church-design-system-929ad584-7773-4846-a963-236afa2b46e3/tokens/typography.css">
  <link rel="stylesheet" href="_ds/good-news-church-design-system-929ad584-7773-4846-a963-236afa2b46e3/tokens/spacing.css">
  <link rel="stylesheet" href="_ds/good-news-church-design-system-929ad584-7773-4846-a963-236afa2b46e3/styles.css">
  <script src="_ds/good-news-church-design-system-929ad584-7773-4846-a963-236afa2b46e3/_ds_bundle.js"></script>
  <style>
    body { margin: 0; background: var(--surface-page); font-family: var(--font-body); color: var(--text-body); letter-spacing: var(--track-body); }
    a { color: var(--text-link); text-decoration: none; }
    a:hover { color: var(--forest-700); text-decoration: underline; }
    input, select, button { font-family: var(--font-ui); }
    .print-only { display: none; }
    @keyframes dw-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
    @keyframes dw-bar-in { from { opacity: 0; transform: translateY(100%); } to { opacity: 1; transform: none; } }
    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
    }
    @media print {
      .no-print, .screen-only { display: none !important; }
      .print-only { display: block !important; }
      body { background: #fff; }
      @page { size: letter landscape; margin: 10mm 4mm; }
      main { padding: 0 !important; }
    }
  </style>
</helmet>

<div style="min-height: 100vh; display: flex; flex-direction: column;">

  <header class="no-print" style="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);">
    <div style="max-width: 1140px; margin: 0 auto; padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;">
      <div style="display: flex; align-items: baseline; gap: 12px;">
        <div style="font-family: var(--font-display); font-size: 22px; letter-spacing: var(--track-display); color: var(--text-strong);">Daily Word</div>
        <div style="width: 18px; height: 1px; background: var(--accent-soft);"></div>
        <div style="font-family: var(--font-ui); font-weight: 300; font-size: 13px; letter-spacing: -0.006em; color: var(--text-muted);">a year in Scripture</div>
      </div>
      <sc-if value="{{ hasPlan }}" hint-placeholder-val="{{ false }}">
        <nav style="display: flex; gap: 24px;">
          <sc-for list="{{ navTabs }}" as="tab" hint-placeholder-count="4">
            <button onClick="{{ tab.go }}" style="{{ tab.style }}" style-active="transform: scale(.97);">{{ tab.label }}</button>
          </sc-for>
        </nav>
      </sc-if>
    </div>
  </header>

  <main style="flex: 1; width: 100%; max-width: 1140px; margin: 0 auto; padding: 56px 24px 110px; box-sizing: border-box;">

    <!-- ============ SETUP ============ -->
    <sc-if value="{{ isSetup }}" hint-placeholder-val="{{ true }}">
      <div style="max-width: 720px; margin: 0 auto;">
        <div style="margin-bottom: 48px;">
          <div style="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(--accent-soft); margin-bottom: 18px;">Build your plan</div>
          <h1 style="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;">Deep in the <i>Word</i></h1>
          <p style="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;">Choose the parts of the Bible you want to read and how often. We'll turn it into a simple day-by-day plan for the next year.</p>
        </div>

        <div style="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-bottom: 14px;">Your sections</div>
        <div style="display: flex; flex-direction: column; gap: 1px; background: var(--border-subtle); border: 1px solid var(--border-subtle); border-radius: 18px; overflow: hidden; margin-bottom: 36px;">
          <sc-for list="{{ selections }}" as="sel" hint-placeholder-count="3">
            <div style="background: var(--surface-card); padding: 20px 22px; display: flex; align-items: center; gap: 18px; flex-wrap: wrap;" style-hover="background: var(--gray-50);">
              <div style="flex: 1; min-width: 190px;">
                <div style="font-family: var(--font-display); font-size: 21px; letter-spacing: var(--track-display); color: var(--text-strong);">{{ sel.name }}</div>
                <div style="font-size: var(--text-caption); color: var(--text-faint); margin-top: 4px;">{{ sel.desc }}</div>
              </div>
              <div style="display: flex; align-items: center; gap: 4px;">
                <button onClick="{{ sel.dec }}" style-active="transform: scale(.97);" style="width: 44px; height: 44px; border-radius: 999px; border: none; background: none; color: var(--text-muted); font-size: 17px; cursor: pointer; line-height: 1; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease, color 140ms ease;" style-hover="background: var(--gray-100); color: var(--text-strong);">−</button>
                <div style="min-width: 124px; text-align: center;">
                  <div style="font-family: var(--font-ui); font-weight: var(--weight-bold); font-size: 15px; letter-spacing: -0.006em; color: var(--forest-500);">{{ sel.freqLabel }}</div>
                  <div style="font-size: var(--text-caption); color: var(--text-faint); margin-top: 2px;">{{ sel.paceLabel }}</div>
                </div>
                <button onClick="{{ sel.inc }}" style-active="transform: scale(.97);" style="width: 44px; height: 44px; border-radius: 999px; border: none; background: none; color: var(--text-muted); font-size: 17px; cursor: pointer; line-height: 1; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease, color 140ms ease;" style-hover="background: var(--gray-100); color: var(--text-strong);">+</button>
              </div>
              <button onClick="{{ sel.remove }}" title="Remove" style-active="transform: scale(.97);" style="border: none; background: none; color: var(--gray-400); font-size: 19px; cursor: pointer; padding: 6px 4px;" style-hover="color: var(--coral-500);">×</button>
            </div>
          </sc-for>
        </div>

        <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 28px; margin-bottom: 44px;">
          <div>
            <div style="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-bottom: 12px;">Start from a proven plan</div>
            <select value="" onChange="{{ onTemplatePick }}" style="width: 100%; padding: 12px 14px; min-height: 46px; border: 1.5px solid var(--border-default); border-radius: 8px; background: var(--surface-card); color: var(--text-body); font-size: var(--text-body-sm); outline: none; box-sizing: border-box;">
              <option value="">Choose a plan…</option>
              <sc-for list="{{ templates }}" as="t" hint-placeholder-count="6">
                <option value="{{ t.name }}">{{ t.label }}</option>
              </sc-for>
            </select>
          </div>
          <div>
            <div style="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-bottom: 12px;">Or add a single book</div>
            <select value="{{ bookPick }}" onChange="{{ onBookPick }}" style="width: 100%; padding: 12px 14px; min-height: 46px; border: 1.5px solid var(--border-default); border-radius: 8px; background: var(--surface-card); color: var(--text-body); font-size: var(--text-body-sm); outline: none; box-sizing: border-box;">
              <option value="">Add a book…</option>
              <sc-for list="{{ bookOptions }}" as="b" hint-placeholder-count="5">
                <option value="{{ b.name }}">{{ b.label }}</option>
              </sc-for>
            </select>
          </div>
        </div>

        <div style="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-bottom: 14px;">Add a section</div>
        <div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 40px;">
          <sc-for list="{{ presets }}" as="p" hint-placeholder-count="8">
            <button onClick="{{ p.add }}" disabled="{{ p.used }}" style="{{ p.style }}" style-active="transform: scale(.97);" style-hover="background: var(--forest-50);">{{ p.name }}</button>
          </sc-for>
        </div>

        <div style="position: sticky; bottom: 18px; background: rgba(251,246,242,.92); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid var(--border-subtle); border-radius: 16px; box-shadow: 0 6px 22px rgba(9,27,52,.08); padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;">
          <div style="font-size: var(--text-body-sm); color: var(--text-muted);">{{ summaryLine }}</div>
          <div style="display: flex; align-items: center; gap: 10px;">
            <sc-if value="{{ showDiscard }}" hint-placeholder-val="{{ false }}">
              <button onClick="{{ discardEdits }}" style-active="transform: scale(.97);" style="border: none; background: none; border-radius: 8px; padding: 12px 14px; min-height: 44px; cursor: pointer; color: var(--text-muted); font-size: var(--text-body-sm);" style-hover="color: var(--text-strong); background: var(--gray-100);">Discard changes</button>
            </sc-if>
            <x-import component-from-global-scope="GoodNewsChurchDesignSystem_929ad5.Button" size="lg" onClick="{{ startPlan }}" disabled="{{ noSelections }}" hint-size="180px,48px">{{ startBtnLabel }}</x-import>
          </div>
        </div>
      </div>
    </sc-if>

    <!-- ============ TODAY ============ -->
    <sc-if value="{{ isToday }}" hint-placeholder-val="{{ false }}">
      <div style="max-width: 660px; margin: 0 auto;">
        <div style="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;">
          <div style="min-width: 0;">
            <div style="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(--accent-soft); margin-bottom: 10px;">{{ dayEyebrow }}</div>
            <div style="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);">{{ dayTitle }}</div>
          </div>
          <div class="no-print" style="display: flex; gap: 6px; flex-shrink: 0;">
            <button onClick="{{ prevDay }}" style-active="transform: scale(.97);" disabled="{{ atStart }}" title="Previous day" style="width: 44px; height: 44px; border-radius: 999px; border: 1px solid var(--border-default); background: none; cursor: pointer; color: var(--text-strong); font-size: 15px; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease;" style-hover="background: var(--surface-card);">←</button>
            <button onClick="{{ nextDay }}" style-active="transform: scale(.97);" disabled="{{ atEnd }}" title="Next day" style="width: 44px; height: 44px; border-radius: 999px; border: 1px solid var(--border-default); background: none; cursor: pointer; color: var(--text-strong); font-size: 15px; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease;" style-hover="background: var(--surface-card);">→</button>
          </div>
        </div>
        <div style="display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 30px; flex-wrap: wrap;">
          <div style="font-size: var(--text-caption); color: var(--text-faint);">{{ daySubtitle }}</div>
          <sc-if value="{{ hasDayReadings }}" hint-placeholder-val="{{ false }}">
            <button class="no-print" onClick="{{ openDayPage }}" style-active="transform: scale(.97);" style="border: 1px solid var(--forest-200); background: var(--surface-card); color: var(--forest-500); border-radius: 999px; padding: 10px 20px; min-height: 44px; font-size: var(--text-body-sm); font-weight: 450; cursor: pointer; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease;" style-hover="background: var(--forest-50);">Read today's passages</button>
          </sc-if>
        </div>

        <sc-if value="{{ dayEmpty }}" hint-placeholder-val="{{ false }}">
          <div style="padding: 56px 0; color: var(--text-muted); font-family: var(--font-display); font-size: var(--text-h3); font-style: italic;">A day of rest — nothing scheduled today.</div>
        </sc-if>

        <div style="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);">
          <sc-for list="{{ dayReadings }}" as="r" hint-placeholder-count="3">
            <div onClick="{{ r.toggle }}" style="{{ r.cardStyle }}" style-hover="filter: brightness(.985);">
              <div style="{{ r.checkStyle }}">{{ r.checkMark }}</div>
              <div style="flex: 1; min-width: 0;">
                <div style="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;">{{ r.source }}</div>
                <div style="{{ r.passageStyle }}">{{ r.passage }}</div>
                <div style="font-size: var(--text-caption); color: var(--text-faint); margin-top: 6px;">{{ r.count }}</div>
              </div>
              <button class="no-print" onClick="{{ r.read }}" style-active="transform: scale(.97);" style="flex-shrink: 0; font-size: var(--text-body-sm); font-weight: 450; color: var(--forest-500); border: 1px solid var(--forest-200); border-radius: 999px; padding: 10px 18px; min-height: 44px; background: none; cursor: pointer; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease;" style-hover="background: var(--forest-50);">Read</button>
            </div>
          </sc-for>
        </div>

        <div style="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);">
          <div style="padding: 20px 4px 18px;">
            <div style="font-family: var(--font-display); font-size: 34px; line-height: 1; letter-spacing: -0.028em; color: var(--text-strong);">{{ streakNum }}</div>
            <div style="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;">{{ streakWord }}</div>
          </div>
          <div style="padding: 20px 4px 18px; border-left: 1px solid var(--border-subtle); padding-left: 20px;">
            <div style="font-family: var(--font-display); font-size: 34px; line-height: 1; letter-spacing: -0.028em; color: var(--text-strong);">{{ pctNum }}</div>
            <div style="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;">Caught up</div>
          </div>
          <div style="padding: 20px 4px 18px; border-left: 1px solid var(--border-subtle); padding-left: 20px;">
            <div style="font-family: var(--font-display); font-size: 34px; line-height: 1; letter-spacing: -0.028em; color: var(--text-strong);">{{ chapsToday }}</div>
            <div style="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;">Chapters today</div>
          </div>
        </div>
        <div style="height: 3px; background: var(--gray-100); border-radius: 999px; overflow: hidden;">
          <div style="{{ progressBarStyle }}"></div>
        </div>

        <sc-if value="{{ hasMissed }}" hint-placeholder-val="{{ false }}">
          <div class="no-print" style="margin-top: 40px;">
            <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 8px;">
              <div style="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);">Catch up</div>
              <div style="font-size: var(--text-caption); color: var(--text-faint);">{{ missedSummary }}</div>
            </div>
            <div style="display: flex; flex-direction: column;">
              <sc-for list="{{ missedList }}" as="m" hint-placeholder-count="3">
                <div onClick="{{ m.toggle }}" style="display: flex; align-items: center; gap: 14px; padding: 12px 8px; min-height: 44px; border-bottom: 1px solid var(--border-subtle); cursor: pointer; box-sizing: border-box;" style-hover="background: var(--gray-50);">
                  <div style="width: 18px; height: 18px; border-radius: 999px; border: 1.5px solid var(--border-default); flex-shrink: 0;"></div>
                  <div style="flex: 1; min-width: 0;">
                    <div style="font-family: var(--font-display); font-size: 17px; letter-spacing: var(--track-display); color: var(--text-strong);">{{ m.passage }}</div>
                    <div style="font-size: var(--text-caption); color: var(--text-faint); margin-top: 2px;">{{ m.meta }}</div>
                  </div>
                  <button onClick="{{ m.read }}" style-active="transform: scale(.97);" style="font-size: var(--text-caption); font-weight: 450; color: var(--forest-500); padding: 8px; border: none; background: none; cursor: pointer;" style-hover="text-decoration: underline;">Read</button>
                </div>
              </sc-for>
            </div>
            <sc-if value="{{ missedMore }}" hint-placeholder-val="{{ false }}">
              <div style="font-size: var(--text-caption); color: var(--text-faint); padding: 12px 8px 0;">…and {{ missedMoreCount }} more — tap older days in the calendar</div>
            </sc-if>
          </div>
        </sc-if>
      </div>
    </sc-if>

    <!-- ============ DAY READER (all of today's passages) ============ -->
    <sc-if value="{{ isRead }}" hint-placeholder-val="{{ false }}">
      <div style="max-width: 680px; margin: 0 auto; padding-bottom: 130px;">
        <button class="no-print" onClick="{{ closeDay }}" style-active="transform: scale(.97);" style="border: none; background: none; color: var(--text-muted); font-size: var(--text-body-sm); padding: 8px 12px 8px 0; min-height: 44px; cursor: pointer;" style-hover="color: var(--text-strong);">← Back</button>
        <div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-top: 6px;">
          <div style="min-width: 0;">
            <div style="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(--accent-soft);">{{ readEyebrow }}</div>
            <h1 style="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;">{{ readTitle }}</h1>
            <div style="font-size: var(--text-caption); color: var(--text-faint); margin-top: 10px;">{{ readSubtitle }}</div>
          </div>
          <div class="no-print" style="display: flex; align-items: center; gap: 2px; flex-shrink: 0;">
            <button onClick="{{ fontDown }}" style-active="transform: scale(.97);" title="Smaller text" style="border: none; background: none; font-family: var(--font-serif); font-size: 14px; color: var(--text-faint); cursor: pointer; min-width: 40px; min-height: 40px;" style-hover="color: var(--text-strong);">A</button>
            <button onClick="{{ fontUp }}" style-active="transform: scale(.97);" title="Larger text" style="border: none; background: none; font-family: var(--font-serif); font-size: 21px; color: var(--text-faint); cursor: pointer; min-width: 40px; min-height: 40px;" style-hover="color: var(--text-strong);">A</button>
          </div>
        </div>
        <div style="width: 34px; height: 2px; background: var(--accent-soft); margin: 26px 0 8px;"></div>

        <sc-if value="{{ readLoading }}" hint-placeholder-val="{{ false }}">
          <div style="color: var(--text-muted); font-style: italic; font-family: var(--font-display); font-size: var(--text-h3); padding: 30px 0;">Loading today's passages…</div>
        </sc-if>
        <sc-if value="{{ readError }}" hint-placeholder-val="{{ false }}">
          <div style="color: var(--danger); font-size: var(--text-body-sm); padding: 14px 0;">{{ readErrorMsg }}</div>
        </sc-if>

        <sc-for list="{{ readBlocks }}" as="b" hint-placeholder-count="2">
          <div data-block="{{ b.idx }}" style="{{ b.blockStyle }}">
            <div style="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;">
              <div style="min-width: 0;">
                <div style="font-size: var(--text-caption); font-weight: var(--weight-semibold); letter-spacing: var(--track-eyebrow); text-transform: uppercase; color: var(--taupe-500); margin-bottom: 8px;">{{ b.name }}</div>
                <div style="font-family: var(--font-display); font-size: clamp(22px, 4.6vw, 28px); letter-spacing: -0.026em; line-height: 1.1; color: var(--text-strong);">{{ b.ref }}</div>
              </div>
              <button onClick="{{ b.toggle }}" style="{{ b.checkStyle }}" style-active="transform: scale(.97);">{{ b.checkLabel }}</button>
            </div>
            <sc-for list="{{ b.paras }}" as="p" hint-placeholder-count="3">
              <p style="{{ readerParaStyle }}">
                <sc-for list="{{ p.spans }}" as="s" hint-placeholder-count="3">
                  <span data-verse="{{ s.vid }}" onClick="{{ s.resync }}" title="Tap to resync the audio here" style="{{ s.style }}">{{ s.t }}</span>
                </sc-for>
              </p>
            </sc-for>
          </div>
        </sc-for>

        <div style="font-size: var(--text-caption); color: var(--text-faint); margin-top: 34px; padding-top: 16px; border-top: 1px solid var(--border-subtle);">{{ readCopyright }}</div>
        <div style="font-size: var(--text-caption); color: var(--text-faint); margin-top: 6px;">{{ audioCopyright }}</div>
        <div style="font-size: var(--text-caption); color: var(--text-faint); margin-top: 6px;">{{ audioNote }}</div>
        <div style="font-size: var(--text-caption); color: var(--text-faint); margin-top: 6px;">{{ followNote }}</div>
        <div class="no-print" style="margin-top: 30px;">
          <x-import component-from-global-scope="GoodNewsChurchDesignSystem_929ad5.Button" size="lg" onClick="{{ markDayRead }}" hint-size="200px,48px">Mark the day read</x-import>
        </div>
      </div>
    </sc-if>

    <!-- ============ CALENDAR ============ -->
    <sc-if value="{{ isCalendar }}" hint-placeholder-val="{{ false }}">
      <div>
        <div style="margin-bottom: 44px; max-width: 660px;">
          <div style="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(--accent-soft); margin-bottom: 14px;">Your year</div>
          <h1 style="font-family: var(--font-display); font-weight: var(--weight-regular); font-size: clamp(36px, 7vw, 58px); line-height: 1.02; letter-spacing: -0.028em; color: var(--text-strong); margin: 0;">365 days in the <i>Word</i></h1>
        </div>
        <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(212px, 1fr)); gap: 34px 28px;">
          <sc-for list="{{ months }}" as="m" hint-placeholder-count="6">
            <div>
              <div style="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); padding-bottom: 10px; border-bottom: 1px solid var(--border-subtle); margin-bottom: 12px;">{{ m.label }}</div>
              <div style="display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px;">
                <sc-for list="{{ m.cells }}" as="c" hint-placeholder-count="30">
                  <div onClick="{{ c.go }}" title="{{ c.title }}" style="{{ c.style }}">{{ c.n }}</div>
                </sc-for>
              </div>
            </div>
          </sc-for>
        </div>
        <div style="display: flex; gap: 20px; margin-top: 44px; padding-top: 18px; border-top: 1px solid var(--border-subtle); font-size: var(--text-caption); color: var(--text-muted); flex-wrap: wrap;">
          <div style="display: flex; align-items: center; gap: 7px;"><span style="width: 11px; height: 11px; border-radius: 3px; background: var(--forest-500); display: inline-block;"></span> All read</div>
          <div style="display: flex; align-items: center; gap: 7px;"><span style="width: 11px; height: 11px; border-radius: 3px; background: var(--forest-200); display: inline-block;"></span> Partly read</div>
          <div style="display: flex; align-items: center; gap: 7px;"><span style="width: 11px; height: 11px; border-radius: 3px; background: var(--gray-100); display: inline-block;"></span> Not yet</div>
          <div style="display: flex; align-items: center; gap: 7px;"><span style="width: 11px; height: 11px; border-radius: 3px; border: 1.5px solid var(--coral-500); box-sizing: border-box; display: inline-block;"></span> Today</div>
        </div>
      </div>
    </sc-if>

    <!-- ============ FULL PLAN (printable) ============ -->
    <sc-if value="{{ isPlan }}" hint-placeholder-val="{{ false }}">
      <div style="max-width: 760px; margin: 0 auto;">
        <div class="no-print" style="display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 34px; flex-wrap: wrap;">
          <div>
            <div style="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(--accent-soft); margin-bottom: 12px;">Full plan</div>
            <h1 style="font-family: var(--font-display); font-weight: var(--weight-regular); font-size: clamp(36px, 7vw, 58px); line-height: 1.02; letter-spacing: -0.028em; color: var(--text-strong); margin: 0;">Your reading plan</h1>
          </div>
          <x-import component-from-global-scope="GoodNewsChurchDesignSystem_929ad5.Button" variant="secondary" onClick="{{ printPlan }}" hint-size="120px,40px">Print</x-import>
        </div>
        <div class="screen-only" style="display: flex; flex-direction: column;">
          <sc-for list="{{ planRows }}" as="row" hint-placeholder-count="10">
            <div style="{{ row.style }}">
              <div style="width: 104px; flex-shrink: 0; font-family: var(--font-ui); font-weight: var(--weight-semibold); font-size: var(--text-caption); letter-spacing: 0.02em; text-transform: uppercase; color: var(--taupe-500); padding-top: 3px;">{{ row.date }}</div>
              <div style="flex: 1; font-family: var(--font-display); font-size: 17px; letter-spacing: var(--track-display); line-height: 1.5; color: var(--text-strong);">{{ row.readings }}</div>
            </div>
          </sc-for>
        </div>
        <div class="print-only">
          <div style="display: flex; justify-content: space-between; align-items: baseline; border-bottom: 1px solid #091B34; padding-bottom: 5px; margin-bottom: 8px;">
            <div style="font-family: var(--font-display); font-size: 14px; letter-spacing: -0.022em; color: #091B34;">Daily Word — a year in Scripture</div>
            <div style="font-family: var(--font-ui); font-size: 7px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: #165B48;">Good News Church &nbsp;·&nbsp; goodnewsde.com</div>
          </div>
          <div style="column-count: 6; column-gap: 9px;">
            <sc-for list="{{ planRows }}" as="row" hint-placeholder-count="10">
              <div style="font-size: 7px; line-height: 1.3; break-inside: avoid; color: #091B34;"><span style="font-weight: 700;">{{ row.date }}</span>&nbsp; {{ row.compact }}</div>
            </sc-for>
          </div>
        </div>
      </div>
    </sc-if>

  </main>

  <sc-if value="{{ showAudioBar }}" hint-placeholder-val="{{ false }}">
    <div class="no-print" style="position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 0 16px calc(24px + env(safe-area-inset-bottom)); pointer-events: none;">

      <sc-if value="{{ queueOpen }}" hint-placeholder-val="{{ false }}">
        <div style="pointer-events: auto; width: 100%; max-width: 520px; box-sizing: border-box; border-radius: 22px; padding: 16px 18px; display: flex; flex-direction: column; gap: 12px; 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-bar-in 220ms cubic-bezier(.23,1,.32,1) both;">
          <div style="display: flex; align-items: center; justify-content: space-between;">
            <div style="font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: #23303B;">{{ queueTitle }}</div>
            <div style="font-size: 11px; color: #33414D;">{{ queueMeta }}</div>
          </div>
          <div style="display: flex; flex-direction: column; gap: 7px; max-height: 40vh; overflow-y: auto;">
            <sc-for list="{{ queueItems }}" as="q" hint-placeholder-count="3">
              <button onClick="{{ q.go }}" style="{{ q.style }}" style-active="transform: scale(.99);">
                <div style="{{ q.markStyle }}">{{ q.mark }}</div>
                <div style="flex: 1; min-width: 0; text-align: left; font-family: var(--font-display); font-size: 17px; letter-spacing: -0.02em; color: {{ q.ink }}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{ q.label }}</div>
                <div style="font-size: 11px; color: #33414D; flex-shrink: 0;">{{ q.source }}</div>
              </button>
            </sc-for>
          </div>
        </div>
      </sc-if>

      <div style="pointer-events: auto; display: flex; align-items: center; gap: 12px; border-radius: 999px; padding: 8px 14px 8px 8px; max-width: 100%; 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-bar-in 260ms cubic-bezier(.23,1,.32,1) both;">
        <button onClick="{{ audioToggle }}" title="Play or pause" style-active="transform: scale(.94);" style="width: 52px; height: 52px; border-radius: 999px; border: none; background: var(--surface-navy); color: #fff; font-size: 16px; cursor: pointer; flex-shrink: 0; transition: transform 140ms cubic-bezier(.23,1,.32,1), filter 140ms ease;" style-hover="filter: brightness(1.08);">{{ audioIcon }}</button>
        <div style="min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 6px;">
          <div style="font-family: var(--font-ui); font-size: 13px; font-weight: 600; letter-spacing: -0.006em; color: #101C26; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{ audioNow }}</div>
          <div style="font-size: 11px; color: #33414D; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{{ audioStatus }}</div>
        </div>
        <div style="display: flex; align-items: center; gap: 4px; flex-shrink: 0;">
          <button onClick="{{ audioPrev }}" title="Previous chapter" style-active="transform: scale(.97);" style="width: 38px; height: 38px; border-radius: 999px; border: none; background: rgba(255,255,255,.6); color: #101C26; cursor: pointer; font-size: 14px; line-height: 1; flex-shrink: 0; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease;" style-hover="background: rgba(255,255,255,.8);">‹</button>
          <button onClick="{{ audioNext }}" title="Next chapter" style-active="transform: scale(.97);" style="width: 38px; height: 38px; border-radius: 999px; border: none; background: rgba(255,255,255,.6); color: #101C26; cursor: pointer; font-size: 14px; line-height: 1; flex-shrink: 0; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease;" style-hover="background: rgba(255,255,255,.8);">›</button>
          <button onClick="{{ cycleSpeed }}" title="Playback speed" style-active="transform: scale(.97);" style="{{ speedStyle }}">{{ speedLabel }}</button>
          <button onClick="{{ toggleFollow }}" title="Follow the words while it reads" style-active="transform: scale(.97);" style="{{ followStyle }}">Follow</button>
          <button onClick="{{ toggleQueue }}" title="Today's chapters" style-active="transform: scale(.97);" style="{{ queueBtnStyle }}">{{ queueIcon }}</button>
          <button onClick="{{ audioStop }}" title="Stop" style-active="transform: scale(.97);" style="width: 38px; height: 38px; border-radius: 999px; border: none; background: rgba(255,255,255,.6); color: #101C26; cursor: pointer; font-size: 14px; line-height: 1; flex-shrink: 0; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease;" style-hover="background: rgba(255,255,255,.8);">×</button>
        </div>
      </div>
    </div>
  </sc-if>

  <footer class="no-print" style="background: var(--surface-navy); color: var(--text-on-dark); padding: 30px 24px;">
    <div style="max-width: 1140px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; font-size: var(--text-body-sm);">
      <span style="opacity: .8;">Your plan and progress are saved on this device.</span>
      <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
        <span style="opacity: .8;">A ministry of <a href="https://goodnewsde.com" target="_blank" rel="noopener" style="color: var(--text-on-dark); text-decoration: underline;">Good News Church</a></span>
        <sc-if value="{{ hasPlan }}" hint-placeholder-val="{{ false }}">
          <button onClick="{{ resetPlan }}" style-active="transform: scale(.97);" style="border: 1px solid var(--border-on-dark); background: none; color: var(--text-on-dark); border-radius: 999px; padding: 8px 16px; min-height: 40px; font-size: var(--text-caption); cursor: pointer;" style-hover="background: rgba(255,255,255,.08);">Start over</button>
        </sc-if>
      </div>
    </div>
  </footer>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;minutesPerChapter&quot;:{&quot;editor&quot;:&quot;int&quot;,&quot;default&quot;:3,&quot;min&quot;:2,&quot;max&quot;:10,&quot;tsType&quot;:&quot;number&quot;,&quot;section&quot;:&quot;Plan&quot;}}">
class Component extends DCLogic {
  constructor(props) {
    super(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: 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, as the printed
      // calendar does.
      { name: "M'Cheyne", desc: "The classic 1842 Scottish calendar: four bookmarks — OT once, NT and Psalms twice — about 4 chapters a day.", sels: [
        { name: "Family · Genesis onward", baseName: "M'Cheyne family 1",
          books: range("Genesis","2 Chronicles"), freq: 1 },
        { name: "Family · Matthew onward", baseName: "M'Cheyne family 2",
          books: [...range("Matthew","John"), "Psalms", ...range("Acts","Revelation")], freq: 1 },
        { name: "Secret · Ezra onward", baseName: "M'Cheyne secret 1",
          books: range("Ezra","Malachi").filter(n => n !== "Psalms"), freq: 1 },
        { name: "Secret · Acts onward", baseName: "M'Cheyne secret 2",
          books: [...range("Acts","Revelation"), "Psalms", ...range("Matthew","John")], freq: 1 }] },
      { name: "New Testament in a year", desc: "A gentle pace — one chapter most days.", sels: [
        { name: "New Testament", baseName: "New Testament", books: range("Matthew","Revelation"), freq: 1 }] },
      { name: "Psalms & Proverbs monthly", desc: "The historic wisdom habit: five psalms and a proverb a day, twelve times a year.", sels: [
        { name: "Psalms", baseName: "Psalms", books: ["Psalms"], freq: 12 },
        { name: "Proverbs", baseName: "Proverbs", books: ["Proverbs"], freq: 12 }] },
      { name: "Gospels every season", desc: "Live in the life of Jesus — all four Gospels, four times a year.", sels: [
        { name: "Gospels", baseName: "Gospels", books: range("Matthew","John"), freq: 4 }] },
      { name: "Whole Bible in 90 days", desc: "Intensive: about 13 chapters a day, four passes in a year.", sels: [
        { name: "Whole Bible", baseName: "Whole Bible", books: names, freq: 4 }] }
    ];
    const saved = this.load();
    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;
  }

  LEAD = 0; // the estimate already accounts for the spoken intro

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

  load() {
    try {
      const raw = localStorage.getItem("gnc-reading-plan");
      if (!raw) return null;
      const s = JSON.parse(raw);
      if (s && s.startDate) s.viewDay = this.todayIndex(s.startDate);
      if (s && s.view === "read") s.view = "today";
      return s;
    } catch (e) { return null; }
  }
  save() {
    const { view, selections, startDate, checks, viewDay, readerFont, follow, speed } = this.state;
    try { localStorage.setItem("gnc-reading-plan", JSON.stringify({ view, selections, startDate, checks, viewDay, readerFont, follow, speed })); } catch (e) {}
  }
  setSt(patch) { this.setState(patch, () => this.save()); }
  editSt(patch) {
    if (this.state.startDate && this.state.view === "setup") this.setState(patch);
    else this.setSt(patch);
  }

  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";
  }

  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;
  }

  // ---------- day reader ----------
  // Every reading scheduled for a day, on one page: one block per plan stream,
  // each block holding its chapters as verse-tagged spans so audio can follow along.
  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." } });
    }
  }

  // api.bible JSON content → paragraphs of verse-tagged text runs
  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;
  }

  // The page may be scrolled by `window` OR by a scrollable ancestor (the app runs
  // embedded), so find whichever actually moves before tweening it.
  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);
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.activeVid && this.state.activeVid !== prevState.activeVid && this.state.follow !== false) {
      requestAnimationFrame(() => this.scrollToEl('[data-verse="' + this.state.activeVid + '"]'));
    }
  }

  // ---------- audio ----------
  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;
  }

  componentDidMount() { this.ensureAudioBible().then(() => this.forceUpdate()); }

  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;
  }

  // presigned resourceUrl expires and shouldn't be streamed directly — download, then play
  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." } });
    }
  }

  // No licensed audio edition publishes verse timecodes yet, so when they're absent
  // we spread the chapter's duration across its verses by character count. Good enough
  // to keep the page moving with the reading; swapped out automatically if real
  // timecodes ever arrive in the response.
  // rough spoken length of "Book, chapter N" — long book names take longer to say
  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;
  }

  // tap the verse the narrator is actually on — the estimate re-anchors there
  resyncTo = (e) => {
    const vid = e.currentTarget && e.currentTarget.getAttribute("data-verse");
    const tcs = this.timecodes;
    if (!vid || !tcs || !this.audioEl || !this.audioEl.src) return;
    const seg = tcs.find(x => x.vid === vid);
    if (!seg) return;
    this.syncOffset = seg.a - (this.audioEl.currentTime + this.LEAD);
    this.setState({ activeVid: vid });
  };

  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 });
  }
  componentWillUnmount() {
    if (this.audioEl) this.audioEl.pause();
    if (this.audioObjectUrl) URL.revokeObjectURL(this.audioObjectUrl);
  }

  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;
  }

  renderVals() {
    const st = this.state;
    const hasPlan = !!st.startDate;
    let view = hasPlan ? st.view : "setup";
    if (view === "read" && !st.day) view = st.prevView || "today";
    const navView = view === "read" ? (st.prevView || "today") : view;
    const steps = this.FREQ_STEPS;

    const selections = st.selections.map((sel, i) => {
      const C = this.chaptersOf(sel.books);
      const perDay = C * sel.freq / 365;
      const stepIdx = Math.max(0, steps.indexOf(sel.freq));
      return {
        name: sel.name,
        desc: sel.books.length === 1 ? C + " chapters" : sel.books.length + " books · " + C + " chapters",
        freqLabel: this.freqLabel(sel.freq),
        paceLabel: perDay >= 1 ? "≈" + (Math.round(perDay * 10) / 10) + " ch/day" : "≈" + Math.round(perDay * 7 * 10) / 10 + " ch/week",
        inc: () => { if (stepIdx < steps.length - 1) { const s = [...st.selections]; s[i] = { ...sel, freq: steps[stepIdx + 1] }; this.editSt({ selections: s }); } },
        dec: () => { if (stepIdx > 0) { const s = [...st.selections]; s[i] = { ...sel, freq: steps[stepIdx - 1] }; this.editSt({ selections: s }); } },
        remove: () => this.editSt({ selections: st.selections.filter((_, k) => k !== i) })
      };
    });

    const usedNames = new Set(st.selections.map(s => s.baseName || s.name));
    const coveredBooks = new Set(st.selections.flatMap(s => s.books));
    const presets = this.PRESETS.map(p => ({
      name: p.name,
      used: usedNames.has(p.name),
      style: "padding: 9px 16px; border-radius: 999px; font-size: 14px; letter-spacing: -0.006em; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease, border-color 140ms ease; cursor: " + (usedNames.has(p.name) ? "default" : "pointer") + "; border: 1px solid " + (usedNames.has(p.name) ? "var(--border-subtle)" : "var(--forest-200)") + "; background: " + (usedNames.has(p.name) ? "transparent" : "var(--surface-card)") + "; color: " + (usedNames.has(p.name) ? "var(--text-faint)" : "var(--forest-500)") + "; font-weight: 450;",
      add: () => { if (!usedNames.has(p.name)) this.addSelection(p.name, p.books, 1); }
    }));

    const templates = this.TEMPLATES.map(t => ({ name: t.name, label: t.name + " — " + t.desc }));
    const onTemplatePick = (e) => {
      const t = this.TEMPLATES.find(x => x.name === e.target.value);
      if (t) this.editSt({ selections: t.sels.map(s => ({ ...s, books: [...s.books] })) });
      e.target.value = "";
    };

    const bookOptions = this.BOOKS.filter(b => !usedNames.has(b[0])).map(b => ({ name: b[0], label: b[0] + " (" + b[1] + (b[1] === 1 ? " chapter)" : " chapters)") + (coveredBooks.has(b[0]) ? " — in another section" : "") }));

    const totalPerDay = st.selections.reduce((s, sel) => s + this.chaptersOf(sel.books) * sel.freq / 365, 0);
    const summaryLine = st.selections.length
      ? "About " + (Math.round(totalPerDay * 10) / 10) + " chapters a day (" + Math.round(totalPerDay * (this.props.minutesPerChapter ?? 4)) + " min)"
      : "Add something above to get started";

    const au = st.audio;
    const day = st.day;
    const dayQ = view === "read" ? this.dayAudioQueue() : [];
    const dayChapterCount = day ? day.blocks.reduce((n, b) => n + b.chapterIds.length, 0) : 0;

    const vals = {
      readerParaStyle: "font-family: var(--font-serif); line-height: 1.8; letter-spacing: 0; color: var(--text-body); margin: 0 0 " + Math.round((st.readerFont || 18) * 0.35) + "px; font-size: " + (st.readerFont || 18) + "px; text-wrap: pretty;",
      fontUp: () => this.setSt({ readerFont: Math.min(28, (st.readerFont || 18) + 2) }),
      fontDown: () => this.setSt({ readerFont: Math.max(14, (st.readerFont || 18) - 2) }),
      hasPlan, isSetup: view === "setup", isToday: view === "today", isCalendar: view === "calendar", isPlan: view === "plan", isRead: view === "read",
      selections, presets, templates, onTemplatePick, bookOptions, summaryLine, noSelections: !st.selections.length,
      bookPick: "",
      onBookPick: (e) => { const n = e.target.value; if (n) this.addSelection(n, [n], 1); },
      startBtnLabel: hasPlan ? "Save my changes" : "Start my plan",
      showDiscard: hasPlan && view === "setup",
      discardEdits: () => { const saved = this.load(); this.setState({ selections: (saved && saved.selections) || st.selections }); },
      startPlan: () => {
        if (hasPlan) { this.setSt({ view: "today", viewDay: this.todayIndex(st.startDate) }); return; }
        const d = new Date(); const iso = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
        this.setSt({ startDate: iso, checks: {}, view: "today", viewDay: 0 });
      },
      resetPlan: () => { if (confirm("Start over? Your progress will be cleared.")) this.setSt({ startDate: null, checks: {}, view: "setup", viewDay: 0, day: null }); },
      printPlan: () => window.print(),
      navTabs: [["today","Today"],["calendar","Calendar"],["plan","Full plan"],["setup","Edit plan"]].map(([v, label]) => ({
        label,
        style: "border: none; background: none; padding: 6px 0 5px; font-size: 14px; letter-spacing: -0.006em; white-space: nowrap; cursor: pointer; transition: color 160ms ease, border-color 160ms cubic-bezier(.23,1,.32,1); font-weight: " + (navView === v ? "600" : "450") + "; color: " + (navView === v ? "var(--forest-500)" : "var(--text-muted)") + "; border-bottom: 2px solid " + (navView === v ? "var(--forest-500)" : "transparent") + ";",
        go: () => {
          this.stopAudio();
          if (view === "setup" && v !== "setup" && hasPlan) { const saved = this.load(); this.setSt({ selections: (saved && saved.selections) || st.selections, view: v, day: null }); return; }
          this.setSt({ view: v, day: null });
        }
      })),
      // audio bar
      audioIcon: au && au.state === "playing" ? "❚❚" : "▶",
      audioNow: day && au && au.n ? (day.blocks[(this.audioQueue && this.audioQueue[au.i] ? this.audioQueue[au.i].bi : 0)] || {}).ref || "Today's reading" : (day ? "Today's reading" : ""),
      audioStatus: !au ? (dayQ.length && dayQ.length < dayChapterCount ? "Audio for " + dayQ.length + " of " + dayChapterCount + " chapters · " + this.audioBibleName : (this.audioBibleName ? "Audio: " + this.audioBibleName : ""))
        : au.state === "loading" ? "Loading audio…"
        : au.state === "error" ? au.error
        : au.state === "none" ? "No audio for these chapters yet"
        : au.state === "done" ? "Finished · " + (this.audioBibleName || "")
        : "Chapter " + (au.i + 1) + " of " + au.n + (this.audioBibleName ? " · " + this.audioBibleName : ""),
      audioToggle: () => {
        const a = this.state.audio;
        if (a && a.state === "playing") this.pauseAudio();
        else if (a && a.state === "paused") this.resumeAudio();
        else this.startAudio();
      },
      audioPrev: () => { if (this.audioQueue && this.audioIdx > 0) this.playAt(this.audioIdx - 1); },
      audioNext: () => { if (this.audioQueue && this.audioIdx < this.audioQueue.length - 1) this.playAt(this.audioIdx + 1); },
      audioStop: () => this.stopAudio(),
      queueOpen: !!st.queueOpen,
      queueIcon: st.queueOpen ? "⌄" : "⌃",
      toggleQueue: () => this.setState({ queueOpen: !st.queueOpen }),
      queueBtnStyle: "width: 38px; height: 38px; border-radius: 999px; border: none; cursor: pointer; font-size: 12px; line-height: 1; flex-shrink: 0; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 140ms ease; " + (st.queueOpen ? "background: rgba(255,255,255,.85); color: #101C26;" : "background: rgba(255,255,255,.6); color: #101C26;"),
      queueTitle: day ? (day.idx === this.todayIndex(st.startDate) ? "Today" : "Day " + (day.idx + 1)) + " · " + dayQ.length + " chapters" : "",
      queueMeta: au && au.n ? "Chapter " + (au.i + 1) + " of " + au.n : (this.audioBibleName || ""),
      queueItems: dayQ.map((q, qi) => {
        const playing = !!(au && (au.state === "playing" || au.state === "paused" || au.state === "loading") && au.i === qi);
        return {
          label: this.labelForCid(q.cid),
          source: (day && day.blocks[q.bi] ? day.blocks[q.bi].name : ""),
          ink: playing ? "#101C26" : "#16232E",
          mark: playing ? (au.state === "playing" ? "❚❚" : "▶") : String(qi + 1),
          markStyle: "width: 22px; flex-shrink: 0; text-align: center; font-size: " + (playing ? "10px" : "11px") + "; color: " + (playing ? "var(--surface-navy)" : "#2A3742") + ";",
          style: "display: flex; align-items: center; gap: 12px; height: 46px; padding: 0 14px; border: none; border-radius: 14px; cursor: pointer; box-sizing: border-box; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 160ms ease; background: " + (playing ? "rgba(255,255,255,.7)" : "rgba(255,255,255,.26)") + ";",
          go: () => { if (this.audioQueue) this.playAt(qi); else this.startAudio().then(() => this.playAt(qi)); }
        };
      }),
      speedLabel: (st.speed || 1) + "×",
      speedStyle: "height: 38px; padding: 0 11px; border-radius: 999px; border: none; cursor: pointer; font-size: 12px; font-weight: 600; letter-spacing: -0.006em; flex-shrink: 0; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 160ms ease; " + ((st.speed || 1) !== 1 ? "background: var(--surface-navy); color: #fff;" : "background: rgba(255,255,255,.6); color: #33414D;"),
      cycleSpeed: () => {
        const rates = [1, 1.25, 1.5, 1.75, 2, 0.75];
        const next = rates[(rates.indexOf(st.speed || 1) + 1) % rates.length];
        this.applySpeed(next);
        this.setSt({ speed: next });
      },
      toggleFollow: () => this.setSt({ follow: st.follow === false }),
      followStyle: "height: 38px; padding: 0 13px; border-radius: 999px; border: none; cursor: pointer; font-size: 12px; font-weight: 600; letter-spacing: -0.006em; flex-shrink: 0; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 160ms ease; " + (st.follow === false ? "background: rgba(255,255,255,.6); color: #33414D;" : "background: var(--surface-navy); color: #fff;"),
      showAudioBar: !!(view === "read" && day && this.audioBibleId && dayQ.length),
      audioCopyright: view === "read" && this.audioBibleId && dayQ.length ? "Audio: " + (this.audioBibleFull || this.audioBibleName) + (this.audioBibleCopyright ? " — " + this.audioBibleCopyright : "") : "",
      audioNote: view === "read" && day && this.audioReady && !dayQ.length ? "No audio for these chapters — " + (this.audioBibleFull || this.audioBibleName || "the licensed audio Bible") + " doesn't include them." : "",
      followNote: view === "read" && this.estimated && au && (au.state === "playing" || au.state === "paused") ? "Follow-along timing is estimated — this audio edition doesn't publish verse timecodes. If it drifts, tap the verse being read to resync." : ""
    };

    if (!hasPlan) return vals;

    const todayIdx = this.todayIndex(st.startDate);
    const di = Math.max(0, Math.min(364, st.viewDay));
    const fmtShort = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });

    const dayReadings = st.selections.map((sel, si) => {
      const chs = this.dayChapters(sel, di);
      if (!chs.length) return null;
      const key = "d" + di + "s" + si;
      const checked = !!st.checks[key];
      return {
        source: sel.name,
        passage: this.formatChapters(chs),
        count: chs.length + (chs.length === 1 ? " chapter" : " chapters"),
        checkMark: checked ? "✓" : "",
        checkStyle: "width: 24px; height: 24px; border-radius: 999px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; transition: background-color 140ms cubic-bezier(.23,1,.32,1), border-color 140ms cubic-bezier(.23,1,.32,1), color 140ms cubic-bezier(.23,1,.32,1); " + (checked ? "background: var(--forest-500); color: #fff; border: 1.5px solid var(--forest-500);" : "background: none; color: transparent; border: 1.5px solid var(--border-default);"),
        passageStyle: "font-family: var(--font-display); font-size: clamp(21px, 4.4vw, 27px); letter-spacing: -0.026em; line-height: 1.15; transition: color 180ms cubic-bezier(.23,1,.32,1); " + (checked ? "color: var(--text-faint); text-decoration: line-through;" : "color: var(--text-strong);"),
        cardStyle: "display: flex; align-items: center; gap: 18px; padding: 22px 24px; cursor: pointer; transition: filter 140ms cubic-bezier(.23,1,.32,1), background-color 180ms cubic-bezier(.23,1,.32,1); animation: dw-rise 260ms cubic-bezier(.23,1,.32,1) both; animation-delay: " + Math.min(si * 50, 200) + "ms; background: " + (checked ? "var(--gray-50)" : "var(--surface-card)") + ";",
        read: (e) => { e.stopPropagation(); this.openDay(di, si); },
        toggle: () => this.setSt({ checks: { ...st.checks, [key]: !checked } })
      };
    }).filter(Boolean);

    const dayDate = this.dateOf(di);
    const weekday = dayDate.toLocaleDateString("en-US", { weekday: "long" });
    vals.dayEyebrow = (di === todayIdx ? "Today" : "Day " + (di + 1)) + " · " + weekday;
    vals.dayTitle = dayDate.toLocaleDateString("en-US", { month: "long", day: "numeric" });
    vals.daySubtitle = "Day " + (di + 1) + " of 365";
    vals.dayReadings = dayReadings;
    vals.dayEmpty = !dayReadings.length;
    vals.hasDayReadings = dayReadings.length > 0;
    vals.openDayPage = () => this.openDay(di);
    vals.prevDay = () => this.setSt({ viewDay: di - 1 });
    vals.nextDay = () => this.setSt({ viewDay: di + 1 });
    vals.atStart = di === 0;
    vals.atEnd = di === 364;
    vals.chapsToday = dayReadings.reduce((s, r) => s + parseInt(r.count, 10), 0);

    let due = 0, done = 0;
    for (let i = 0; i <= todayIdx; i++) st.selections.forEach((sel, si) => {
      if (this.dayChapters(sel, i).length) { due++; if (st.checks["d" + i + "s" + si]) done++; }
    });
    const pct = due ? Math.round(done / due * 100) : 0;
    vals.progressBarStyle = "height: 100%; border-radius: 999px; background: var(--forest-500); transition: width 280ms cubic-bezier(.23,1,.32,1); width: " + pct + "%;";
    vals.pctNum = pct + "%";
    vals.streakNum = this.streak(todayIdx);
    vals.streakWord = "Day streak";

    const missed = [];
    for (let i = todayIdx - 1; i >= 0 && missed.length <= 60; i--) st.selections.forEach((sel, si) => {
      const key = "d" + i + "s" + si;
      if (st.checks[key]) return;
      const chs = this.dayChapters(sel, i);
      if (!chs.length) return;
      missed.push({
        passage: this.formatChapters(chs),
        meta: sel.name + " · " + fmtShort(this.dateOf(i)),
        read: ((idx, sidx) => (e) => { e.stopPropagation(); this.openDay(idx, sidx); })(i, si),
        toggle: () => this.setSt({ checks: { ...st.checks, [key]: true } })
      });
    });
    vals.hasMissed = missed.length > 0;
    vals.missedList = missed.slice(0, 8);
    vals.missedMore = missed.length > 8;
    vals.missedMoreCount = missed.length > 60 ? "60+" : String(missed.length - 8);
    vals.missedSummary = (missed.length > 60 ? "60+" : missed.length) + " missed reading" + (missed.length === 1 ? "" : "s");

    // day reader view values
    if (view === "read" && day) {
      const d = this.dateOf(day.idx);
      vals.readEyebrow = (day.idx === todayIdx ? "Today" : "Day " + (day.idx + 1)) + " · " + d.toLocaleDateString("en-US", { weekday: "long" });
      vals.readTitle = d.toLocaleDateString("en-US", { month: "long", day: "numeric" });
      vals.readSubtitle = day.blocks.map(b => b.ref).join("  ·  ");
      vals.readLoading = !!day.loading;
      vals.readError = !!day.error;
      vals.readErrorMsg = day.error || "";
      vals.readCopyright = day.copyright || "";
      vals.readBlocks = day.blocks.map((b, bi) => {
        const checked = !!st.checks[b.key];
        return {
          idx: bi, name: b.name, ref: b.ref,
          blockStyle: "padding-top: 34px; animation: dw-rise 300ms cubic-bezier(.23,1,.32,1) both; animation-delay: " + Math.min(bi * 60, 240) + "ms;",
          paras: b.paras.map(p => ({ spans: p.spans.map(s => ({
            vid: s.vid, t: s.t, resync: this.resyncTo,
            style: s.vid && s.vid === st.activeVid
              ? "cursor: pointer; background: var(--forest-50); box-shadow: 0 0 0 4px var(--forest-50); border-radius: 2px; transition: background-color 160ms cubic-bezier(.23,1,.32,1), box-shadow 160ms cubic-bezier(.23,1,.32,1);"
              : "cursor: pointer; transition: background-color 420ms cubic-bezier(.23,1,.32,1), box-shadow 420ms cubic-bezier(.23,1,.32,1);"
          })) })),
          checkLabel: checked ? "✓ Read" : "Mark read",
          checkStyle: "flex-shrink: 0; border-radius: 999px; padding: 9px 16px; min-height: 40px; font-size: 13px; font-weight: 450; cursor: pointer; transition: transform 140ms cubic-bezier(.23,1,.32,1), background-color 160ms cubic-bezier(.23,1,.32,1), border-color 160ms cubic-bezier(.23,1,.32,1), color 160ms cubic-bezier(.23,1,.32,1); " + (checked ? "border: 1px solid var(--forest-500); background: var(--forest-500); color: #fff;" : "border: 1px solid var(--border-default); background: none; color: var(--text-muted);"),
          toggle: () => this.setSt({ checks: { ...st.checks, [b.key]: !checked } })
        };
      });
      vals.closeDay = () => { this.stopAudio(); this.setSt({ view: st.prevView || "today", day: null }); };
      vals.markDayRead = () => {
        const checks = { ...st.checks };
        day.blocks.forEach(b => { checks[b.key] = true; });
        this.stopAudio();
        this.setSt({ checks, view: st.prevView || "today", day: null });
      };
    } else {
      vals.readBlocks = [];
      vals.closeDay = () => this.setSt({ view: "today", day: null });
      vals.markDayRead = () => {};
      vals.readLoading = false; vals.readError = false; vals.readErrorMsg = ""; vals.readCopyright = "";
      vals.readEyebrow = ""; vals.readTitle = ""; vals.readSubtitle = "";
    }

    if (view === "calendar") {
      const months = [];
      let cur = null;
      for (let i = 0; i < 365; i++) {
        const d = this.dateOf(i);
        const label = d.toLocaleDateString("en-US", { month: "long", year: "numeric" });
        if (!cur || cur.label !== label) {
          cur = { label, cells: [] };
          months.push(cur);
          for (let p = 0; p < d.getDay(); p++) cur.cells.push({ n: "", style: "aspect-ratio: 1;", go: null, title: "" });
        }
        let sched = 0, chk = 0;
        st.selections.forEach((sel, si) => { if (this.dayChapters(sel, i).length) { sched++; if (st.checks["d" + i + "s" + si]) chk++; } });
        const isToday = i === todayIdx, future = i > todayIdx;
        let bg, color;
        if (sched && chk === sched) { bg = "var(--forest-500)"; color = "#fff"; }
        else if (chk > 0) { bg = "var(--forest-200)"; color = "var(--forest-700)"; }
        else if (future) { bg = "var(--gray-50)"; color = "var(--text-faint)"; }
        else { bg = "var(--gray-100)"; color = "var(--gray-600)"; }
        cur.cells.push({
          n: d.getDate(),
          title: fmtShort(d) + " — day " + (i + 1),
          style: "aspect-ratio: 1; display: flex; align-items: center; justify-content: center; border-radius: 5px; font-size: 10.5px; cursor: pointer; box-sizing: border-box; transition: transform 120ms cubic-bezier(.23,1,.32,1); background: " + bg + "; color: " + color + ";" + (isToday ? " outline: 1.5px solid var(--coral-500); outline-offset: 1px; font-weight: 700;" : ""),
          go: ((idx) => () => this.setSt({ view: "today", viewDay: idx }))(i)
        });
      }
      vals.months = months;
    } else vals.months = [];

    if (view === "plan") {
      vals.planRows = [];
      for (let i = 0; i < 365; i++) {
        const dayChs = st.selections.map(sel => this.dayChapters(sel, i));
        const parts = dayChs.map(chs => this.formatChapters(chs)).filter(Boolean);
        const compactParts = dayChs.map(chs => this.formatChapters(chs, true)).filter(Boolean);
        vals.planRows.push({
          date: fmtShort(this.dateOf(i)),
          readings: parts.join("  ·  ") || "—",
          compact: compactParts.join(" · ") || "—",
          style: "display: flex; gap: 20px; padding: 12px 8px; border-bottom: 1px solid var(--border-subtle);" + (i === todayIdx ? " background: var(--forest-50); border-radius: 8px;" : "")
        });
      }
    } else vals.planRows = [];

    return vals;
  }
}
</script>
</body>
</html>
