# 農曆換算器 — 設計文件

## 1. 一句話

國曆與農曆雙向換算（1900-01-31 至 2100-12-31），同時給出生肖、年／月／日干支與 24 節氣時刻，
並明確區分「農曆年」（正月初一分界）與「歲次」（立春分界）。

## 2. 設計意圖

起點是一個很具體的問題：**民國 42 年（1953）2 月 4 日是農曆哪一天？怎麼確認沒算錯？**

這個日期同時踩到兩個坑，而坊間換算器多半只處理第一個：

1. 它在春節（1953-02-14）之前，所以農曆年仍是**壬辰（龍）**，不是癸巳（蛇）。
2. 它正好是**立春當天**（10:45 交節）。八字／農民曆的歲次以立春分界，
   所以同一天 10:45 之後歲次已是癸巳。

因此本工具的核心不只是「換算」，而是**把這兩種年份分界同時攤在檯面上**：
兩者不一致時主動跳出提醒，並附上該年立春的精確時刻。

其餘設計取捨：

- **純前端、無建置**：跟站上其他工具一致，vanilla JS + ES module。換算完全在瀏覽器內
  完成，使用者輸入的日期不會被送到本站的任何伺服器。

  > **但「資料不上傳」不等於「日期不會外流」**（待裁決，見下）。`app.js` 會用
  > `history.replaceState` 把查詢日期寫進網址列的 `?date=`（這是刻意的，為了讓結果可
  > 分享），而本頁跟站上其他頁一樣載入 GTM。GA4 的 Enhanced Measurement 預設會監聽
  > History API 變化並把含查詢字串的 `page_location` 一起送出——若該容器開著這個預設，
  > 使用者查的生日就會進到 Analytics。GTM 容器的實際設定不在本 repo 內，無法從程式碼
  > 確認，需要在 GTM 後台查。三個選項：(a) 確認容器沒開 history-change 追蹤就維持現狀；
  > (b) 在 GTM 端把 `date` 參數從 `page_location` 濾掉；(c) 改用不含日期的分享機制。
  > index.html 的 `<meta name="description">` 目前寫「資料不上傳」，若選 (a) 之外的
  > 方向，那句也要一起改。
- **查表而非瀏覽器內天文計算**：曆表離線算好進 repo（24 KB，gzip 14 KB），
  頁面端 O(1) 查表。代價是範圍鎖死 1900-01-31（農曆庚子年正月初一，曆表的第一天）至 2100-12-31，
  超出範圍**明確拒絕**，不猜——注意下界不是 1900-01-01，1900-01-01～01-30 會被拒絕。
- **曆法邏輯與 DOM 完全分離**：`lunar-core.js` 是純函數，可直接單元測試；
  `app.js` 只做介面。

## 3. 現況實作

| 檔案 | 角色 |
|------|------|
| `index.html` | 頁面骨架，遵循站上工具的 GTM／consent／`include.js` boilerplate。農曆輸入面板放在 `<template>` 裡（見 §4 第 4 層） |
| `style.css` | 樣式，色彩接site theme token，支援深色模式 |
| `lunar-core.js` | 純邏輯：換算、干支、節氣查詢。無 DOM 依賴 |
| `lunar-data.js` | **機器產生**的 1900–2100 曆表，請勿手改；重產後三處 `?v=` 要換新值（見 `app.js` 那一列） |
| `app.js` | 介面層：輸入、渲染、`?date=` 網址狀態、切換模式時把輸入面板換進／換出文件。`index.html` 引用它、它 import `lunar-core.js`、`lunar-core.js` 再 import `lunar-data.js`，三處都帶同一個 `?v=` 版本參數；改了這三支 JS 任何一支（含重產曆表）三處要一起換，理由見 `index.html` 結尾的註解 |
| `../../scripts/generate-lunar-data.py` | 離線資料產生器（需 pyephem），**不在 `npm test` 內** |
| `../../scripts/verify-lunar-data.py` | 獨立複核器（Meeus 第 49 章，純標準函式庫）。CI 會單獨跑它複核真實曆表；`npm test` 只用 `tests/verifyLunarDataExitCode.js` 拿改壞的曆表副本確認它的離開碼 |
| `../../tests/lunarCalendarPage.js` | 頁面測試：jsdom 驗介面接線，以及「任何時刻文件裡只有一個輸入面板」 |
| `../../tests/lunarCalendar.js` | 曆法正確性：公認春節／閏月年、全範圍 73,384 天不變量 |
| `../../tests/lib/cssRuleParser.js` | 解析 CSS 規則與選擇器。**不是這個工具專用的**：全站好幾支守門共用它（`grep -rl cssRuleParser tests` 列得出來），改它的解析行為要把那幾支都跑過 |
| `../../tests/lib/headlessProbe.js` | 用系統 Chrome 的 headless 量「瀏覽器實際算出來的樣式」。這個工具的測試**已經不用它**（見 §4 第 4 層），留著給全站的顏色對比守門與 §5 的深色顏色量測用；它在真實 Chrome 上的前提（視窗寬度、色彩偏好旗標、compatMode）由 `tests/colorContrastPage.js` 每次量測時驗 |
| `../../tests/ciConfigUnit.js` | 把 `ci.yml` 去掉註解後實際生效的那二十幾行整份釘住（chromium 的版號除外）：複核那一步在、排在 `npm test` 之前、兩步都沒有被改寫成失敗也放行。workflow 的任何修改都會紅在這裡，要看過再更新它釘的內容。§4 第 1 層說它是會擋下合併的守門，靠這支釘住——沒有它，那一步被刪掉全套照樣綠 |

### 渲染失敗時畫面長什麼樣

換算本身不會失敗（純查表），但**頁面可能根本沒跑起來**。三類情境各有一道防線，
設計決策記在這裡，不要只靠讀測試檔逆推：

| 情境 | 防線 | 使用者看到 |
|---|---|---|
| ES module 載入失敗（網路、CSP、瀏覽器太舊） | `index.html` 的 5 秒逾時守門，檢查 `output.dataset.lcReady` | 明確的「載入失敗，請重新整理」，不是永遠空白 |
| 渲染期拋非預期例外 | `renderGuarded()` 的 try/catch | `.lc-error` 訊息，且輸入欄的 `aria-invalid`／`aria-describedby` 一併收掉 |
| 連建立 `<p>` 都失敗 | `renderGuarded()` 內層 try/catch 降級成純文字 | 純文字錯誤訊息 |

**`output.dataset.lcError` 是狀態旗標，不能只靠 `.lc-error` 元素判斷。**
降級路徑不會產生任何元素，只靠 `querySelector('.lc-error')` 的話，錯誤狀態的兩條
復原路徑（清空欄位還原上一筆結果、切換模式清畫面）都會失效，使用者永遠卡在錯誤訊息上
只能重新整理。**每一條會清掉錯誤畫面的路徑都必須同時 `delete dataset.lcError`**
——`setMode()` 與 `change` 監聽器的兩個分支都踩過這個坑，各修過一次。

### 曆表編碼

- `YEARS[i]`（農曆 1900+i 年）＝ `<正月初一於該公曆年的日序 base36 2 碼><閏月號 base36 1 碼>
  <各月大小 4 碼 hex，bit i = 1 表第 i 個月 30 天>`。
  正月初一**逐年明寫**而非從 1900 年累加，任何一年出錯都不會污染其他年份。
- `TERMS[i]`（公曆 1900+i 年）＝ 24 個節氣各 4 碼 base36，值為 `日序 * 1440 + 當日分鐘`。

### 演算法

1. **定朔**：合朔時刻所在的 UTC+8 當地日期為初一。
2. **定氣**：含冬至（太陽視黃經 270°）的朔望月為十一月。
3. **置閏**：兩個十一月之間若有 13 個月，第一個不含中氣者為閏月。
4. **干支**：日干支以 60 日循環（錨點 1900-01-01 為甲戌日）；年干支以正月初一分界；
   歲次與月建以節氣分界（立春起寅月），月干由年上起月法推出。

⚠️ **交節當日不給單一答案，而是並列交節前後兩組。** 歲柱以立春、月柱以各月的「節」
分界，分界點是**時刻**不是日期——1953-02-04 立春 10:45，同一天 09:00 出生是壬辰年癸丑月、
11:00 出生是癸巳年甲寅月。本工具的輸入只有日期、沒有時刻，所以交節當日的「歲次干支」與
「月干支」會顯示成「交節前 → 交節後（當日交節）」，並在提示裡附上該次交節的精確時刻；
沒有出生時刻就無法斷定是哪一組，工具不替使用者猜。非交節日則照常只有一組。

## 4. 怎麼確認沒算錯

這是本工具最花工夫的部分。驗證分四層：

1. **兩套獨立天文實作互校**（可重跑：`python3 scripts/verify-lunar-data.py`）。
   曆表由 pyephem（VSOP87／ELP 星曆）產生；`scripts/verify-lunar-data.py` 則以
   Meeus《Astronomical Algorithms》第 49 章的截斷級數**自行重算**每一次合朔，
   只用 Python 3 標準函式庫、完全不碰 pyephem，再逐一比對曆表裡的每個初一。
   最近一次結果：**曆表的 2,486 個初一全部相符，零例外**。
   加 `--stats`（需 `pip install ephem`）會另外印出兩套實作的合朔時刻差異：
   n=2,487，中位 4.9 秒、95 百分位 24.4 秒、最大 34.8 秒。
   腳本同時會列出**合朔落在當地午夜後 15 分鐘內的 26 個邊界日**——這些是時區定義
   一有出入就會差一天的日子，下面〈已知且刻意的差異〉那 5 天全在其中。
   這支腳本對真實曆表的複核不在 `npm test` 流程內（`npm test` 只拿曆表的副本驗它的離開碼，見 `tests/verifyLunarDataExitCode.js`），但**CI 會以獨立步驟跑它**（容器裡另外裝 python3），
   所以它是會擋下合併的守門，不只是人工工具——改壞曆表時 `npm test` 的每一支農曆
   測試都照樣 PASS，只有這支會紅。
   這一層現在**同時複核定朔與節氣**：
   - **定朔**（`YEARS`）以 Meeus 第 49 章重算，2,486 個初一全部相符。
   - **節氣**（`TERMS`，4,824 個值）以 Meeus 第 25 章的太陽視黃經重算。該解法的黃經
     誤差約 0.01 度（約 15 分鐘），所以**只複核到「哪一天」**，時間差則以 25 分鐘為
     容許值。實測全數相符：時間差中位 3.3 分、95 百分位 8.8 分、最大 13.9 分。
     另有 **13 筆貼著當地午夜**（如 2016 小暑 00:03、2097 立夏 00:07），本公式的精度
     決定不了它們該算哪一天，腳本會單獨列出並**不**判為錯誤。
   突變驗證：把 2000 年立春從 2/4 20:40 改成 2/5 01:40（整整位移一天），腳本回報
   「差 304.1 分」並以 exit 1 結束——這正是先前這一層看不到的那個缺口。

   **這支複核器自己怎麼證明不是恆真的**：`main()` 一開始會無條件跑 `self_test()`，
   在記憶體裡把曆表副本逐一弄壞（中段月長、末年月長、閏月號記錯、閏月漏記、節氣時刻、定朔比對），確認每道對帳都會報錯、
   且原資料下不報錯——兩側都成立才算數；最後再確認出口判斷 `verdict()` 對每一類錯誤都判失敗、全空時才放行。曾經改用「對帳年數相加等於曆表年數」當守門，
   但那兩個數字結構上是寫死的常數（200 與 1），相加恆等於 201，「不相等就失敗」永遠不會發生，是個永遠通過的假守門。

   同一層另有兩道對帳，補的都是「朔比對結構性看不到」的位元：

   - **月長合計**（201 個 bit）。每年最後一個月的大小碼不影響任何一個初一，所以朔比對
     對它失明。拿「走完全年」的落點跟次年正月初一對帳補起前 200 個，最後一年改拿 Meeus
     獨立算出的合朔收尾。
   - **閏月號**（74 個值）。第 3 碼原本只被用來決定該年是 12 還是 13 個月，「閏的是幾月」
     沒有任何一層在驗——實測把 2004 年由閏二月改成閏十一月，本腳本與
     `tests/lunarCalendar.js` 都照樣印綠勾，但那一年有 266 天的月份標示錯位。
     現在照本文件第 3 節的規則獨立推導：含冬至的朔望月為十一月，相鄰兩個十一月之間
     若有 13 個月，其中第一個不含中氣者為閏月。**錨點必須是冬至月而不是正月**——
     以正月起算的簡化版會把 2033 年推成「閏七月」而非公認的閏十一月（著名的 2033 年
     問題），也會誤判 1985／2034／2053。突變驗證：74 個閏月號逐一改成鄰近月號，
     74 個全部被抓到。

2. **公認歷史事實**。`tests/lunarCalendar.js` 逐項比對 10 個廣為人知的春節日期
   （1912-02-18、1949-01-29、2000-02-05、2024-02-10 …）、5 個公認閏月年份
   （含著名的 2033 閏十一月），以及 1949-10-01 為**己丑年癸酉月甲子日**
   ——年、月、日三柱同時對上，等於一次驗證三組獨立公式。
3. **曆法不變量**。全範圍 **73,384 天**逐日檢查：往返換算一致、農曆日連續遞增、
   月長只有 29／30 天、小月不接受三十日、節氣間距 14–16 天、立春恆在 2/3–2/5、
   冬至恆在 12/20–12/23。初一之間的平均間隔為 **29.5304 天**，與朔望月
   29.5306 天相符——這個數字是整份曆表的總體健檢。
4. **頁面接線與「只有一組輸入面板」**。`tests/lunarCalendarPage.js` 用 jsdom 實跑頁面，確認 HTML 與
   `app.js` 的元素 id 對得上、立春提醒只在該出現時出現，以及兩組輸入面板不會同時出現。

   最後這一項不靠量 CSS，靠結構：**非使用中的輸入面板不在文件裡。** 農曆面板在 `index.html` 裡放在
   `<template id="panel-lunar-template">`，`app.js` 初始化時複製一份停進一個不接上文件的
   DocumentFragment；`setMode()` 切換時把要用的面板換進文件、原本那一組換回 DocumentFragment。
   不在文件裡的節點沒有任何樣式選得到，所以不管樣式表怎麼寫——`display … !important`、`@media`、
   深色主題、`:hover`、容器查詢——都不可能讓隱藏的那一組顯示出來。欄位值與事件監聽跟著節點走，
   切走再切回不會遺失；停放中的欄位照樣會被回填（`app.js` 的 `el()` 先找文件、找不到再找停放處）。

   測試守的就是這個結構：每一步切換、換算、錯誤狀態之後，文件裡都只有預期的那一個輸入面板；
   `index.html` 裡農曆面板只在 `<template>` 裡；留在文件裡的那一組面板不帶 `hidden` 屬性（改回用 `hidden` 切換會紅）。

   反方向「使用中的那一組被 CSS 藏起來」改用一道不啟動瀏覽器的靜態掃描守（owner 2026-10-15 的選擇）：
   `style.css` 與 `css/site-chrome.css` 裡，選擇器（含巢狀規則的祖先、巢在樣式規則裡的 at-rule 所屬的規則）
   提到 `.lc-panel`、`.lc-fields`／`.lc-field` 或面板與欄位的 id 的規則，不准宣告 `display: none`，
   寫在任何 `@media` 裡的也算。

   **沒有守的**（取捨見下面〈兩組輸入面板的守門：從量 CSS 改成不放進文件〉）：
   - 上面那道靜態掃描看不到的藏法：不提這些名稱的選擇器（`.lc-root > div`、`:has()` 之類）、`visibility`／
     `opacity`／零尺寸、跳脫寫法、HTML 的 `style` 屬性與腳本設的樣式。它比的是字面，不算層疊。
     這類錯至少一眼看得到（整個輸入區消失），不是兩組面板重疊那種只在某些寬度、某些主題才出現的錯。
   - Safari、Firefox 的行為（切換、上一頁／重新整理後的表單狀態、焦點）沒有自動化；要靠人工試，結果記在 WORKLOG。
     截至 2026-10-15 還沒有試過；owner 決定先列為 `TODO.md` 的待辦，不擋合併。

### 已知且刻意的差異：5 個「午夜邊界日」

本工具與部分坊間換算器（採用 1900–2100 壓縮曆表的那一類）在 **5 個日期**上差一天：

| 合朔時刻（UTC+8） | 本工具的初一 | 坊間表的初一 |
|---|---|---|
| 1914-11-18 00:01:42 | 11/18 | 11/17 |
| 1916-02-04 00:05:15 | 02/04 | 02/03 |
| 1920-11-11 00:04:48 | 11/11 | 11/10 |
| 1933-07-23 00:03:01 | 07/23 | 07/22 |
| 1978-09-03 00:08:52 | 09/03 | 09/02 |

表中時刻與 `scripts/verify-lunar-data.py`（Meeus 第 49 章）重算的結果差 1–3 秒
（例如 1914-11-18 腳本給 00:01:44），不影響落在哪一天。

原因不是誤差，而是**時區定義不同**：這 5 次合朔都落在 UTC+8 午夜後 1.7–8.9 分鐘，
換成**北京地方平時（UTC+7:46）**就全部退回前一天——5 個案例無一例外，
這正是那批舊曆表的時區。本工具一律採 UTC+8，與
GB/T 33661《農曆的編算和頒行》及香港天文台曆表一致。

另外，用來對照的 `lunardate` 套件在 **1954-11-25**（合朔 20:30，離午夜三個半小時，
無時區爭議）給出的是 11/26，以及 2100 年整年的年份欄位錯亂——這兩處是該套件自身的
資料問題，不是邊界爭議。

### 兩組輸入面板的守門：從量 CSS 改成不放進文件

原本兩組輸入面板都留在文件裡、用 `hidden` 屬性切換，於是「沒有任何 CSS 能把隱藏面板顯示出來」要另外守：
在真實 headless Chrome 裡依樣式表推導出的寬度與淺／深兩主題逐一量面板的 `display`，加上一層靜態掃描
（PR #307，2026-10-15 合併）。當時 `tests/lunarCalendarPage.js` 的時間幾乎全花在啟動瀏覽器上（2026-10-11 在
load average 約 10 的機器上量到 14–16 秒，其中不啟動瀏覽器的部分一兩秒；每跑一次啟動 10 次），而且那套守門
仍有一串已知盲區（`:hover`、低於 500px 的斷點、`@container`、`@supports` 內的寬度條件、runtime 才產生的 DOM 狀態等）。

<!-- 本段反單引號成對：共 34 個、17 個 code span。2026-10-15 以 GitHub markdown API（mode=gfm）與 marked 實測渲染皆無落單的反單引號。 -->
它防的事有多常發生：截至 2026-10-11，全站 CSS 的歷史上沒有任何一筆 commit 加過值不是 `none` 的
`display … !important`（`git log -G'display[[:space:]]*:[[:space:]]*(flex|grid|block|inline)[^;]*!important' origin/master -- '*.css'`
查到 0 筆。空白要寫 `[[:space:]]`：macOS 的 git 不認 `\s`，寫成 `\s` 的話這條指令不管有沒有都回 0 筆。
要確認指令本身有效，把值換成 `none` 再查一次：`tools/lunar-calendar/style.css` 加入重設規則的那筆 commit
所在的分支上，`[[:space:]]` 的寫法查得到它，`\s` 的寫法仍是 0 筆。2026-10-15 對 `origin/master` `af05d900`
重查：非 `none` 的仍是 0 筆；那筆 commit 已隨 #307 進 master，對照組現在直接對 `origin/master` 查就查得到，
`[[:space:]]` 寫法回 `af05d900`、`\s` 寫法 0 筆）。

2026-10-11 的反方審查提出兩條替代做法；#307 合併後 owner 把第一條列為後續工作（`TODO.md`「農曆換算器 #307
合併後的三件後續工作」的 B），可刪範圍由 C（#343）先盤點：

1. **非使用中的面板不放進文件**（採用，見 §4 第 4 層）。產品碼只動 `app.js` 的 `el()`、`setMode()`、`init()` 三處，
   加上 `index.html` 的 `<template>`，並拿掉 `style.css` 的 `[hidden]` 重設規則。實際處置：
   - **刪掉**（約 2,700 行，2026-10-15 以 `git diff --numstat` 計）：`tests/lib/cssGuards.js`、`tests/cssGuardsUnit.js`、
     `tests/lib/pageStylesheets.js`、`tests/pageStylesheetsUnit.js`、`tests/lunarCalendarPage.js` 量面板的那一段與為它服務的
     常數（`RUNTIME_STYLESHEETS` 與它跟 `include.js` 的對帳、`MEDIA_EM_PX`、`SAMPLED_MEDIA_FEATURES`），以及
     `tests/lib/cssRuleParser.js` 裡只有它們在用的 `mediaWidthBreakpoints`／`mediaFeatures`／`selectsHiddenAttribute`
     與其單元測試。
   - **搬走**：`linksStylesheet`（判斷 `<link>` 會不會被當成樣式表套用）連同它在 `pageStylesheetsUnit.js` 的正反例，
     搬到 `tests/lib/headlessProbe.js`（與它依賴的 `DEFERRED_STYLESHEET_ONLOAD` 放一起）與 `tests/headlessProbeUnit.js`；
     `tests/lunarCalendarPage.js`「style.css 必須真的被當成樣式表套用」那條斷言照用。
   - **探針前提的金絲雀**：色彩偏好旗標、`data-theme`、quirks mode 這幾條 `tests/colorContrastPage.js` 每一輪本來就檢查，
     不用搬。C 列為「待 owner 判斷」的視窗寬度那條（實得 `innerWidth` 等於要求的寬度），owner 2026-10-16 選定併進
     `tests/colorContrastPage.js` 既有的啟動（不多開 Chrome）。媒體查詢 1em 那條只對量面板的多寬度取樣有意義，沒有搬。
   - **代價**：①測試裡以 id 取農曆欄位的地方改成取元素參照（`captureFields()`／`field()`）。②失去「`style.css`
     在瀏覽器裡真的套用」的確認：href、檔案在不在、禁止 `<base>` 這些靜態檢查還在，但 `style.css` 沒讓 `.lc-root`
     拿到樣式（規則被刪、選擇器改名、CSS 解析錯誤），或靜態的 `linksStylesheet` 判斷與瀏覽器認定不一致的寫法，
     這一頁已經沒有任何瀏覽器測試看得到；要補就把農曆頁加進 `tests/colorContrastPage.js` 的頁面清單（多一次啟動）。
     ③「使用中的面板不是 `display: none`」從瀏覽器量測退成字面的靜態掃描（見 §4 第 4 層）。④Safari／Firefox 與
     瀏覽器的表單自動還原沒有自動化測試（列在 `TODO.md`）。
2. **不算層疊，直接禁掉機制。**（沒有採用） 作者樣式要勝過頂層的 `display: none !important`，只能再寫一條帶
   `!important` 的 `display`／`all`。所以幾十行、不啟動瀏覽器的文字掃描（重設規則在頂層且帶
   `!important`；本頁載入的本地 CSS 沒有任何值不是 `none` 的 `display`／`all` `!important`、沒有
   `@import` 與反斜線跳脫；樣式表清單沒有變動）就擋得住，而且**下面列為盲區的 `:hover` 與低於 500px
   的斷點它也擋得住**——現行的瀏覽器量測在這兩種寫法上是放行的。代價：本頁與全站共用的
   `css/site-chrome.css` 從此不能出現任何非 `none` 的 `display … !important`，而且依據是對層疊規則的
   推理、不是量測。

**留下來的**：`tests/lib/headlessProbe.js` 與 `tests/headlessProbeUnit.js` 不屬於這套守門——`tests/colorContrastPage.js`
（#342，2026-10-15 合併；依據是 `WORKLOG.md` 2026-09-25「PR #284：手刻對比守門凍結」一節記的 owner 裁決）每一輪都在用，
§5 也打算拿它量本頁的深色顏色。所以 CI 照樣裝釘版的 chromium，本機 `npm test` 照樣需要 Chrome。
`tests/lib/cssRuleParser.js` 另有 `tests/docsExportDarkMode.js` 等測試在用，也留著。

**被移除的實作要找回來的話**，在 #307 的合併 commit `af05d900`（含面板守門的最後版本）上都還在，例如
`git show af05d900:tests/lib/pageStylesheets.js`（收齊頁面的 `<link>`／`<style>`、include.js 注入的樣式表與遞迴 `@import`）、
`git show af05d900:tests/lunarCalendarPage.js`（用探針在淺／深兩主題量面板的完整呼叫，以及 `RUNTIME_STYLESHEETS`
與 `js/include.js` 實際注入的樣式表逐一對帳、脫節就紅的那段）、`git show af05d900:tools/lunar-calendar/DESIGN.md`
（那套守門逐條的已知盲區）。之後誰要再用探針量本站頁面，要自己把 include.js 注入的樣式表交給
`measureInChrome` 的 `extraStylesheets`——現在沒有任何檢查確認這份清單跟 include.js 同步。

### 深色顏色斷言的已知盲區

**深色顏色對比的三條斷言（`tests/lunarCalendarPage.js` 的第 13、13b、13c 項）讀的是樣式表原始碼，
不是瀏覽器算出來的值。** 同一個選擇器的規則（含選擇器群組、包在 at-rule 裡的）會一起看；同一個屬性出現
兩個不同的值時直接失敗、不去猜哪一個生效。看的是「字面上同一個選擇器」底下的宣告，所以模擬不到的不只
`!important` 與更高權重：**同權重、換個寫法、排在後面**就夠了（`[data-theme="dark"]:root .lc-root`、
`.lc-method :is(a)`），在後代元素上重設同一個變數、用 `background-image` 蓋掉底色也都看不到（這幾種都實測過：
測試全綠，而瀏覽器在深色主題算出來的邊框、底色或連結顏色變了）。根本的做法是用 `tests/lib/headlessProbe.js`
讀 `getComputedStyle(...).getPropertyValue('--lc-border')`，那是減法（可以整組刪掉手工層疊模擬）——尚未進行，見 §5。

## 5. 待辦

- 農曆生日「每年對應到哪一天」的查詢（同一個 `lunar-core` 就能做）。
- 節氣區塊目前只列當年；可考慮加上「下一個節氣還有幾天」。
- 曆表上限 2100 年底；要延長需改 `generate-lunar-data.py` 的 `LAST_YEAR` 重跑，
  但更遠年份的星曆精度會退化，屆時需重新評估驗證方式。年限寫死在不只一處，要一起改：
  產生器自己在 `build_month_table()` 裡寫死的合朔視窗（`'1898/11/01'`、`'2102/06/01'` 與
  `range(1898, 2102)`，要涵蓋到新終點之後一年的正月初一，不跟著 `LAST_YEAR` 走）與檔頭文字、
  `scripts/verify-lunar-data.py` 自己的 `LAST_YEAR`（筆數檢查用；它的 `delta_t()` 對遠年份也要重看）、
  `lunar-core.js` 的 `MAX_SOLAR`、`index.html` 的副標題與 meta description、本文件開頭與 §2 的日期
  （後三處與 `MAX_SOLAR` 不一致時 `tests/lunarCalendarPage.js` 會紅）、本文件 §3 檔案表裡的「1900–2100 曆表」與
  `lunar-core.js` 兩處 JSDoc 的「（1900-2100）」（這兩處沒有測試看著）、`tests/lunarCalendar.js` 的範圍與
  天數、`tests/lunarCalendarPage.js` 裡拿來當「範圍外」輸入的 `2101-06-01`，以及 §4 裡跟著年數走的
  各個計數（初一、節氣、閏月的個數），以及重產曆表後 `index.html`、`app.js`、`lunar-core.js` 三處的 `?v=` 版本參數（見 §3 `app.js` 那一列；沒換的話網址不變，回訪者會拿到快取裡的舊曆表）。
- **深色顏色的三條斷言（`tests/lunarCalendarPage.js` 的第 13、13b、13c 項）改成讀瀏覽器算出來的值。**
  現在讀的是樣式表原始碼（見 §4〈深色顏色斷言的已知盲區〉）。這支測試已經不啟動瀏覽器，要做的話是用
  `tests/lib/headlessProbe.js` 在淺／深兩主題各啟動一次讀 `getComputedStyle`，換掉手工的模擬（例如把農曆頁加進
  `tests/colorContrastPage.js` 的頁面清單，該檔每頁每個主題各啟動一次）——尚未進行。
- 八字四柱（含時柱與真太陽時）刻意不做——正確性難以驗證，且與本工具「可查證」的定位衝突。
