/* ==========================================================================
   台南鹽埕林姓宗親會｜B1 設計系統 ＋ 全站共用元件
   --------------------------------------------------------------------------
   🔴 視覺來源＝舊版 demo（已於 2026-08-17 刪除，見 CLAUDE.md 5.2 的取回指令）。
      2026-08-13 客戶看過多頁後表示比較喜歡
      demo 的風格，PM 定案「視覺與排版整套改用 demo 的」，Figma 首頁稿不再是依據。
      ⚠️ 這連帶推翻了客戶自己提供的 參考色票.jpg（深綠系）——是客戶的選擇，
         不是我方偏離需求。舊色票已作廢，不要「修正」回去。

   ⚠️ demo 的字面不可照抄的幾處見 CLAUDE.md 6.4（站名林姓／導覽六項／eyebrow 色／
      版權列字級／clamp() 字級）。
   ⚠️ 這裡是全站唯一的樣式來源。頁面專屬樣式才寫進同檔名 .css。
   ⚠️ 間距只能來自本檔變數；不寫 inline style、不用 !important。
   ========================================================================== */

/* ==========================================================================
   1. 色票（2026-08-13 R1a 整套更換：奶油白／珊瑚粉／咖啡棕）
   --------------------------------------------------------------------------
   對比度全部本機實測（WCAG AA：內文 4.5:1、大字與非文字元件 3:1），見 CLAUDE.md 3.1。

   🔴 命名不照抄 demo 的變數名，兩個是地雷：
      ① demo 的 --maroon-800 是 #656565 純灰，名字說棕、值是灰，
         而且它同時當內文色「和」主按鈕 hover 底色 → 主按鈕 hover 會變灰。那是 demo 的 bug。
      ② demo 的 --coral-400 #FFB3AE 與 --coral-500 #FFAEA1 差 1.6%，肉眼無差 → 合併成一階。
   ========================================================================== */
:root {
  /* --- 主色票（取自 demo，名稱與實際色相一致） --- */
  --c-maroon: #4A2827;  /* 咖啡棕．主色：標題、主 CTA 底、頁尾底 */
  --c-cream:  #FEFAF5;  /* 奶油白．頁面背景（60%） */
  --c-sand:   #FBF1E6;  /* 奶油．區塊底、卡片底、圓形插圖底 */
  --c-coral:  #FFAEA1;  /* 珊瑚粉．強調色（10%）🔴 只能當裝飾底色與圖形 */
  --c-berry:  #A8555F;  /* 莓紅．eyebrow 與粉調文字唯一可用的一階 */
  /* 🔴 2026-08-14 R2i：內文色 #656565 → #4A3F3D，對比 5.61 → 9.76（AA 最低標 → AAA）。
     ⚠️ 原本的問題**不只是淺，還是「純灰用在暖色底上」**——色溫不搭會讓人覺得更淺。
        改成帶褐調的深灰後兩件事一起解決。
     ✅ 與標題的咖啡棕（12.44）仍有階層差，不會變成標題內文一樣重。
     **被否決**：直接用標題的咖啡棕當內文（12.44）／`#3D3330`（11.79）——階層都會消失。 */
  --c-ink:    #4A3F3D;  /* 內文深灰（帶褐調） */
  --c-line:   #F0E2D6;  /* 奶油調分隔線 ⚠️ 只有 1.1:1，純裝飾，不可當表單外框 */
  --c-white:  #ffffff;

  /* 🔴 珊瑚粉不得承載任何文字，只能當裝飾底色與圖形：
        白字 on 珊瑚粉 1.78:1、珊瑚粉字 on 奶油白 1.71:1，兩者都遠低於門檻。
        demo 沒踩到按鈕那一半（它的 CTA 用咖啡棕底），但它的 eyebrow 全部用
        #fc938e（2.08:1）→ 我方一律改用莓紅，見 --text-eyebrow。
     ⚠️ 這與舊色票的「蜜桃橘與淡青不得承載文字」是同一條規則換了色值，規則沒有放寬。 */

  /* --- 語意色：文字 --- */
  --text-main:      var(--c-ink);     /* 內文．on 奶油白 5.61／on 奶油 5.23／on 白 5.83 ✅ */
  --text-heading:   var(--c-maroon);  /* 標題．on 奶油白 12.44 ✅（比舊的深綠 8.90 更好） */
  --text-eyebrow:   var(--c-berry);   /* 小標．on 奶油白 4.89／on 奶油 4.56 ✅ */
  --text-brand:     var(--c-maroon);  /* 站名．on 白 12.93 ✅ */
  --text-inverse:   var(--c-cream);   /* 深底上的文字．on 咖啡棕 12.44 ✅ */
  --text-inverse-soft: #E7D8CE;       /* 深底上的次要文字．on 咖啡棕 9.31 ✅
                                         ⚠️ 不用 demo 的 rgba(255,255,255,.85)：
                                            透明度會隨底色漂移，換底就得重驗一次 */

  /* 🔴 暖底（珊瑚粉）上的文字色 —— 本案對比度鐵則的單一開關
     咖啡棕 on 珊瑚粉 7.28:1 ✅；白字只有 1.78:1，列在 CLAUDE.md 3.1「❌ 不可用」。
     ⚠️ 要改只改這一行，全站生效。 */
  --text-on-warm:   var(--c-maroon);

  /* --- 語意色：底 --- */
  --bg-page:        var(--c-cream);   /* 奶油白 */
  --bg-alt:         var(--c-sand);    /* 奶油．交錯區塊底（demo 的 --color-surface-alt）
                                         ⚠️ 原名 --bg-cream，08-13 改名：奶油白已經是頁面底，
                                            再叫 cream 會分不出誰是誰 */
  --bg-card:        var(--c-white);
  --bg-footer:      var(--c-maroon);  /* demo 的頁尾是咖啡棕底＋奶油白字 */

  /* 圖上遮罩：暖色調不用純黑（demo 用 rgba(46,25,25,…)）
     ⚠️ 濃度不是抄來的，是回推的——白字要 ≥4.5:1，最亮的照片也得撐得住。 */
  /* 🔴 濃度是回推的，不是抄 demo 的：
     假設照片最亮處＝純白，白字要 ≥4.5:1 → 遮罩至少 0.65。
     實測 a=0.50 只有 3.27:1，而 demo 的漸層在 45% 處正好是 0.45——
     文字尾端就落在那一段，長輩在戶外強光下讀不到。
     ⚠️ 文字區在容器內佔到約 50% 寬，所以 0%–50% 一律 ≥0.68，之後才淡出給照片。 */
  --overlay-hero: linear-gradient(90deg,
                    rgba(46, 25, 25, 0.78) 0%,
                    rgba(46, 25, 25, 0.68) 50%,
                    rgba(46, 25, 25, 0.32) 100%);
  --overlay-cta:  rgba(46, 25, 25, 0.72);   /* 置中文字，整片都要撐得住．白字 6.62:1 ✅ */

  /* 🔴 「文字壓在照片上」的通用遮罩（2026-08-17 R4d：關於我們的創辦人的話）。
     **與 `--overlay-cta` 同值是算出來的，不是抄的**：兩處都是「奶油白／白字 ＋ 任意裁切的照片」，
     所以都要由「最壞情況＝底色純白」回推 → 小字 4.5:1 需 alpha ≥ 0.62，取 0.72 留餘裕（6.35:1）。
     🔴 **這裡刻意是「平的」不是漸層**（內頁 hero 是 0.68→0.76 漸層）：
        `background-attachment: fixed` 之下，**裁到照片的哪一段會隨視窗高度改變**，
        照亮度分布調出來的漸層在別的視窗尺寸就對不上了。平的才是唯一與裁切無關的做法。
     ⚠️ `2273434843.jpg` 實測最亮處亮度 0.954（逆光的太陽光斑，幾乎是純白）——
        **設計稿上看起來暗是遮罩造成的，不是照片本身暗**，不要因此調淡。 */
  --overlay-photo: rgba(46, 25, 25, 0.72);

  /* --- 語意色：線 --- */
  /* ⚠️ ~~--border-btn／--border-tab~~ 已刪除：改用 demo 的按鈕與頁籤外觀後，
     兩者都退化成「＝咖啡棕」的別名，留著就是隱形分叉。 */
  --border-soft:    var(--c-line);  /* 純裝飾用的分隔線（頁尾分隔、卡片內細線） */
  --border-field:   #9A857C;        /* 表單欄位外框．3.36／3.13／3.49 ✅
                                       🔴 不可改用 --c-line #F0E2D6（1.1:1）：
                                          那是 demo 的裝飾線，拿來當欄位框等於沒有框 */

  /* --- 語意色：表單與狀態 --- */
  --text-muted:     #6B5B59;   /* 次要說明文字．6.19／5.76／6.43 ✅
                                  🔴 不可用 demo 的 --color-muted #8A7A78（3.93:1）——
                                     那只夠大字，用在欄位說明就掉到門檻以下。 */
  --c-danger:       #b3261e;   /* on 奶油白 6.29 ✅／on 奶油 5.86 ✅ */
  /* ⚠️ ~~--c-success~~ 已刪除：成功狀態一律用主色（咖啡棕）＋圖示＋文字三種訊號，
     不另開一個綠色——那會變成全站唯一一處綠，而且沒有人記得它存在。 */
  --bg-danger-soft: #fdf0ef;
}

/* ✅ eyebrow 的對比度問題已在 08-13 解決（改用莓紅 4.89:1，通過 AA），
   舊版那兩條「低對比下的補償約束」不再需要。
   ⚠️ 但前綴圓點保留——它是 demo 之外我方原有的定位標記，拿掉沒有好處。 */

/* ==========================================================================
   2. 字體：全站襯線體 Noto Serif TC（自架子集）
   --------------------------------------------------------------------------
   🔴 PM 2026-08-13 定案，推翻 08-07 的「全站黑體、零 web font」。
      起因是客戶看過多頁後表示比較喜歡舊版 demo 的風格，而 demo 的古典溫度感
      很大一部分來自全站襯線體。
      ⚠️ 我方建議「標題襯線、內文黑體」，PM 選擇全站襯線體——
         這是知情選擇，代價已寫在下方，**不要當成疏漏而改回黑體**。

   ⚠️ 「全站中文襯線體」與「首屏零閃爍」互斥，這是物理限制不是實作技巧問題：
        swap     → 先 fallback、載完換 → **會跳字一次**（本案採用）
        block    → 空白數秒 → **首屏白畫面**，違反第 8 節鐵則 4
        optional → 第一次進站永遠 fallback → **客戶第一次看不到襯線體**
      沒有第四種。

   🔴 自架子集，不用 @import、不用 Google Fonts 的 CSS，也不用完整分片。
      demo 的 @import 寫在 style.css 第一行 ＝ 三段串聯下載，全程白畫面：
        下載完 style.css → 解析到 @import → 下載 626KB 的 CSS → 才開始下載字體
      而分片方案實測首頁仍要 846KB／字重（中文用字散布廣，分片省不了多少）。

   實測（2026-08-13，`python3 產生-子集字體.py`）：
      子集 1864 碼位（全站實際用字 ∪ 最高頻 1000 字 ∪ 標點英數）
      400 = 316KB、700 = 326KB、合計 **642KB**、Slow 4G 約 **3.7 秒**
      ✅ 未超過 900KB 的回報門檻，兩字重都保留（PM：「還是需要有粗細之分」）

   🔴 改了任何文案就要重跑 `python3 產生-子集字體.py`。
      忘了重跑＝那幾個字默默掉回系統字，沒有任何錯誤訊息。
      擋這件事的是 `./檢查-規範.sh` 第 8 項（讀 woff2 的 cmap 實際比對，已做陽性測試）。

   🔴 FOUT 跳字幅度實測（2026-08-13，本頁面上量的字寬）——
      **結果與原本的預期不同，值得記住**：

        內容              襯線 fallback → 子集   黑體 fallback → 子集
        純中文 34 字        0.0%                  0.0%
        中英混排（版權列）  +6.2%                 −0.3%
        純數字（電話）      +16.2%                −11.2%
        純英文（Email）     +20.9%                +6.5%
        實際段落高度        122px（三者相同）      同

      **中文全形字在任何字體下都是 1em 寬**，所以換字體時中文段落
      **完全不會重排**——不換行、不位移、行數不變。
      跳動只發生在拉丁字母與數字上（電話、Email、版權列這幾處）。

      ⚠️ 因此上方「fallback 用系統襯線」那一條的理由要講精確：
         它省的**不是版面位移**（襯線與黑體 fallback 的中文寬度都是 0% 差異），
         而是**字形風格的視覺跳動**——黑體換襯線是整個字族變了，
         襯線換襯線只是筆畫細節變了。
      ⚠️ 而且從拉丁字寬看，襯線 fallback 反而跳得比黑體多（+20.9% vs +6.5%），
         因為 Songti TC 的拉丁字特別窄。**電話與 Email 那幾行會明顯抖一下。**
         若 PM 覺得那幾處刺眼，解法是給它們單獨指定 fallback 的拉丁字體，
         不是把整站換回黑體。
   ========================================================================== */
@font-face {
  font-family: "Noto Serif TC Subset";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../fonts/noto-serif-tc-500.woff2") format("woff2");
}
@font-face {
  font-family: "Noto Serif TC Subset";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/noto-serif-tc-700.woff2") format("woff2");
}

:root {
  /* 🔴 fallback 用系統「襯線」，不是系統黑體 —— 這一條是本節最有價值的部分。
     FOUT 期間畫面上已經是襯線體，載完換的只是字形細節、不是整個字族，
     跳動幅度遠小於「黑體 → 襯線體」那種整頁重排。
     ⚠️ Android 多數沒有中文襯線 → 那裡的 fallback 仍會是黑體，這是已知限制。
     ⚠️ 這與 08-03「否決系統襯線堆疊」不衝突：那次否決的是拿它當**唯一方案**
        （Windows 只有新細明體、字重極細，對 60 歲以上是災難）。
        判準是「它撐幾百毫秒」還是「它就是最終畫面」。 */
  --font-body: "Noto Serif TC Subset", "Noto Serif TC",
               "Songti TC", "Songti SC", "PMingLiU", "MingLiU",
               Georgia, "Times New Roman", serif;

  /* 🔴 全站只有這一個字體堆疊，標題與內文都走它（PM 定案全站襯線）。
     不要為標題另開一支——那會多一個 @font-face、多一次 FOUT。 */
}

/* ==========================================================================
   3. 字級階層：預設／中／大 三檔
   --------------------------------------------------------------------------
   🔴 三組明確的 token 值，不用單一乘數。
      理由：PM 2026-08-07 要求「三檔的實際尺寸都不可有小數點」，而乘數做不到——
      設某 token 預設 n px，中＝×1.125、大＝×1.25 要同時是整數 → n 必須是 8 的倍數，
      14px 地板之上只剩 16／24／32／40／48／56／64，16 到 24 之間整段空白，
      且內文（16）以下再無合法值，次要按鈕／eyebrow／版權列會全部塌成同一級、階層消失。

   🔴 錨點是 --fs-body: 18px（2026-08-14 R2h 從 16px 上調）。
      ⚠️ **不可取最小的那一階當內文**——`--fs-xs` 是版權列那類用的。
      ⚠️ 「可見文字硬下限 14px」那條**規則沒有變**，它是下限不是目標值；
         08-14 平移後所有 token 都已高於它（實際最小值 16px）。

   🔴 不可用 clamp() 寫字級（demo 的四個大標都是 clamp(1.75rem, 3.5vw, 2.5rem) 這種流體字級）。
      放大按鈕改的是 :root 的 token，但 clamp() 在多數視窗寬度下由中間的 vw 項主導，
      而 vw 完全不受 root font-size 影響 → 按下「大」，主標一動也不動。
      最陰險的是它看起來像有反應（內文確實變大了），要量 computed 值才發現主標沒動。
      → 換算成三檔定值：桌機取 clamp 的上限、手機取下限，中間交給斷點覆寫。見 CLAUDE.md 9.11。

   ⚠️ 全部用 rem，且 html 的 font-size 維持 100%（不可寫死 16px）——
      寫死會覆蓋使用者自己在瀏覽器調大的預設字級，對本案的目標族群是反效果。

   🔴 2026-08-13 R1a：大字級三階改用 demo 的階層（demo 的階層較「平」，
      襯線體視覺重量重，不需要那麼大的字級對比）。小字級四階與 demo 原本一致、完全不動。
        --fs-hero  52 → 44（demo clamp 上限 2.75rem）
        --fs-h2    38 → 40（demo clamp 上限 2.5rem）
        --fs-h3    26 → 36（demo clamp 上限 2.25rem）
      ⚠️ demo 的版權列 0.8rem＝12.8px 低於本案硬下限，一律走 --fs-xs，不照抄。
   ========================================================================== */
/* 🔴 2026-08-14 R2h：三檔整組往上平移一階。
   PM 原話：「預設尺寸時文字大小很小，『中』反而比較像預設應該要有的大小」
   → 新預設＝原「中」、新中＝原「大」、新大＝再往上一階（×1.125 取整）。
   ⚠️ 內文 16→18px、實際最小值 14→16px。
      **「硬下限 14px」那條規則不變**（它是下限不是目標值），只是現在全部都高於它。
   🔴 **字級全面放大＝所有版面尺寸都改變**，不是「改幾個數字」——
      8 個寬度都要重驗橫向溢出，尤其 320px。 */
:root {
  --fs-hero:     3.125rem;  /* 50px 主視覺大標／日期大數字 */
  --fs-h2:       2.8125rem; /* 45px 區塊主標 */
  --fs-h3:       2.5rem;    /* 40px 次級標題：長文的章節標題、彈窗標題、頁尾品牌名
                               🔴 **不是卡片標題**。CLAUDE.md 6.5 把它標成「卡片標題」是錯的——
                                  36px 是 demo 區塊主標（.about-title 等）clamp 的上限，
                                  demo 的卡片標題一律 1.25rem＝20px（＝我方的 --fs-lead）。
                                  照 36 做，卡片標題會跟區塊主標一樣大（實測 2026-08-13）。
                               ⚠️ 卡片標題一律走 --fs-lead：.branch-card__title／
                                  .service-card__title／.news-card__title 三者都是。 */
  --fs-brand:    2rem;      /* 32px 站名主體（站名長度與 demo 不同，不跟著 demo 改） */
  --fs-lead:     1.375rem;  /* 22px 主視覺內文／房別標籤／卡片標題 */
  --fs-body:     1.125rem;  /* 18px 內文、導覽、主按鈕 */
  --fs-sm:       1.0625rem; /* 17px 次要按鈕（分類頁籤、家庭樹卡按鈕） */
  --fs-xs:       1rem;      /* 16px eyebrow／tag／header 按鈕 */
  /* 🔴 2026-08-14 R2m④ 重新引入 --fs-fine。
     ⚠️ 當初刪它的理由是「地板抬到 14px 後與 --fs-xs 恆等，兩個永遠相同的 token
        就是隱形分叉」——**那個理由因為三檔往上平移而不成立了**：
        --fs-xs 現在是 16px，兩者差一階。
     用途：PM 指出**版權宣告不是常常需要知道的訊息**，應該比其他小字再低一階。
     🔴 不可在 .site-footer__legal 直接寫死 14px——那樣字級放大按鈕對它就失效了（9.11）。
     🔴 **只給版權列這一類「存在感應該最低」的文字**，
        不要拿它去救任何「排不下」的版面問題——那是版面問題，不是字級問題。 */
  --fs-fine:     0.875rem;  /* 14px 版權列 —— 全站最小的一階（＝硬下限，剛好合法） */

  --lh-tight:    1.16;   /* 按鈕單行 */
  --lh-heading:  1.3;    /* 標題（demo 的 h1–h4 是 1.3，比原本的 1.4 緊一階） */
  --lh-body:     1.6;    /* 內文 */
  --lh-prose:    1.9;    /* 長段落（demo 的 .about-text／.service-text 都是 1.9） */
  --lh-lead:     1.5;    /* 主視覺內文 */

  /* 🔴 2026-08-14 R2i：字重整組往上一階（PM 定案），維持兩個字重檔。
     **`--fw-normal` 400 → 500**、`--fw-bold` 維持 700。
     ✅ 檔案大小不變——自架子集的「換字重」是**重產一次**，不是累加一個檔案：
        把 400 那支換成 500 即可（`產生-子集字體.py` 的 WEIGHTS）。

     🔴 **`--fw-medium` 已移除，不要加回來。**
        它原本是 500，但自架子集只有 400／700 → 瀏覽器找不到 500，
        **靜默退回 400**：沒有錯誤訊息，DevTools 的 Computed 還照樣顯示 500
        （那是 CSS 計算值，不是實際渲染字重）。於是 CSS 讀起來像「內文設了中等
        字重」，實際上全站內文都是 regular。完整說明見 CLAUDE.md 9.17。
     ⚠️ **本案只有兩個字重檔，就只准有兩個字重 token。** 留第三層＝下一次同類失效的來源。
        自動檢查見 `./檢查-規範.sh` 第 9 項（R2j）。 */
  --fw-normal:   500;
  --fw-bold:     700;
  /* ⚠️ 不用 300 以下字重：使用者年齡層偏高，細字重在戶外讀不到。
     ⚠️ 500 只在「載入襯線體之前的 fallback 階段」有意義——子集只含 400 與 700
        （見第 2 節），500 會由瀏覽器合成，不要拿它當設計層次。 */
}

/* 中檔 */
:root[data-fs="mid"] {
  --fs-hero:  3.5rem;     /* 56 */
  --fs-h2:    3.125rem;   /* 50 */
  --fs-h3:    2.8125rem;  /* 45 */
  --fs-brand: 2.25rem;    /* 36 */
  --fs-lead:  1.5625rem;  /* 25 */
  --fs-body:  1.25rem;    /* 20 */
  --fs-sm:    1.1875rem;  /* 19 */
  --fs-xs:    1.125rem;   /* 18 */
  --fs-fine:  1rem;       /* 16 */
}

/* 大檔 */
:root[data-fs="large"] {
  --fs-hero:  3.9375rem;  /* 63 */
  --fs-h2:    3.5rem;     /* 56 */
  --fs-h3:    3.1875rem;  /* 51 */
  --fs-brand: 2.5rem;     /* 40 */
  --fs-lead:  1.75rem;    /* 28 */
  --fs-body:  1.375rem;   /* 22 */
  --fs-sm:    1.3125rem;  /* 21 */
  --fs-xs:    1.25rem;    /* 20 */
  --fs-fine:  1.125rem;   /* 18 */
}

/* ==========================================================================
   4. 間距
   --------------------------------------------------------------------------
   ⚠️ 間距由容器的 gap／padding 提供，子元件不自帶 margin。
   🔴 刻度統一為 4 的倍數（PM 2026-08-06 授權：來源稿的間距是大概抓的，由我方統一）。

   🔴 2026-08-13 R1a：版面數值改用 demo 的。逐項對照：

        Figma 稿             demo            採用   為什麼
        容器 1800px          1400px          1400   demo 的版面比例是客戶說喜歡的那個
        左右邊距 60px        20px            20     ⚠️ demo 是 padding:20px 四邊，那是它
                                                    重複定義 .site-container 的 bug，
                                                    我方只取 padding-inline
        區塊上下 120px       96／64（RWD）   96/64  demo 手機收到 64
        區塊留白單一值       分桌機／手機     分開   原本 120px 在手機太鬆
   ========================================================================== */
:root {
  --sp-1:   0.25rem;  /*  4px */
  --sp-2:   0.5rem;   /*  8px */
  --sp-3:   0.75rem;  /* 12px */
  --sp-4:   1rem;     /* 16px */
  --sp-5:   1.25rem;  /* 20px 卡片內距、容器左右邊距 */
  --sp-6:   1.5rem;   /* 24px */
  --sp-7:   1.75rem;  /* 28px demo 的插圖→標題間距 */
  --sp-8:   2rem;     /* 32px 按鈕左右內距、輪播卡片 gap */
  --sp-10:  2.5rem;   /* 40px */
  --sp-12:  3rem;     /* 48px 標題群→內容、導覽列與頁尾連結的 gap */
  --sp-14:  3.5rem;   /* 56px demo 的區塊標題群→輪播 */
  --sp-15:  3.75rem;  /* 60px */
  --sp-16:  4rem;     /* 64px 區塊上下留白（手機）、demo 的 about-grid gap */
  --sp-20:  5rem;     /* 80px */
  --sp-24:  6rem;     /* 96px 區塊上下留白（桌機） */
  --sp-30:  7.5rem;   /* 120px */

  /* 版面 */
  --container-max: 87.5rem;   /* 1400px．demo 的容器寬（原 Figma 1800px） */
  --container-pad: var(--sp-5);   /* 20px */

  /* ────────────────────────────────────────────────────────────────────────
     🔴 R9-10／11（PM 2026-08-19）：**裝飾元素往容器外浮出的上限**（單一來源）。
     ────────────────────────────────────────────────────────────────────────
     **問題**：粉色方塊與五張手繪插圖都用**固定的負值**往外推
     （`left: calc(var(--sp-10) * -1)` 這類），而**容器外的留白會隨視窗變化**——
     1920 有 273px、1440 只剩 33px。固定 40px 在 1920 是「浮在留白裡」，
     到 1440 就超出去被 `main { overflow-x: clip }` 裁掉半截。
     🔴 **裁掉時完全不會報錯**，畫面上只是「插圖看起來破了」（PM：被視窗切掉一半）。

     **這個 token ＝「從 `.wrap` 的內容邊緣，還可以再往外推幾 px」**，
     算式是「容器外的留白 ＋ 一份容器內距 − 保留給視窗邊緣的間距」。
     視窗夠寬時它遠大於各處想要的偏移 → `min()` 取到原值，**寬螢幕的浮空效果完全不變**
     （PM 沒有要動那個效果）；視窗變窄時它自己收斂到 0 → 插圖貼齊容器邊，不再被裁。

     🔴 **保留值取 `--sp-8`(32px) 不是 `--sp-5`(20px)，因為 `100vw` 含捲軸**：
        實測 1920 的視窗，`documentElement.clientWidth` 只有 1905——
        **`100vw` 比真正可用的寬度多算了 15px（捲軸），推算出的留白因此多 7.5px**。
        保留 32 才能保證實際間距 ≥24.5px；保留 20 只剩 12.5px，會看起來仍然貼著邊。
        ⚠️ 這是量出來的，不要「順手」改小。

     **兩種定位基準的用法不同，用錯會差一個 `--container-pad`（20px）**：
       ・基準是 `.wrap` 的**子元素**（grid 欄位，如 `.about__media`／`.about__text`）
         → 直接用 `var(--decor-bleed)`
       ・基準是 `.wrap` **本身**（如 `.creed .wrap`／`.letter > .wrap`）
         → 用 `calc(var(--decor-bleed) - var(--container-pad))`
           絕對定位的 `left`／`right` 是從**padding box**（＝ `.wrap` 外緣）起算的，
           比內容邊緣多出一份左右內距，所以要扣回去。
           ✅ 這個式子在窄螢幕會變成負數（＝往內縮），**那是刻意的**：
              它讓插圖停在容器的**內容**邊緣而不是外緣，narrow 時才不會貼死視窗。
       🔴 **判斷基準是誰要用量的**（CLAUDE.md 9.22）：往上找第一個 `position` 不是
          `static` 的祖先。本案六個元素實測就是上面兩類，不要用猜的。 */
  --decor-bleed: max(0px, (100vw - min(100vw, var(--container-max))) / 2
                         + var(--container-pad) - var(--sp-8));
  --header-h:      5.5rem;    /* 88px．demo 的 header 是 logo 高＋上下 20px */

  /* 區塊上下留白：手機 64、桌機 96（demo 的 --spacing-section / -mobile） */
  --section-pad:   var(--sp-16);
}

@media (min-width: 768px) {
  :root { --section-pad: var(--sp-24); }
}

/* ==========================================================================
   5. 圓角與陰影
   --------------------------------------------------------------------------
   🔴 **2026-08-14 R2a：全站移除圓角。**
      PM 原話：「demo 的風格不像之前新版那麼圓潤、可愛，所以像按鈕、卡片等等
      都不需要加上圓角。」
      ⚠️ **這一點與 demo 不同，不是漏抄**——demo 自己的 `.btn` 是 `--radius-full`
         （膠囊）。**PM 是在 demo 的基礎上要求更簡約**，日後看到 demo 是膠囊
         不要「修正」回去。
      ⚠️ 移除圓角**不影響**點擊區 ≥44×44px 那條，兩者無關。

   🔴 **「膠囊」與「正圓」是兩件事，不可一起歸零。**
      原本只有一個 `--r-pill`，同時被用在①膠囊按鈕②正圓元素（大頭貼、圓點、
      圓形插圖底、社群按鈕）。把它設成 0，**正圓元素會一起變方塊**——
      而那不是 PM 要的（他講的是「按鈕、卡片」）。
      → 因此 08-14 拆成兩個 token，並把 `--r-pill` **改名**為 `--r-round`：
        名字寫著 pill，下一個人就會拿它去做膠囊按鈕，那正是要避免的。
   ========================================================================== */
:root {
  /* 🔴 圓角一律 0。要整站改回圓角就改這幾行，不必逐處找。 */
  --r-sm:    0;
  --r-md:    0;
  --r-lg:    0;

  --r-card:  var(--r-lg);   /* 卡片、面板外框 */
  --r-img:   0;             /* 照片一律直角（demo 的特徵，R1a 就是 0） */
  --r-field: var(--r-sm);   /* 表單欄位 */

  /* 🔴 這一個**不是**圓角，是「把元素做成正圓」的工具，只用在本來就該是圓的東西：
        大頭貼 `.avatar`／圓點 `.carousel-dot`、`.eyebrow::before`／
        圓形插圖底 `.service-card__icon`
     ⚠️ **不可拿它做按鈕、標籤、卡片**——那會把 R2a 的移除圓角又做回去。
     ⚠️ 大頭貼維持圓形是刻意的：方形頭像在族譜脈絡下看起來像證件照。
        若 PM 要連頭像也方，改 `.avatar` 那一處即可，不要動這個 token。 */
  --r-round: 62.4375rem;    /* 999px */

  /* 陰影：暖色調，不用純黑（demo 的 rgba(64,35,35,…)）。
     ⚠️ 純黑陰影在奶油底上會發灰，這是 demo 溫度感的來源之一。 */
  --shadow-sm:    0 0.125rem 0.5rem rgba(64, 35, 35, 0.06);
  --shadow-md:    0 0.5rem 1.5rem rgba(64, 35, 35, 0.10);
  --shadow-lg:    0 1rem 3rem rgba(64, 35, 35, 0.14);

  --shadow-hover: var(--shadow-md);
  --shadow-focus: 0 0 0 0.1875rem rgba(74, 40, 39, 0.45);  /* 咖啡棕，原為深綠 */
}

/* ==========================================================================
   6. 動效
   --------------------------------------------------------------------------
   CLAUDE.md 動畫準則④：時長 ≤400ms、不串接長 delay 鏈。
   D1 8.5：進場單張 300ms、stagger 80ms，六張總計 ≤700ms 收完。
   ========================================================================== */
:root {
  --dur-fast:    0.16s;
  --dur-base:    0.24s;
  --dur-enter:   0.3s;   /* 進場動畫單張（AOS 的 duration，由 script.js 傳入） */
  --stagger:     0.08s;  /* 依序進場間隔（AOS 的 delay 級距） */
  --ease:        cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ==========================================================================
   7. Base Reset
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: var(--fw-normal);
  line-height: var(--lh-body);
  color: var(--text-main);
  background-color: var(--bg-page);
  /* 🔴 2026-08-14 R2i：移除 `-webkit-font-smoothing: antialiased`。
     它在 macOS 上讓字**明顯變細**（demo 的 CSS 有這一行，R1b 照抄了），
     而本案受眾偏高齡——PM 反映「文字太細，長輩不好閱讀」，這是三個原因之一。
     ⚠️ 不要因為「大家都這樣寫」而加回去：它是為了讓深底白字看起來乾淨，
        本站是淺底深字，用它只有壞處。 */
}

/* 🔴 加 body 前綴不是多餘的：Tailwind preflight 會把 h1–h6 的 font-weight 與 font-size
   重設為 inherit，而它注入在本檔之後、特異性又相同 → 沒有前綴的話標題會掉成內文字重。
   ⚠️ 這個問題原本被 .font-display 的 bold 蓋住，2026-08-07 拆掉襯線體才暴露出來。
   同一類坑見 CLAUDE.md 9.5（Tailwind 會安靜覆蓋我方樣式，且沒有任何錯誤訊息）。 */
body h1, body h2, body h3, body h4, body h5, body h6 {
  margin: 0;
  line-height: var(--lh-heading);
  font-weight: var(--fw-bold);
}
/* 🔴 `blockquote`／`figure`／`dl` 也要歸零（2026-08-17 R4b 實際踩到）。
   瀏覽器預設給 `blockquote` 上下 `1em`（本案 18px）的 margin，而它會
   **穿過沒有內距的 `.wrap` 逃到 section 外面**（margin collapse）——
   於是那一區的實際下方留白變成 96＋18，與其他區塊不一致。
   ⚠️ 症狀不會是「版面壞掉」，只是那一區莫名鬆一點；
      是把裝飾對齊 section 下緣時量出來差 18px 才發現的。 */
p, ul, ol, blockquote, figure, dl, dd { margin: 0; }
ul, ol { padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }

/* 🔴 手繪裝飾（葉子、水墨筆刷）刻意溢出容器邊界——那是 demo 溫度感的來源，
   但溢出的部分會撐出橫向捲軸（實測 60px）。在 main 裁掉即可。

   ⚠️ 用 overflow-x: clip，不是 hidden。兩個理由：
     ① hidden 會觸發 CSS 的 overflow-x/y 耦合規則，把沒設定的 overflow-y
        從 visible 變成 auto → main 本身內容剛好有幾 px 的高度差，
        就會被瀏覽器當成一個「真的在捲動」的獨立容器，頁面裡多長出一條假捲軸。
     ② hidden 會讓 main 成為捲動容器，裡面的 position: sticky（家庭樹頁的左欄）
        會相對它而不是視窗定位 → 黏不住，而且不會報錯。
   clip 不建立捲動容器，純粹視覺裁切，兩個副作用都沒有。 */
main { overflow-x: clip; }

/* 🔴 圖片一律標 width/height 或 aspect-ratio（CLAUDE.md 第 4 節第 3 條）
   這裡只給保底，不能取代 HTML 上的尺寸標示。 */
img {
  max-width: 100%;
  height: auto;
  display: block;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* ==========================================================================
   8. 🔴 B2 首屏閃爍：四條鐵則的落實（CLAUDE.md 第 4 節）
   --------------------------------------------------------------------------
   驗法：DevTools → Network 勾 Disable cache、節流 Slow 4G → 重載。
   用自己電腦的熱快取看，永遠看不到這個 bug。
   ========================================================================== */

/* --- 鐵則 1：內文與標題一律系統中文字體堆疊，不載任何 web font ---
   實測 Google Fonts 的 Noto Sans TC：CSS 本體 241KB 阻塞渲染、切成 210 個分片，
   font-display: swap 的定義就是「先系統字、載完再換」——它設計上就會跳，不是解法。
   台灣裝置的蘋方／微軟正黑品質足夠，且零載入＝零閃爍。
   🔴 不要因為「標題想要有特色」而回頭加 web font：那會把這一整條鐵則打回原點。 */

/* --- 鐵則 2：進場動畫的初始隱藏寫進 CSS，且「預設可見、由 JS 反向啟用」 ---
   🔴 不可寫成無條件 [data-aos]{opacity:0}——那樣 JS 一沒跑，整頁內容永遠看不見，
      而客戶只會覺得「網站壞了」。JS 會在 <html> 補上 .anim-on，CSS 只在有它時才隱藏。
   判準：任何「靠 JS 才看得見」的寫法都要問一次「JS 沒跑會怎樣？」 */
/* 🔴 這一條是本節的關鍵，不可刪 —— 沒有它，前面那句「預設可見、由 JS 反向啟用」是假的。
   AOS 自己的 CSS 就寫了無條件的 [data-aos^=fade]{opacity:0}，它不管有沒有 .anim-on。
   所以只加 `.anim-on [data-aos]{opacity:0}` 並不會讓內容在 JS 沒跑時可見——
   AOS 的原始規則仍然把它藏著，而且完全沒有訊號。
   （實測：拔掉所有 <script> 渲染，有 5 個區塊的內容是看不見的。）
   ⚠️ 判準：任何「靠 JS 才看得見」的寫法都要問一次「JS 沒跑會怎樣？」
      ——答案是空白就必須改寫，這一條就是那個改寫。 */
html:not(.anim-on) [data-aos] {
  opacity: 1;
  transform: none;
}

/* 🔴 一律加 :not(.aos-animate) —— 這是把 aos.css 移到本檔之前的代價，不可省。
   aos.css 靠 `.aos-animate` 那條規則把元素顯示出來；本檔排在它後面，
   同特異性時本檔勝出，於是「動畫該結束時」元素還是 opacity: 0，永遠不出現。
   用 :not() 明確讓位，就不必去推算兩邊的特異性誰大——推算錯了不會報錯，
   只會看到一整區內容永遠是空的。 */
.anim-on [data-aos]:not(.aos-animate) { opacity: 0; }

/* 🔴 AOS 的 fade-up 預設位移 100px，實測太大：元素從下方 100px 移上來的途中，
   會壓過它下面那個「沒有進場動畫」的靜止元素（實測撞到分類頁籤與照片牆）。
   收斂成 16px —— 進場感還在，但不會侵入下一個元素的位置。
   ⚠️ 這幾條要生效，aos.css 必須排在本檔之前（見 index.html 的 <link> 順序）。 */
.anim-on [data-aos^="fade-up"]:not(.aos-animate)    { transform: translate3d(0, 1rem, 0); }
.anim-on [data-aos^="fade-down"]:not(.aos-animate)  { transform: translate3d(0, -1rem, 0); }
.anim-on [data-aos^="fade-left"]:not(.aos-animate)  { transform: translate3d(1rem, 0, 0); }
.anim-on [data-aos^="fade-right"]:not(.aos-animate) { transform: translate3d(-1rem, 0, 0); }

/* ⚠️ 進場動畫只用 AOS 一套，不另外自造一份——兩套並存必然分叉
   （同一個區塊哪一套生效，會變成得看那段是誰寫的）。 */

/* 🔴 鐵則：首屏（第一個畫面）內容不做出場動畫。
   一進站就看到空白再淡入，體感是「網站在轉」；且 JS 掛掉時首屏會永遠不出現。 */
.anim-on .no-anim,
.anim-on .no-anim [data-aos] {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  /* 🔴 這裡必須帶上 :not(.aos-animate)，不可只寫 .anim-on [data-aos]。
     media query **不增加權重**：`.anim-on [data-aos]`(0,2,0) 蓋不過上面
     `.anim-on [data-aos]:not(.aos-animate)`(0,3,0)，逃生門會永遠失效。

     失效的後果只在「訪客開了系統的減少動態效果」時出現，而且完全不報錯：
     AOS 以 disable:true 啟動時會把當下 DOM 裡的 data-aos 全部移除，但**之後才由 JS
     插入的節點沒被移除**（最新消息輪播、十房卡、相簿格線都是動態插入），
     它們永遠拿不到 .aos-animate，於是永久停在 opacity:0 —— 整片空白。
     本案受眾是長輩，「減少動態效果」的開啟率遠高於一般網站，這條不是邊角情況。 */
  .anim-on [data-aos],
  .anim-on [data-aos]:not(.aos-animate) {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ==========================================================================
   8.6 華麗進場動畫（2026-09-01，PM：「動畫要完整，每個區塊都要有，華麗一點」）
   --------------------------------------------------------------------------
   本節做五件事，五件是一組，不可只挑幾條進來：
     8.6.1 修好 80ms 級距的 data-aos-delay（原本靜默失效，站上四處都中）
     8.6.2 新增 lin-* 自訂進場動畫（AOS 內建那七種做不出模糊／揭幕／傾斜）
     8.6.3 區塊內部的逐行連鎖（eyebrow → 標題 → 內文），不必逐行加 data-aos
     8.6.4 內頁頁首 .page-head 的載入進場（**不是** AOS，理由見該段）
     8.6.5 逃生門補強 ＋ 8.6.6 手機關掉所有水平位移（防橫向捲軸）

   🔴 第 8 節那三條鐵則**全部繼續適用**，本節只在它們之上加東西：
      ・`html:not(.anim-on)`（JS 沒跑）時一律可見 —— 本節新增的 `filter`
        與 `clip-path` 也必須在那裡歸零，否則 JS 掛掉時看到的是「糊成一片」
        或「被裁掉一半」，比空白更難聯想到是動畫的問題（8.6.5）。
      ・`prefers-reduced-motion` 時一律不動 —— 同理要補 `filter`／`clip-path`／
        以及 8.6.3 的子元素連鎖與 8.6.4 的 keyframes（8.6.5）。
      ・首屏不做**捲動觸發**的動畫 —— 8.6.4 用的是載入即播的 CSS animation，
        不是 AOS，差別見該段。

   🔴 自訂動畫名稱一律 `lin-` 前綴，**不可取 fade-／zoom-／slide-／flip- 開頭的名字**：
      aos.css 用的是 `[data-aos^=fade]` 這種前綴選擇器，取到同前綴的名字會把
      AOS 自己的初始狀態（例如 fade-up 的 translate3d(0,100px,0)）一起套上來，
      兩份 transform 疊加，而且完全不報錯。

   ⚠️ duration／delay／easing 不必在這裡寫死：aos.css 的
      `[data-aos][data-aos][data-aos-duration="600"]` 與
      `body[data-aos-duration="500"] [data-aos]` 都是對通用 `[data-aos]` 寫的，
      自訂名稱照樣吃得到。全域值由 script.js 的 AOS.init() 設在 <body> 上，
      個別元素要不一樣時在 HTML 上加 data-aos-duration／data-aos-easing。
      🔴 **這兩個屬性的值必須是 50 的倍數**，理由見 8.6.1。
   ========================================================================== */

/* --------------------------------------------------------------------------
   8.6.1 🔴 80ms 級距的 delay —— 修一個一直存在的靜默失效
   --------------------------------------------------------------------------
   **問題**：aos.css 的 delay 規則是 SCSS 迴圈從 50 每次 +50 產生的，
   只有 50／100／150…3000 這些值有對應的 CSS 規則（`data-aos-delay="0"` 也沒有）。
   而 D1 8.5 指定的 stagger 是 **80ms**，站上原本就寫著
   `data-aos-delay="80"／"160"／"240"／"320"`（各房卡、相簿卡、首頁消息卡、消息列表）
   —— **這些值在 aos.css 裡通通沒有規則**，`transition-delay` 於是停在瀏覽器預設的 0，
   六張卡是「同時」進場，不是依序。

   🔴 完全不報錯：屬性拼寫正確、AOS 也認得（它只讀 data-aos-delay 決定要不要提前
      套 class），只是那一行 CSS 根本不存在。畫面上看起來就只是「動畫有跑」，
      沒有人會發現少了依序感。
      驗法：DevTools 點任一張卡 → Computed → `transition-delay`，修之前是 `0s`。

   ⚠️ 修法是補規則，不是把 80 改成 100 —— 80ms 是 D1 8.5 定的值。
   ⚠️ 選擇器要壓過 `body[data-aos-delay="…"] [data-aos].aos-animate`(0,3,1)，
      所以帶上 `.anim-on` 湊到 (0,4,0)。少一個 class 就會被壓回去，一樣不報錯。
   ⚠️ 這裡只補「80 的倍數」。要用別的級距時，**要嘛用 50 的倍數（AOS 原生就有），
      要嘛回來這裡補**——直接寫一個沒有規則的數字就是這個坑本身。
   -------------------------------------------------------------------------- */
.anim-on [data-aos][data-aos-delay="80"],
.anim-on [data-aos][data-aos-delay="160"],
.anim-on [data-aos][data-aos-delay="240"],
.anim-on [data-aos][data-aos-delay="320"],
.anim-on [data-aos][data-aos-delay="400"],
.anim-on [data-aos][data-aos-delay="480"],
.anim-on [data-aos][data-aos-delay="560"],
.anim-on [data-aos][data-aos-delay="640"],
.anim-on [data-aos][data-aos-delay="720"] { transition-delay: 0s; }

.anim-on [data-aos][data-aos-delay="80"].aos-animate  { transition-delay: 80ms; }
.anim-on [data-aos][data-aos-delay="160"].aos-animate { transition-delay: 160ms; }
.anim-on [data-aos][data-aos-delay="240"].aos-animate { transition-delay: 240ms; }
.anim-on [data-aos][data-aos-delay="320"].aos-animate { transition-delay: 320ms; }
.anim-on [data-aos][data-aos-delay="400"].aos-animate { transition-delay: 400ms; }
.anim-on [data-aos][data-aos-delay="480"].aos-animate { transition-delay: 480ms; }
.anim-on [data-aos][data-aos-delay="560"].aos-animate { transition-delay: 560ms; }
.anim-on [data-aos][data-aos-delay="640"].aos-animate { transition-delay: 640ms; }
.anim-on [data-aos][data-aos-delay="720"].aos-animate { transition-delay: 720ms; }

/* --------------------------------------------------------------------------
   8.6.2 自訂進場動畫 lin-*
   --------------------------------------------------------------------------
   用法一覽（挑的原則寫在每一條旁邊）：
     lin-rise        淡入＋上移＋去模糊．**全站主力**，段落／標題群／一般區塊
     lin-rise-sm     同上但幅度小一半．接在別的動畫後面、不想搶戲時用
     lin-zoom        淡入＋上移＋放大到原尺寸．**卡片**（消息卡／各房卡／相簿卡）
     lin-pop         純縮放彈入．**小元素**（按鈕列、標籤、分頁），配 ease-out-back
     lin-blur        模糊散開．**手繪裝飾圖**（葉子、筆刷）
     lin-tilt        3D 微傾立起．**單一主角**（大卡片、信件、表單），一頁最多一兩處
     lin-swing-left  自左側滑入＋微旋轉．左右分欄的左欄
     lin-swing-right 自右側滑入＋微旋轉．左右分欄的右欄
     lin-wipe-up     由下往上揭幕．**圖片**
     lin-wipe-left   由左往右揭幕．**圖片**
     lin-wipe-right  由右往左揭幕．**圖片**

   🔴 aos.css 只給 fade／zoom／slide／flip 四個前綴設 `transition-property`，
      自訂名稱**完全沒有 transition**（會瞬間跳出來，那不是動畫是閃現）。
      下面這一條不可省，也不可只列 opacity, transform —— filter 與 clip-path
      漏掉的話，模糊與揭幕兩類會直接跳，而且只有那幾個元素會怪。
   -------------------------------------------------------------------------- */
.anim-on [data-aos^="lin-"] {
  transition-property: opacity, transform, filter, clip-path;
}

/* ① 主力：上移＋去模糊。模糊讓它比單純位移「有厚度」，這是華麗感的來源。 */
.anim-on [data-aos="lin-rise"]:not(.aos-animate) {
  transform: translate3d(0, 2rem, 0);
  filter: blur(0.375rem);
}

.anim-on [data-aos="lin-rise-sm"]:not(.aos-animate) {
  transform: translate3d(0, 1rem, 0);
}

/* ② 卡片．🔴 縮放**只能往小的縮**（0.94 → 1）。
   反過來從 1.06 縮到 1 的話，起始狀態比版位寬，左右各吐出幾 px —— 那正是
   手機橫向捲軸的來源之一（8.6.6）。本節所有 scale 起始值一律 < 1。 */
.anim-on [data-aos="lin-zoom"]:not(.aos-animate) {
  transform: translate3d(0, 1.5rem, 0) scale(0.94);
}

/* ③ 小元素．搭配 HTML 上的 data-aos-easing="ease-out-back" 會有一點回彈。
   ⚠️ 回彈只給「不影響版面的小東西」——大區塊回彈會讓下面的內容跟著晃。 */
.anim-on [data-aos="lin-pop"]:not(.aos-animate) {
  transform: scale(0.82);
}

/* ④ 裝飾圖．它們本來就是溢出容器的手繪素材，模糊散開最貼近水墨的感覺。

   🔴 **這一條刻意「只動 filter，不動 transform」**（2026-09-01 實測改掉的）。
      原本還帶了 `transform: scale(0.92)`，看起來更有層次，但那會**整條蓋掉裝飾圖
      自己的定位 transform** —— `.creed__decor` 是 `translateY(54.3%)`、
      `.letter__brush` 是 `translateY(50%)`，那兩個值是從墨量重心量出來的
      （見 about.css 的長註解），被蓋掉之後筆刷會先跳上去 76px 再滑回來。
      ⚠️ 權重上我方 (0,3,0) 必勝那些單一 class 的定位規則，**它們沒有還手的餘地**，
         而且不會報錯，只會看到「筆刷位置怪怪的」。
      ⚠️ 判準（往後新增動畫都適用）：**會套在裝飾／絕對定位元素上的動畫，
         不要用 transform** —— 那類元素多半正在用 transform 定位。
         需要位移感就換 filter／opacity／clip-path。 */
.anim-on [data-aos="lin-blur"]:not(.aos-animate) {
  filter: blur(0.75rem);
}

/* ⑤ 3D 微傾．⚠️ 透視會讓元素的**外框略為變寬**（近端邊緣被放大），
   桌機有 `main { overflow-x: clip }` 吃掉，手機另外降級（8.6.6）。
   ⚠️ 一頁最多用一兩處：滿頁都在傾斜會暈，本案受眾是長輩。 */
.anim-on [data-aos="lin-tilt"]:not(.aos-animate) {
  transform: perspective(75rem) rotateX(10deg) translate3d(0, 2.5rem, 0);
  transform-origin: 50% 100%;
}

/* ⑥ 側向滑入．旋轉刻意只有 2deg —— 再大一點，長條形的元素轉起來四角會掃出
   明顯的空白，而且旋轉會讓外框變寬（同 ⑤，手機降級）。 */
.anim-on [data-aos="lin-swing-left"]:not(.aos-animate) {
  transform: translate3d(-2.5rem, 0.75rem, 0) rotate(-2deg);
}
.anim-on [data-aos="lin-swing-right"]:not(.aos-animate) {
  transform: translate3d(2.5rem, 0.75rem, 0) rotate(2deg);
}

/* ⑦ 揭幕（clip-path）．**位移是 0**，所以它在任何寬度都不可能撐出捲軸 ——
   手機上最華麗又最安全的一種，圖片優先用它。

   🔴 展開狀態刻意寫成 `inset(-2rem)` 而不是 `inset(0)`：
      clip-path 動畫結束後仍然掛在元素上，寫 inset(0) 會把**陰影與 focus 外框
      一起裁掉**（裁的是 border box）。留 2rem 餘裕，看起來與沒有裁切完全一樣。
      ⚠️ 不可改寫成「動畫結束就把 clip-path 設回 none」：none 與 inset()
         之間不能內插，那樣是瞬間跳一下，不是揭幕。
   ⚠️ clip-path 會建立 stacking context，也會成為 position: fixed 子孫的
      containing block —— **不要加在 `.bg-parallax` 或含固定定位元素的容器上**
      （視差背景會失效，而且不報錯）。它是給圖片與純內容區塊用的。 */
.anim-on [data-aos^="lin-wipe"] { clip-path: inset(-2rem); }

.anim-on [data-aos="lin-wipe-up"]:not(.aos-animate)    { clip-path: inset(100% -2rem -2rem -2rem); }
.anim-on [data-aos="lin-wipe-left"]:not(.aos-animate)  { clip-path: inset(-2rem 100% -2rem -2rem); }
.anim-on [data-aos="lin-wipe-right"]:not(.aos-animate) { clip-path: inset(-2rem -2rem -2rem 100%); }

/* --------------------------------------------------------------------------
   8.6.3 區塊標題群的逐行連鎖
   --------------------------------------------------------------------------
   `.section-head` 只要自己帶 data-aos，裡面的 eyebrow → 標題 → 說明
   就會依序補上來，**子元素不必各自加 data-aos**。

   ⚠️ 不要再給子元素加 data-aos：那會變成 AOS 與本段兩套同時作用在同一個元素上，
      誰贏要看兩邊的 transform 誰後宣告，而且不會報錯。
   ⚠️ 這裡的 transition 必須自己寫 —— AOS 的 duration／delay 只設在
      **帶 data-aos 的那個元素**上，子元素吃不到。
   🔴 隱藏狀態掛在 `.anim-on … :not(.aos-animate)` 底下，與第 8 節同一個邏輯：
      JS 沒跑 → 沒有 .anim-on → 子元素就是正常可見的最終狀態。
   -------------------------------------------------------------------------- */
.anim-on .section-head[data-aos] .section-head__text > * {
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
}
.anim-on .section-head[data-aos]:not(.aos-animate) .section-head__text > * {
  opacity: 0;
  transform: translate3d(0, 0.75rem, 0);
}
.anim-on .section-head[data-aos].aos-animate .section-head__text > *:nth-child(1) { transition-delay: 80ms; }
.anim-on .section-head[data-aos].aos-animate .section-head__text > *:nth-child(2) { transition-delay: 180ms; }
.anim-on .section-head[data-aos].aos-animate .section-head__text > *:nth-child(3) { transition-delay: 280ms; }
.anim-on .section-head[data-aos].aos-animate .section-head__text > *:nth-child(4) { transition-delay: 380ms; }

/* eyebrow 前綴圓點：跟著所屬的 [data-aos] 一起彈進來。
   ⚠️ 這個圓點是既有的 `.eyebrow::before`，本段只加動畫、不改它的外觀——
      動畫結束後與原本一模一樣。 */
.anim-on [data-aos] .eyebrow::before,
.anim-on [data-aos].eyebrow::before {
  transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.2s;
}
.anim-on [data-aos]:not(.aos-animate) .eyebrow::before,
.anim-on [data-aos]:not(.aos-animate).eyebrow::before {
  transform: scale(0);
}

/* --------------------------------------------------------------------------
   8.6.4 內頁頁首 .page-head 的載入進場
   --------------------------------------------------------------------------
   🔴 **刻意不用 AOS**。內頁的 `.page-head` 就是首屏，而 AOS 是捲動觸發的——
      首屏掛 data-aos 等於第 8 節鐵則②說的「一進站先看到空白再淡入」。
      這裡改用 CSS animation：**載入即播、不等捲動、不等任何資料**，
      使用者不會有任何一刻在等內容出現。

   🔴 仍然掛在 `.anim-on` 底下（鐵則③）：JS 沒跑就完全不播，
      內容直接是最終狀態，不是空白。
      ⚠️ animation-fill-mode: both 讓 delay 期間停在 from —— 這是**刻意的**，
         但也正因如此，`.anim-on` 這個前綴不可拿掉：拿掉的話 JS 掛了會停在
         opacity: 0 永遠不動。

   背景照片的緩慢推近（Ken Burns）掛在 `::before`，不是整個 section ——
   ⚠️ `.page-head--photo` 本身有 overflow: hidden，放大的部分會被裁掉，
      所以**不會**多出捲軸；改成放大 section 本身就會。
   -------------------------------------------------------------------------- */
@keyframes lin-head-in {
  from { opacity: 0; transform: translate3d(0, 1.25rem, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes lin-head-zoom {
  from { transform: scale(1.12); }
  to   { transform: scale(1); }
}

.anim-on .page-head .eyebrow,
.anim-on .page-head__title,
.anim-on .page-head__lead,
.anim-on .page-head .breadcrumb {
  animation: lin-head-in 0.6s var(--ease) both;
}
.anim-on .page-head .eyebrow    { animation-delay: 0.05s; }
.anim-on .page-head__title      { animation-delay: 0.15s; }
.anim-on .page-head__lead       { animation-delay: 0.28s; }
.anim-on .page-head .breadcrumb { animation-delay: 0.36s; }

.anim-on .page-head--photo::before {
  animation: lin-head-zoom 2.4s var(--ease) both;
}

/* --------------------------------------------------------------------------
   8.6.5 逃生門補強之一：JS 沒跑
   --------------------------------------------------------------------------
   🔴 第 8 節的逃生門只歸零了 opacity 與 transform。本節多用了 filter 與 clip-path，
      兩者也要一起歸零，否則逃生門是半開的：內容「看得到」但糊成一片或被裁掉一半，
      比整片空白更難聯想到是動畫造成的。
   ⚠️ 另一道逃生門（減少動態效果）在 8.6.7，**刻意放在本節最後面**，理由見那裡。
   -------------------------------------------------------------------------- */
html:not(.anim-on) [data-aos] {
  filter: none;
  clip-path: none;
}

/* --------------------------------------------------------------------------
   8.6.6 🔴 手機：關掉所有水平位移（防橫向捲軸）
   --------------------------------------------------------------------------
   **問題**：帶水平位移的進場動畫（fade-left／fade-right／lin-swing-*）在起始狀態
   會把元素推到版位外面。桌機看不出來（`main { overflow-x: clip }` 吃掉了），
   但那道保險有兩個缺口：
     ① `overflow-x: clip` 是較新的值，**iOS Safari 16 以前不支援**——
        不支援時整條宣告被丟掉，等於沒有裁切。本案受眾用舊 iPhone 的比例不低。
     ② `<main>` 以外的東西不在它的保護範圍（頁尾就在外面）。
   而手機的版位本來就只剩十幾 px 的餘裕，一推就出去 → 整頁可以左右拖動，
   長輩會以為「網站跑版了」。

   🔴 **正解是「手機不做水平位移」，不是把 overflow: hidden 加在 html／body 上**：
      那會讓根元素變成捲動容器，scroll-behavior: smooth、position: sticky
      與「回到頂端」都會出現難查的副作用（第 7 節 main 那一條已經寫過同一個坑）。

   ⚠️ 用 `[data-aos*="left"]`／`[data-aos*="right"]` 這種**模糊比對**是刻意的：
      它一次涵蓋 fade-left／fade-up-right／zoom-in-left／slide-right／lin-swing-*，
      **也涵蓋以後才加進來的新名字**。逐一列舉的話，下一個人新增一種側向動畫
      就會再踩一次，而且要到有人用手機看才會發現。
   ⚠️ 這裡只換 transform，不動 opacity —— 動畫還在，只是改成垂直的。
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  .anim-on [data-aos*="left"]:not(.aos-animate),
  .anim-on [data-aos*="right"]:not(.aos-animate) {
    transform: translate3d(0, 1.25rem, 0);
  }

  /* 透視傾斜與旋轉一樣會讓外框變寬，手機一律降成單純上移。 */
  .anim-on [data-aos="lin-tilt"]:not(.aos-animate) {
    transform: translate3d(0, 1.5rem, 0);
  }

  /* ⚠️ 揭幕（clip-path）是 0 位移，**手機保留**——它正是手機上最安全的華麗做法。
     上面那條模糊比對會連 lin-wipe-left／lin-wipe-right 的 transform 一起設掉，
     但它們本來就沒有 transform，設了也只是多一段上移，仍然沒有水平位移。 */

  /* 模糊在低階手機是實打實的成本（每一幀都要重算），縮到剛好看得出層次即可。
     ⚠️ 不是「手機不需要動畫」，是同一個效果換一組便宜的參數。 */
  .anim-on [data-aos="lin-rise"]:not(.aos-animate) { filter: blur(0.1875rem); }
  .anim-on [data-aos="lin-blur"]:not(.aos-animate) { filter: blur(0.375rem); }
}

/* 🔴 768px 以上仍然做水平位移，而那時**唯一的保險是 `main { overflow-x: clip }`**。
   `overflow: clip` 是較新的值：**Safari 16 以前不支援**，不支援時整條宣告被丟掉
   → 沒有裁切 → 平板／舊 Mac 上會出現橫向捲軸，而且不會有任何錯誤訊息。
   （實測 768／1024 寬：`.about__media` 等元素的起始狀態確實超出視窗，
     只是被 clip 吃掉了；把 clip 拿掉就會看到捲軸。）

   這裡直接問瀏覽器「你支援 clip 嗎」，不支援就一律改成垂直位移 ——
   ⚠️ **這比「再多加一個斷點」正確**：問題的條件是「瀏覽器版本」不是「螢幕寬度」，
      用寬度猜的話，舊 Safari 的桌機視窗仍然會中，而新 Safari 的平板卻白白少了動畫。
   ⚠️ 條件寫 `overflow-x: clip` 而不是 `overflow: clip`：要問的就是實際用到的那一個。 */
@supports not (overflow-x: clip) {
  .anim-on [data-aos*="left"]:not(.aos-animate),
  .anim-on [data-aos*="right"]:not(.aos-animate) {
    transform: translate3d(0, 1.25rem, 0);
  }
  .anim-on [data-aos="lin-tilt"]:not(.aos-animate) {
    transform: translate3d(0, 1.5rem, 0);
  }
}

/* 🔴 頁尾在 `<main>` 外面，`main { overflow-x: clip }` 保護不到它。
   頁尾四欄現在各自有進場動畫，加一道自己的裁切保險。
   ⚠️ 用 clip 不是 hidden，理由與第 7 節 main 那一條完全相同
      （hidden 會讓它變成捲動容器，還會觸發 overflow-x/y 耦合）。 */
.site-footer { overflow-x: clip; }

/* --------------------------------------------------------------------------
   8.6.7 🔴 逃生門補強之二：減少動態效果（本節最後一段，位置不可調動）
   --------------------------------------------------------------------------
   **2026-09-01 實測抓到的 bug，不是理論上的顧慮。**
   第 8 節那一段 `@media (prefers-reduced-motion: reduce)` 的選擇器是
   `.anim-on [data-aos]:not(.aos-animate)` ＝ **(0,3,0)**，
   而 8.6.2 每一條初始狀態也正好是 **(0,3,0)**（`.anim-on` ＋ 屬性 ＋ `:not()`）。
   同權重時**後宣告的贏**，8.6 整節都排在第 8 節後面
   → 開了「減少動態效果」的訪客會看到：**opacity 已經是 1（看得見），
      但 transform 還停在初始狀態** —— 整頁的元素往下偏 32px、裝飾圖縮成 0.92、
      側欄還轉了 2 度。不是空白，是「版面看起來怪怪的」，最難查的那一種。
      （實測 /about：22 個元素中招。）

   🔴 因此這一段用**兩道保險**，兩道都要留：
     ① **位置**：放在 8.6 的最後 —— 後面不可以再有任何會設 transform 的規則。
        🔴 **新增動畫請加在 8.6.2，不要加在這一段後面**，否則會再壞一次。
     ② **權重**：`[data-aos]` 刻意寫兩次把權重墊到 (0,4,0)，
        壓過 8.6.2／8.6.6／`@supports` 那些 (0,3,0) 的規則。
        **這不是筆誤，不要「順手清掉一個」**（同第 99 節 `[hidden][hidden]` 的手法）。
        有了②，就算日後有人在後面插規則也還有一層擋著。
   ⚠️ `!important` 不是正解（第 7 節第 2 條禁用，而且它連 JS 也蓋不掉）。
   ⚠️ 為什麼不是「靠 AOS 的 disable 就好」：AOS 以 `disable: true` 啟動時
      只會拔掉**當下 DOM 裡**的 data-aos，之後才由 JS 插入的節點（消息卡、
      各房卡、相簿格）沒被拔到，它們仍然吃得到本節的規則。
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .anim-on [data-aos][data-aos],
  .anim-on [data-aos][data-aos]:not(.aos-animate) {
    opacity: 1;
    transform: none;
    filter: none;
    clip-path: none;
    transition: none;
    transition-delay: 0s;
  }

  /* 8.6.3 的子元素連鎖是 (0,5,0)，這裡把 `[data-aos]` 寫兩次墊到 (0,6,0)。 */
  .anim-on .section-head[data-aos][data-aos] .section-head__text > *,
  .anim-on .section-head[data-aos][data-aos]:not(.aos-animate) .section-head__text > *,
  .anim-on .section-head[data-aos][data-aos].aos-animate .section-head__text > * {
    opacity: 1;
    transform: none;
    transition: none;
    transition-delay: 0s;
  }

  .anim-on [data-aos][data-aos] .eyebrow::before,
  .anim-on [data-aos][data-aos]:not(.aos-animate) .eyebrow::before,
  .anim-on [data-aos][data-aos].eyebrow::before,
  .anim-on [data-aos][data-aos]:not(.aos-animate).eyebrow::before {
    transform: none;
    transition: none;
  }

  /* 8.6.4 的載入動畫（page-head）不吃上面那幾條 —— 它是 animation 不是 transition。 */
  .anim-on .page-head .eyebrow,
  .anim-on .page-head__title,
  .anim-on .page-head__lead,
  .anim-on .page-head .breadcrumb,
  .anim-on .page-head--photo::before {
    animation: none;
  }
}

/* --- 鐵則 3：圖片一律標尺寸 ---
   HTML 上必須寫 width/height；這裡給的是 fallback 比例，避免漏標時整塊塌掉。 */
.img-cover {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  /* 🔴 不寫 transform: scale() ／ object-position 偏移。
     那種值是針對某一張圖的構圖算出來的，客戶換圖就會把主體推出框外，
     而且不報錯、只是看起來很怪。構圖一律在上傳前裁好。 */
}

/* ==========================================================================
   8.5 登入狀態的顯示切換
   --------------------------------------------------------------------------
   🔴 用 CSS 依 html[data-auth] 控制，不用 JS 逐個加 hidden。
      JS 的做法會讓使用者先看到未登入版、再跳一次變成登入版——那是首屏閃爍的一種。
      html 上的 data-auth 由 <head> 的 inline script 在首次繪製前寫入。
   ⚠️ 前台權限是二元的：未登入看首頁／關於我們／最新消息，登入才看家庭樹與相簿。
   ========================================================================== */
html[data-auth="guest"]  [data-auth-only="member"] { display: none; }
html[data-auth="member"] [data-auth-only="guest"]  { display: none; }

/* ==========================================================================
   9. 字級三檔切換（CLAUDE.md 已定案要做）
   --------------------------------------------------------------------------
   長輩多半不會用系統級字級調整，站上要有自己的控制。
   切換方式：script.js 在 <html> 上設 data-fs="mid" / "large"，預設不設屬性。
   ========================================================================== */
/* 🔴 這裡刻意什麼都不做。字級三檔由 :root[data-fs] 的三組 token 值提供（見第 3 節）。
   ⚠️ 舊的 --font-scale 乘數已整個移除，不是只改按鈕文字——
      兩套縮放機制並存會疊乘，而且與「三檔都是整數」的要求直接衝突。
   ⚠️ html 的 font-size 維持瀏覽器預設的 100%，見 Base Reset。 */

/* ==========================================================================
   10. 版面容器
   ========================================================================== */
.wrap {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

.section {
  padding-block: var(--section-pad);
}

/* 區塊底色 —— 用 class 指定，不在頁面 CSS 裡各寫一份。
   🔴 只有一個修飾詞：demo 的區塊是「奶油白頁面底」與「奶油區塊底」交錯，沒有第三種。
   ~~.section--cream／.section--peach~~ 已刪除——蜜桃橘與米白兩個色都不存在了，
   留著會讓下一個人以為還有第三種區塊底。 */
.section--alt { background-color: var(--bg-alt); }

/* 🔴 R10-10（PM 2026-08-24）：三個列表頁（家庭天地／最新消息／活動相簿）
   在 768px 以下，**第一個內容區塊與上方 hero 的間距減半**（64 → 32px）。

   ✅ **三頁的來源確實是同一個**（先量過才改，沒有三處各改一份）：
      實測 768px 下三頁的間隙都是 **64px，且都由 `.section` 的 `padding-top`
      ＝ `--section-pad` 提供**，margin 一律 0（沒有 9.23 那種 margin 穿出來的情況）。

   ⚠️ **為什麼要多一個 class，不用 `.page-head ~ .section`**：
      `about.html` 有 **5 個** `.section` 也跟在 `.page-head` 後面，
      那個選擇器會把它們全部改掉——**那是區塊之間的間距，不是 hero 與內容的間距**，
      PM 沒有要動它。而畫面上只會「整頁變緊一點」，很難聯想到是選擇器掃太寬。
   ⚠️ 也不用相鄰選擇器 `+`：`family.html`／`albums.html` 的 hero 之後隔著一個
      未登入版的區塊，登入狀態下它是 `display:none` **但仍然佔著 DOM 位置**，
      `+` 會選到那個看不見的、選不到真正顯示的那一個。
   ⚠️ 只動 `padding-top`：`padding-bottom` 是這個區塊與頁尾的距離，PM 沒有要改。
   ⚠️ 用 `--section-pad ÷ 2` 而不是寫死 32px——那個 token 就是製造這段間距的來源，
      日後它一改，這裡要跟著改才是對的（9.22 的同一條判準）。
   ⚠️ **`--section-pad` 自己也在 768 換檔**（`min-width: 768px` → 64 變 96），
      所以實測是 767→**32**、768→**48**、769→96 三段，不是一路 32。
      **兩段都確實是「減半」**（各自對自己那一檔的基準減半），PM 說的是 768px 那一檔，
      在那裡是 96→48 ✅。**看到 767 與 768 不同不要以為是寫錯了。** */
@media (max-width: 768px) {
  .section--under-head { padding-top: calc(var(--section-pad) / 2); }
}

/* ==========================================================================
   11. B3 共用元件
   --------------------------------------------------------------------------
   🔴 造輪子前先查 CLAUDE.md 第 6 節元件表。新增元件就往那裡加一行。
   ⚠️ 各頁只能套用這裡的元件，不可複製首頁區塊改一改——那會產生隱形分叉：
      八個頁面各有一張略微不同的卡片，改一次要改八次，且必然漏掉一兩處。
   ========================================================================== */

/* --------------------------------------------------------------------------
   11.1 按鈕 .btn（2026-08-13 R1b 改用 demo 的三種按鈕）
   demo：padding 14/32、膠囊、min-height 44、文字後接一支細長線條箭頭。
   ⚠️ 原本的「Figma 270×70 固定寬 ＋ 尾端圓點」整組拆掉——
      270px 固定寬在 demo 的 1400px 版面裡過重，圓點也不是 demo 的語彙。

   🔴 三個變體對應 demo 實際存在的三種按鈕，不多不少：
        --primary  咖啡棕底＋奶油白字＋膠囊（demo .btn-primary）
        --accent   珊瑚粉底＋咖啡棕字＋**直角**（demo .about-cta／.cta-banner-btn／.events-cta）
        --outline  白底＋咖啡棕字＋細框膠囊（demo .btn-secondary）
      ~~--dark（深綠底）／--soft（淡青底）已刪除~~——那兩個色不存在了，
      留著不用就是隱形分叉：下一個人會以為它們是現行元件而拿去套。
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 2.75rem;       /* 44px 點擊區下限（demo 也是這個值） */
  padding: 0.875rem var(--sp-8);   /* 14 / 32 */
  border-radius: var(--r-field);
  font-size: var(--fs-sm);
  font-weight: var(--fw-normal);
  line-height: var(--lh-tight);
  transition: background-color var(--dur-base) var(--ease),
              color var(--dur-base) var(--ease),
              box-shadow var(--dur-base) var(--ease),
              transform var(--dur-fast) var(--ease);
}
.btn:active { transform: translateY(1px); }

/* 按鈕內的箭頭。
   🔴 2026-08-14 R2l⑤：改用 **Lucide `arrow-right`**，不再是自訂 SVG。
      PM：「看起來不是圖示，而且很細很小」——原因找到了：舊的 `.btn__arrow`
      是 `20 × 3px`，那是**一條 3px 高的線**，不是圖示，所以怎麼看都不對。
   ✅ 這也回到第 7 節第 3 條（圖示只准 Lucide）。當初把它當成「demo 的設計特徵」
      而做成自訂 SVG，是我方判斷錯了，PM 的直覺是對的。
   🟡 **輪播的長箭頭（96×24 的 #icon-arrow-long-*）保留**——那是 demo 的導航語彙、
      PM 附圖裡也是它，且沒有被指出問題。若也要換請說。 */
.btn__arrow {
  flex: none;
  width: 1.125em;
  height: 1.125em;
  transition: transform var(--dur-base) var(--ease);
}
.btn:hover .btn__arrow { transform: translateX(0.25rem); }

/* 按鈕**前置**圖示（2026-08-17 R3a：header 右上角的登入／我的帳號）。
   ⚠️ 與 `.btn__arrow` 是兩件事：箭頭在文字後面、會隨 hover 往右推；
      這個在文字前面、不動——它的作用是讓長輩一眼認出這顆是「登入」還是「帳號」。
   🔴 圖示只准 Lucide（第 7 節第 3 條）：登入用 `log-in`、帳號用 `circle-user`。
      ⚠️ 帳號不用 `user`——那個已經是大頭貼的預設圖示（§11.14），
      同一個圖形在站上代表兩件事，長輩分不出來。
   ⚠️ 用 em 不用 px：它要跟著按鈕字級走，字級放大鍵才對它生效。 */
.btn__icon {
  flex: none;
  width: 1.25em;
  height: 1.25em;
}
/* 手機收合選單裡的同兩個連結。
   ⚠️ `.site-nav__link` 預設是 block，不加這個 class 圖示會自己占一行。 */
.site-nav__link--icon {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* 主按鈕：咖啡棕底＋奶油白字 12.44:1 —— 全站對比最安全的一組
   🔴 hover 不可照抄 demo：它寫 background: var(--maroon-800)，而那個變數是 #656565
      **純灰**（名字說棕、值是灰）→ 主按鈕 hover 會從咖啡棕變成灰色。那是 demo 的 bug。 */
/* 🔴 2026-08-14 R2l③：按鈕 hover 一律「加陰影」，深底的不再變色。
   PM 原話：「一樣都是按鈕，那就統一懸停時有陰影效果」。
   **深底本來就是深底，再加深沒有回饋**——原本的 #35191A 與 #4A2827 肉眼難分。
   ⚠️ 不要照 demo 的 .btn-primary:hover：它 hover 變 --maroon-800，
      而那個變數是**純灰 #656565**（demo 的 bug，見 3.1 與下方註解）。 */
.btn--primary {
  background-color: var(--c-maroon);
  color: var(--text-inverse);
  box-shadow: var(--shadow-sm);
}
.btn--primary:hover { box-shadow: var(--shadow-md); }

/* 強調按鈕：珊瑚粉底＋**直角**，demo 用在關於我們／CTA／消息三處的「了解更多」
   🔴 文字用咖啡棕不用白 —— 白 on 珊瑚粉只有 1.78:1，CLAUDE.md 3.1 列為不可用；
      咖啡棕 on 珊瑚粉 7.28:1。**demo 那三顆是白字，不可照抄。** */
.btn--accent {
  background-color: var(--c-coral);
  color: var(--text-on-warm);
  border-radius: 0;            /* demo 刻意的直角，與膠囊主按鈕形成對比 */
  box-shadow: none;
}
/* 🔴 R2l③：淺底按鈕 hover 一律轉成深棕底＋米白字＋陰影（12.44:1，對比沒問題）。
   淺底變色才有回饋——珊瑚粉再加深一階肉眼幾乎看不出來。 */
.btn--accent:hover {
  background-color: var(--c-maroon);
  color: var(--text-inverse);
  box-shadow: var(--shadow-md);
}

/* 外框按鈕：家庭樹卡的「查看家庭樹／查看家庭相簿」
   ⚠️ 兩顆必須等寬等重——相簿是這一房的另一半內容，不可看起來像次要選項。 */
.btn--outline {
  padding-inline: var(--sp-5);
  border: 1.5px solid var(--border-soft);
  background-color: var(--bg-card);
  color: var(--text-heading);
}
/* R2l③：同上，淺底 → 深棕底＋米白字＋陰影。原本只換邊框顏色，回饋太弱。 */
.btn--outline:hover {
  background-color: var(--c-maroon);
  border-color: var(--c-maroon);
  color: var(--text-inverse);
  box-shadow: var(--shadow-md);
}

/* header 上的登入按鈕 */
.btn--sm {
  padding-inline: var(--sp-5);
  font-size: var(--fs-xs);
}

/* --------------------------------------------------------------------------
   11.2 區塊標題群 .section-head
   demo：eyebrow（letter-spacing .08em）→ 主標 → 內文（line-height 1.9）
   demo 的區塊標題群是**置中**的（.families-intro／.events-intro，max-width 720px），
   關於我們那一區則是左右分欄裡的左對齊。兩種都要，用修飾詞區分。
   -------------------------------------------------------------------------- */
.section-head {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: end;
  gap: var(--sp-5) var(--sp-10);
}
.section-head__text { grid-column: 1; }
.section-head__action { grid-column: 2; }

/* demo 的預設：置中、限寬 720px */
.section-head--center {
  grid-template-columns: 1fr;
  justify-items: center;
  text-align: center;
}
.section-head--center .section-head__text { max-width: 45rem; }
.section-head--center .section-head__action { grid-column: 1; }

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 0.6875rem;            /* 21px - 10px 圓點 */
  font-size: var(--fs-xs);
  font-weight: var(--fw-normal);
  letter-spacing: 0.08em;    /* demo 的 eyebrow 字距 */
  /* 🔴 色值走 --text-eyebrow ＝ 莓紅 #A8555F（4.89:1）。
     **demo 的 eyebrow 全部用珊瑚粉 #fc938e，實測 2.08:1，長輩讀不到，不可照抄。**
     莓紅是 demo 自己色票裡的 --coral-700，色相幾乎沒變、還是粉調。
     ⚠️ 不要為此另外調一個新色。 */
  color: var(--text-eyebrow);
}
/* 前綴圓點：demo 沒有，我方保留——它讓 eyebrow 這一行在掃視時定位得到，
   而且與主標的字重差異在襯線體下比黑體更不明顯。 */
.eyebrow::before {
  content: "";
  width: 0.625rem;
  height: 0.625rem;
  border-radius: var(--r-round);
  background-color: currentColor;
}

.section-title {
  margin-top: var(--sp-3);
  font-size: var(--fs-h2);
  font-weight: var(--fw-bold);
  color: var(--text-heading);
}

.section-lead {
  margin-top: var(--sp-5);
  font-size: var(--fs-body);
  line-height: var(--lh-prose);   /* demo 的區塊內文是 1.9，比一般內文寬 */
  color: var(--text-main);
}

/* 區塊之間的短線分隔（demo 的 .family-tree-section-divider）
   ⚠️ 取代已刪除的 .wave —— 波浪是 Figma 稿的元素，不是 demo 的。 */
.divider {
  width: 100%;
  max-width: 5rem;
  height: 1px;
  margin: 0 auto;
  border: 0;
  background-color: var(--border-soft);
}

/* --------------------------------------------------------------------------
   11.3 分類頁籤 .tabs（最新消息）
   active＝咖啡棕底＋奶油白字 12.44:1／預設＝白底咖啡棕字 12.93:1。兩者都合規。
   -------------------------------------------------------------------------- */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}
.tab {
  min-height: 2.75rem;       /* 44px 點擊區下限 */
  padding-inline: var(--sp-6);
  display: inline-flex;
  align-items: center;
  border: 1.5px solid var(--border-soft);
  border-radius: var(--r-field);
  background-color: var(--bg-card);
  color: var(--text-heading);
  font-size: var(--fs-sm);
  transition: background-color var(--dur-base) var(--ease),
              border-color var(--dur-base) var(--ease),
              color var(--dur-base) var(--ease);
}
.tab:hover { border-color: var(--c-coral); }
.tab[aria-selected="true"] {
  background-color: var(--c-maroon);
  border-color: var(--c-maroon);
  color: var(--text-inverse);
}

/* --------------------------------------------------------------------------
   11.4 分類標籤 .tag（文章卡上的分類徽章）
   🔴 demo 是珊瑚粉底白字 1.78:1 → 文字改咖啡棕 7.28:1。底色維持珊瑚粉。
   -------------------------------------------------------------------------- */
.tag {
  display: inline-flex;
  align-items: center;
  min-height: 1.75rem;
  padding: 0.1875rem var(--sp-3);
  border-radius: var(--r-field);
  background-color: var(--c-coral);
  color: var(--text-on-warm);
  font-size: var(--fs-xs);
  font-weight: var(--fw-normal);
  line-height: var(--lh-tight);
}

/* --------------------------------------------------------------------------
   11.5 輪播控制 .carousel-arrow／.carousel-dots
   🔴 改用 demo 的細長線條箭頭：無底色、無陰影、一半疊在卡片上一半露在區塊邊界外。
      這是 demo 視覺辨識度最高的元素之一。
   ⚠️ 但 08-04 的四條長輩友善準則全部仍適用：箭頭必須明顯可見、
      點擊區 ≥44×44px、圓點指示要有、自動播放 ≥6 秒且移入暫停。
      → 因此線條再細，**點擊區仍靠 padding 撐到 44px**，不可只有線本身那幾 px。
   -------------------------------------------------------------------------- */
.carousel-arrow {
  min-width: 2.75rem;        /* 44×44 點擊區下限 */
  min-height: 2.75rem;
  padding: var(--sp-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background-color: transparent;
  color: var(--text-heading);
  transition: color var(--dur-base) var(--ease),
              opacity var(--dur-base) var(--ease),
              transform var(--dur-base) var(--ease);
}
.carousel-arrow svg { width: 6rem; height: 1.5rem; }
.carousel-arrow:hover:not(:disabled) { opacity: 0.7; }
.carousel-arrow:disabled {
  opacity: 0.28;
  cursor: not-allowed;
}
/* disabled 元素的對比度 WCAG 豁免；但仍保留可見輪廓，長輩才知道「到頭了」而不是「壞了」 */

.carousel-dots {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.carousel-dot {
  width: 0.625rem;
  height: 0.625rem;
  border-radius: var(--r-round);
  background-color: var(--border-soft);
  transition: width var(--dur-base) var(--ease),
              background-color var(--dur-base) var(--ease);
}
.carousel-dot[aria-current="true"] {
  width: 1.625rem;
  background-color: var(--c-coral);
}
/* 圓點本身只有 10px，但點擊區必須 ≥44px —— 用透明外距補足，不改變視覺 */
.carousel-dot-hit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: 2.75rem;
}

/* --------------------------------------------------------------------------
   11.7 卡片
   -------------------------------------------------------------------------- */
.card {
  background-color: var(--bg-card);
  border-radius: var(--r-card);
  overflow: hidden;
}
/* 🔴 --r-img 是 0：demo 的照片一律直角，那是這套視覺的特徵之一，不是漏標圓角。
   要改回圓角只改 §5 的 --r-img 一處，全站生效。 */
.card__media {
  border-radius: var(--r-img);
  overflow: hidden;
  background-color: var(--bg-alt);   /* 圖片載入前的底色，避免白色空洞 */
}

/* --------------------------------------------------------------------------
   11.7b 家庭樹卡 .branch-card（各房卡片）
   ⚠️ 首頁 ③ 與家庭天地列表頁 C3 用的是同一張卡，所以它在共用元件層，
      不在 index.css —— 留在首頁專屬檔就會被複製成第二份，
      之後改一次要改兩處，而且必然漏掉一處。
   -------------------------------------------------------------------------- */
/* 🔴 R7-03（2026-08-18 PM）：桌機由 3 欄改 **2 欄**。
   **理由**：十房是偶數，兩欄剛好 5 排、最後一排不留空；3 欄會在最後一排留一個空位。
   ⚠️ 899px 以下本來就是 2 欄、599px 以下 1 欄，那兩段不用改
   （2 欄與 1 欄都整除 10）。
   ✅ **分頁不必移除——這一頁本來就沒有 `.pager`**（規格寫「連呼叫一起移除」，
      實測 `family.html`／`family.js` 都是 0 處）。總數固定 10，也確實不需要。 */
.branch-grid {
  margin-top: var(--sp-12);
  display: grid;
  /* 每排幾個由後台決定（頁面管理 → 家庭天地）；沒設定就 2 個 */
  grid-template-columns: repeat(var(--branch-cols, 2), 1fr);
  gap: var(--sp-10);
}

/* 🔴 2026-08-14 R2b：改回 demo 的樣式（PM 附圖）。
   結構＝**白色相框包住照片** → `幾房｜姓名` → `已登錄 XX 位成員` → 兩顆按鈕。
   ⚠️ 卡片本身不再是「白底＋陰影＋圓角」的容器——demo 的家族卡是裸圖＋置中文字，
      PM 要的是在那個基礎上加一層白色相框感。
   ⚠️ 首頁 ③ 與家庭天地列表頁用的是同一份卡片與同一個 renderBranchCards()，
      所以改一次兩頁一起變。這正是當初抽成共用元件的理由。 */
/* 🔴 2026-08-14 R2n／R2r：卡片本體。**兩個地方共用**——
   首頁 ③（輪播）與家庭天地列表頁（grid），改一次兩頁一起變。
   ⚠️ 因此**輪播才有的樣式一律加 `.carousel__track` 前綴**，見下方縮放那段。 */
.branch-card {
  display: flex;
  flex-direction: column;
  text-align: center;
}

.branch-card__media-wrap { overflow: hidden; }

/* 🔴 demo 的家庭樹卡是直式照片（3 / 3.4），不是橫式。
   直式讓一列放得下三張、又不會太矮，是它輪播看起來舒服的原因之一。
   ⚠️ 改這個 aspect-ratio 要同步改下方的 --branch-media-h。 */
/* 🔴 R8-01（PM 2026-08-19）：3 / 3.4（直式）→ **4 / 3（橫式）**。
   起因是 R7-03 把家庭天地列表改成一排兩張，卡變寬之後直式照片跟著變得很高，
   整頁被拉得很長。
   ⚠️ **輪播那一份要一起改**：下方 `--branch-media-h` 是「圖片高度相對卡片寬」的
      百分比，它是從這個 aspect-ratio 算出來的（3.4÷3 → 3÷4）。
      🔴 **只改這裡不改那裡不會報錯**——輪播裡非中間的那幾張，
         下方文字會用舊比例去補位移，卡片文字就會錯位一小段。 */
.branch-card__media {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background-color: var(--bg-alt);
}

/* --------------------------------------------------------------------------
   🔴 「中間大、左右小」**只在輪播裡**，不可套到列表頁
   --------------------------------------------------------------------------
   2026-08-14 修一個迴歸：上一輪把縮放寫在 .branch-card 層，
   結果**家庭天地列表頁的 10 張卡全部被縮到 0.72**（列表頁沒有輪播、
   每一張都是「非中間」），圖片比卡片窄一圈、標題還套了負 margin。
   ⚠️ 沒有報錯，看起來只是「圖片小了點」，很容易被當成設計。
   **判準：只要樣式的意義依賴「這是輪播」，就必須加輪播容器的前綴。**
   -------------------------------------------------------------------------- */
.carousel__track .branch-card {
  --branch-scale: 0.72;
  /* 圖片高度換算成「相對卡片寬」的百分比：aspect-ratio 4 / 3 → 3 ÷ 4 ＝ 75%（R8-01 前是 113.333%）
     ⚠️ 定義在**卡片**上不是縮放層上——下方的 __label 是縮放層的兄弟元素，
        CSS 自訂屬性只往下繼承、不橫向傳遞。 */
  --branch-media-h: 75%;
}

.carousel__track .branch-card__media-wrap {
  transform: scale(var(--branch-scale));
  /* 秒數要與 .carousel__track 的 transition 一致（見 index.css），
     否則圖片縮放與軌道平移不同步，會看到卡片先到位、圖片才長大。 */
  transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1);
  will-change: transform;
}
.carousel__track .branch-card.is-center .branch-card__media-wrap { transform: scale(1); }

/* 🔴 transform 不影響文檔流：圖縮小後上下各空出一段，下方文字不會跟著上移，
   看起來就是「非中間那幾張的文字掉下去了」。這裡把那段空白補回來。
   ⚠️ demo 直接寫死 `calc(20px - 15.87%)`，我方改成由上面兩個值算出來——
      魔術數字的問題不是難看，是改了圖片比例之後文字錯位而且不會報錯。 */
.carousel__track .branch-card:not(.is-center) .branch-card__label {
  margin-top: calc(var(--sp-5) - var(--branch-media-h) * (1 - var(--branch-scale)) / 2);
}

/* 切換中的文字淡出（R2n 階段一／三）。
   🔴 隱藏由**軌道上的單一 class** 控制，不是逐一寫 inline style——
      後者只要流程中途被打斷（分頁切走、連按取消動畫），那 100 個 `opacity: 0`
      就永久留著，整排卡片文字消失且不報錯。實測踩過，見 index.js 的 slide()。
   ⚠️ 秒數要與 index.js 的 FADE_MS 一致（180ms）。 */
.carousel__track .branch-card__label,
.carousel__track .branch-card__meta {
  transition: opacity 0.18s ease;
}
.carousel__track.is-switching .branch-card__label,
.carousel__track.is-switching .branch-card__meta {
  opacity: 0;
}

/* `幾房｜姓名`．⚠️ 分隔線用全形直線「｜」，不是半形 | —— 中文排版的字重與間距才一致 */
.branch-card__label {
  margin-top: var(--sp-5);
  font-size: var(--fs-lead);   /* 卡片標題走 --fs-lead，不是 --fs-h3（見第 3 節） */
  font-weight: var(--fw-bold);
  color: var(--text-heading);
}

/* 🔴 2026-08-14 R2r：加回「涵蓋代數」，卡片變五層
   （圖片／幾房｜姓名／涵蓋代數／已登錄 XX 位／兩顆按鈕）。
   ⚠️ **沒有照我方原建議合成一行**——「第八代 – 第十二代 ・ 已登錄 185 位成員」
      在桌機一張卡只有 380px 寬時會斷成兩行，變成「看起來像一行、實際是兩行」的
      不整齊；分成兩行反而每一行都完整。多出來的高度是一行 16px，卡片撐得住。

   🔴 2026-08-14 R2b 推翻 08-03 定案「血親 N 人・姻親 M 人分開顯示」，改為合併。
   ⚠️ **原定案的理由是「合併定義模糊」，所以合併後必須把定義寫死**：
      **已登錄人數 ＝ 該房家庭樹上所有成員卡（血親＋姻親）**。
   🔴 **人數未登錄時不可顯示「已登錄 0 位成員」**——沒登錄 ≠ 沒有人，
      那會傳達出這一房絕嗣的意思，在宗親會會出事。改顯示「資料整理中」。 */
.branch-card__meta {
  margin-top: var(--sp-2);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

.branch-card__actions {
  margin-top: auto;
  padding-top: var(--sp-5);
  display: grid;
  grid-template-columns: 1fr 1fr;        /* 🔴 兩顆等寬等重：相簿是這一房的另一半內容 */
  gap: var(--sp-3);
}
.branch-card__actions .btn { padding-inline: var(--sp-3); }

/* --------------------------------------------------------------------------
   11.7c 內頁頁首 .page-head（麵包屑＋標題群）
   ⚠️ 首頁沒有這一塊（首頁直接是主視覺），但其餘每一頁都有 —— 因此它在共用元件層。
   ⚠️ 麵包屑用 <nav aria-label="麵包屑">，最後一層是目前位置、不做連結。
   -------------------------------------------------------------------------- */
/* 2026-08-13 R1e 改用 demo 的內頁橫幅結構：整片深底、**標題置中、麵包屑在標題下方**。
   ⚠️ 麵包屑放在標題「下方」是 demo 的做法，與一般網站相反——
      它把標題當成這一頁的門面，麵包屑退成輔助資訊。這是刻意的，不要「修正」回上方。
   🔴 **2026-08-17 R3d：每一個內頁都用照片橫幅**（PM：「內頁的 hero 區都要有背景圖，
      就像關於我們頁面一樣」）。目前 14 個內頁全部是 `.page-head--photo`，
      **沒有純色版在用**——但純色的規則保留著，因為 `--photo` 是加上去的一層。
      ~~原本寫「只有家庭天地／家庭樹兩頁用照片，表單頁與長文頁用純色，
      理由是放大照片只是讓使用者多捲一段」——PM 08-17 推翻，不要照著那句改回去。~~
      ⚠️ 代價是知道的：`--photo` 帶 `min-height: 340px`，表單頁的頁首因此變高，
      而且首屏多一張 110–390KB 的背景圖。這是 PM 的選擇，不是疏漏。 */
.page-head {
  padding-block: var(--sp-16);
  background-color: var(--c-maroon);
  color: var(--text-inverse);
  text-align: center;
}
.page-head > .wrap { display: flex; flex-direction: column; align-items: center; }

/* 照片橫幅版．背景圖由頁面用 --page-head-img 指定（runtime 才知道是哪一張，
   這是本案少數合理的 inline style 之一） */
.page-head--photo {
  position: relative;
  overflow: hidden;
  min-height: 21.25rem;      /* 340px．demo 的高度 */
  display: flex;
  align-items: center;
}
.page-head--photo::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--page-head-img);
  background-size: cover;
  background-position: center 40%;
}
/* 🔴 遮罩濃度回推自「內文 ≥4.5:1」，不是「標題 ≥3:1」——
   標題是大字很好過，真正卡住的是下方的說明文字與麵包屑。
   實測（假設照片最亮處＝純白）：0.62 → 奶油白只有 4.57，貼著門檻；0.68 → 5.5。
   ⚠️ demo 用 0.4→0.6 的漸層，上緣 0.4 只有 2.5:1，而標題垂直置中正好落在那一段。 */
.page-head--photo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(46, 25, 25, 0.68), rgba(46, 25, 25, 0.76));
}
.page-head--photo > .wrap { position: relative; z-index: 1; }

/* 🔴 照片橫幅裡的次要文字要回到奶油白，不可用 --text-inverse-soft。
   那個色是為「純咖啡棕底」調的（9.31:1）；壓在照片上只剩 3.42:1，14–16px 過不了。
   ⚠️ 純色版的 page-head 不受影響，仍走較柔和的 soft 色。 */
.page-head--photo .page-head__lead,
.page-head--photo .breadcrumb { color: var(--text-inverse); }

/* 🔴 page-head 一定是深底（純咖啡棕，或照片＋遮罩），eyebrow 必須改色。
   實測莓紅 `#A8555F` 在這裡只有 **2.54**（純色底）／**1.13–1.50**（照片底）——完全讀不到。
   奶油白是 12.44／5.51–7.32 ✅。
   ⚠️ 與 index.css 的 `.eyebrow--on-dark` 是同一條規則的兩個落點：
      **深底上的 eyebrow 一律奶油白，不用莓紅也不用珊瑚粉。**
      層次改由字級（16 vs 45）、字距與字重撐——與 CTA 那一區的處理一致。
   ⚠️ `.eyebrow::before` 的圓點是 currentColor，會自動跟著變。 */
.page-head .eyebrow { color: var(--text-inverse); }

.page-head__title {
  font-size: var(--fs-h2);
  color: var(--c-white);
}
.page-head__lead {
  margin-top: var(--sp-4);
  max-width: 45rem;
  font-size: var(--fs-body);
  line-height: var(--lh-prose);
  color: var(--text-inverse-soft);
}

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
  font-size: var(--fs-xs);
  color: var(--text-inverse-soft);
}
.breadcrumb a { text-decoration: underline; text-underline-offset: 0.25em; }
.breadcrumb a:hover { color: var(--c-white); }
.breadcrumb li:not(:last-child)::after {
  content: "／";
  margin-left: var(--sp-2);
  opacity: 0.6;
}
.breadcrumb [aria-current="page"] { color: var(--c-white); font-weight: var(--fw-normal); }

/* 需登入才能看的頁面，未登入時顯示的攔截區塊。
   ⚠️ 內容一律由 JS 在確認登入後才抓，HTML 裡不放任何真實資料——
      光靠 CSS 隱藏擋不住看原始碼的人，而這一頁的內容是在世家人的姓名與生年。 */
.auth-wall {
  padding-block: var(--sp-30);
  text-align: center;
}
.auth-wall__actions {
  margin-top: var(--sp-8);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  justify-content: center;
}

/* --------------------------------------------------------------------------
   11.8 空狀態 .empty-state
   ⚠️ 家庭樹相關的空狀態，文案與插圖都不可傳達荒涼／孤單／凋零。
      「這一房還沒登錄」不是「這一房沒人」——在宗親會傳達出絕嗣聯想會出事。
   -------------------------------------------------------------------------- */
.empty-state {
  padding: var(--sp-20) var(--sp-5);
  text-align: center;
  color: var(--text-main);
}
/* 空狀態／錯誤頁的下一步按鈕列。
   🔴 403／404 的共通原則：每一頁都要給**明確的下一步**——
      只說「出錯了」會讓長輩卡在原地，而他不會自己想到要按上一頁。
   ⚠️ 手機的 .btn 是 width:100%，這裡要讓它們各佔一行而不是擠成一排。 */
.empty-state__actions {
  margin-top: var(--sp-8);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  justify-content: center;
}

/* --------------------------------------------------------------------------
   11.10 提示視窗 .swal-lin（SweetAlert2 的外觀覆蓋）
   未登入點「家庭天地」「活動相簿」時彈出，是全站共用元件。
   ⚠️ 特異性要壓過 SweetAlert2 —— 它的 CSS 內嵌在 JS 裡、執行時才注入 <head>，
      永遠排在本檔之後，所以這裡刻意疊到三層 class，而不是用 !important。
   -------------------------------------------------------------------------- */
.swal2-container .swal2-popup.swal-lin {
  /* 上內距 56 不是 40：要空出 44×44 的關閉鈕那一列（R3c）。
     ⚠️ 關閉鈕改成絕對定位後不再佔版面，**必須靠這個內距讓位**，
        否則標題會被壓在它底下——而標題幾乎滿版寬，一定會撞到。 */
  padding: var(--sp-14) var(--sp-8) var(--sp-8);
  border-radius: var(--r-card);
  background-color: var(--c-white);
  font-family: var(--font-body);
}
.swal2-container .swal-lin .swal2-title {
  padding: 0;
  font-size: var(--fs-h3);
  font-weight: var(--fw-bold);
  color: var(--text-heading);
  line-height: var(--lh-heading);
}
.swal2-container .swal-lin .swal2-html-container {
  margin: var(--sp-5) 0 0;
  font-size: var(--fs-xs);
  font-weight: var(--fw-normal);
  color: var(--text-main);
  line-height: var(--lh-body);
}
.swal2-container .swal-lin .swal2-actions {
  margin-top: var(--sp-8);
  gap: var(--sp-4);
  flex-wrap: wrap;
}
/* 兩顆按鈕都要夠明顯：點進來的人可能是「有帳號沒登入」，也可能是「還沒加入」，
   兩種人各佔一半，不該把其中一條路做成次要選項。 */
.swal2-container .swal-lin .swal2-styled {
  min-width: 9.5rem;
  min-height: 3.25rem;      /* 點擊區 ≥44px */
  margin: 0;
  padding-inline: var(--sp-6);
  border-radius: var(--r-field);
  font-size: var(--fs-xs);
  font-weight: var(--fw-normal);
  box-shadow: none;
}
.swal2-container .swal-lin .swal2-styled.swal2-confirm {
  background-color: var(--c-maroon);      /* 白字 on 深綠 9.79:1 ✅ */
  color: var(--text-inverse);
}
.swal2-container .swal-lin .swal2-styled.swal2-cancel {
  background-color: var(--c-coral);      /* 深褐 on 蜜桃橘 6.55:1 ✅ */
  color: var(--text-on-warm);
}
.swal2-container .swal-lin .swal2-styled:focus-visible {
  box-shadow: var(--shadow-focus);
}

/* 關閉鈕（2026-08-17 R3c）。
   🔴 SweetAlert2 預設的 × 是 1.2em 的字、沒有固定點擊區——對長輩太小也太淡。
      這裡把它撐到 44×44 並換成內文色，不是裝飾性的淡灰。
   ⚠️ 位置要避開標題：popup 的上內距是 --sp-10（40px），關閉鈕塞在那一段裡。
   ⚠️ 不要用 `font-size` 撐大點擊區——那會讓 × 本身變得像一顆按鈕上的文字，
      而且字級放大鍵一開就會擠到標題。固定 44×44 才是點擊區下限的意思。 */
.swal2-container .swal-lin .swal2-close {
  position: absolute;   /* 🔴 SweetAlert2 讓它排在標題那一列（flow），會把標題擠歪 */
  top: var(--sp-2);
  right: var(--sp-2);
  width: 2.75rem;
  height: 2.75rem;
  font-size: 1.75rem;
  color: var(--text-main);      /* 5.83:1 on 白 ✅，不用預設的淡灰 */
  transition: color var(--dur-base) var(--ease);
}
.swal2-container .swal-lin .swal2-close:hover { color: var(--c-berry); }
.swal2-container .swal-lin .swal2-close:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

/* --------------------------------------------------------------------------
   11.12 ~~每一頁都有的小元件~~ —— **整節已清空，2026-08-24 R10-09**
   --------------------------------------------------------------------------
   這一節原本放 `.skip-link`／`.footer-scene`／`.font-scaler` 三個。
   `.footer-scene` 2026-08-13 R1e 刪除（Figma 稿的元素，demo 的頁尾直接接內容）；
   `.font-scaler` 在下方「字級放大控制」自成一節；
   🔴 **`.skip-link`（跳到主要內容）2026-08-24 由 PM 定案移除**——見下。
   → 本節已無內容，保留標題只是為了讓「11.12 去哪了」有答案，不必回頭翻 git。

   🔴 **`.skip-link` 是 PM 知情後的取捨，不是漏做無障礙、也不是修 bug。**
   我方回報過「實測它的行為是正確的」（`top: -6rem` 藏在畫面外，只有按 Tab 才滑進來，
   從來不會擋到畫面），**PM 仍決定移除**：「不需要這個」。
   **代價已知並接受**：鍵盤與報讀器使用者每一頁要多按 6–16 次 Tab 才能走到內文。
   ⚠️ **日後做無障礙檢查的人看到這裡沒有 skip link，請不要當成缺陷自己補回來**——
      要補請先問 PM。（`<main id="main">` 那個 id 刻意保留，移除它沒有好處。）
   -------------------------------------------------------------------------- */

/* ==========================================================================
   字級放大控制
   ========================================================================== */
/* 🔴 2026-08-14 R2l⑥：**這是 R2a「全站移除圓角」的唯一例外，PM 明示要保留。**
   ⚠️ 不是漏改——日後有人「順手統一一下」把它拿掉，就是改錯了。
   ⚠️ 因此這裡刻意寫 --r-round 而不是 --r-field：
      寫 --r-field 的話，只要有人再調那個 token，這個例外就會被連帶改掉。 */
/* 🔴 R7-10：浮動工具組（回到頂端 ＋ 字級切換）。
   **只有這個容器 fixed**，兩顆按鈕在裡面用 flex 排——
   ⚠️ 兩顆各自 fixed 的話，bottom 偏移必須各自寫死，改了其中一顆的高度
      另一顆不會跟著讓位——**而且是「蓋住」不是「排不下」，看起來像沒事**。
   ⚠️ 字級切換維持在最下面（原本的位置，長輩已經習慣），新的回到頂端疊在它上方。
      HTML 的順序就是「回到頂端在前、字級列在後」，所以這裡用 `column` 不是
      `column-reverse`——**實測寫反過會讓回到頂端跑到字級列下面**。 */
/* 🔴 **這一組的「佔用高度」是別的元件要閃避的依據**（見 `index.css` 的 `.hero__ctrl`）。
   捲動位置 0 時 `.to-top` 帶著 `hidden`，實際只有字級控制那一顆膠囊
   ＝ 按鈕 `min-height` 2.75rem ＋ 上下各 `--sp-1` ＝ **3.25rem（52px）**，實測相符。
   ⚠️ **這個值同時寫在 `index.css` 的 `--hero-ctrl-clear` 裡，是兩地一值**——
      改了這裡的內距或按鈕尺寸，要回頭改那一處，否則首頁 hero 的箭頭會被蓋回去，
      **而且不會報錯**（z-index 90 遠高於 hero 控制列的 3，只是安靜地蓋住）。 */
.floating-tools {
  position: fixed;
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--sp-2);
}

/* 回到頂端。
   🔴 預設 `hidden`（HTML 就寫著），由 JS 在捲過一段距離後才移除——
      首屏就出現會擋住內容，而首屏本來也不需要回到頂端。
   ⚠️ 淡入淡出用 opacity/transform，不是 display——後者沒有中間狀態。
      因為 `[hidden]` 是 display:none，所以 JS 分兩步：先取消 hidden、下一幀才加 .is-in。 */
.to-top {
  width: 2.75rem;                 /* 44×44 下限 */
  height: 2.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-round);
  background-color: var(--c-white);
  color: var(--text-brand);
  box-shadow: var(--shadow-hover);
  opacity: 0;
  transform: translateY(0.5rem);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease);
}
.to-top[hidden] { display: none; }
.to-top.is-in { opacity: 1; transform: none; }
.to-top:hover { background-color: var(--c-cream-100); }
.to-top i { width: 1.25rem; height: 1.25rem; }

@media (prefers-reduced-motion: reduce) {
  .to-top { transition: none; }
}

.font-scaler {
  display: flex;
  gap: 1px;
  padding: var(--sp-1);
  border-radius: var(--r-round);
  background-color: var(--c-white);
  box-shadow: var(--shadow-hover);
}
.font-scaler button {
  min-width: 2.75rem;            /* 44×44 點擊區 */
  min-height: 2.75rem;
  padding-inline: var(--sp-2);
  border-radius: var(--r-round);   /* 同上：PM 指定的例外 */
  font-size: var(--fs-xs);
  color: var(--c-maroon);
}
.font-scaler button:hover { background-color: rgba(74, 40, 39, 0.1); }
.font-scaler button[aria-pressed="true"] {
  background-color: var(--c-maroon);
  color: var(--text-inverse);
}

/* --------------------------------------------------------------------------
   11.14 大頭貼 .avatar
   🔴 預設圖示＝單一中性 Lucide icon，不分男女（PM 2026-08-07）。
      這個選擇順帶解掉一個資料缺口：要分男女就得每個人都判定性別，
      但客戶的族譜填寫表單沒有性別欄，早期先祖最可能連 rank 都沒有。
      中性圖示讓這個判定完全不需要發生。
   -------------------------------------------------------------------------- */
.avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  overflow: hidden;
  border-radius: var(--r-round);
  background-color: var(--bg-alt);   /* 奶油．不用奶油白，那與頁面底同色、看不出圓形 */
  color: var(--border-field);
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar svg { width: 55%; height: 55%; }
.avatar--sm { width: 3rem;  height: 3rem; }     /* 樹上的節點卡 */
.avatar--md { width: 5rem;  height: 5rem; }
.avatar--lg { width: 8.5rem; height: 8.5rem; }  /* 個人視窗、我的帳號 */

/* --------------------------------------------------------------------------
   11.15 彈窗 .modal
   桌機＝置中視窗；手機＝從底部升起的全螢幕 sheet（長輩單手可及）。
   四條長輩友善要求（CLAUDE.md 第 3 節）：
     ・關閉鈕 ≥44×44　・點遮罩可關　・Esc 可關　・焦點開啟移入、關閉回到原節點
   ⚠️ 用原生 <dialog>：焦點鎖定與 Esc 由瀏覽器處理，比自己實作可靠。
   -------------------------------------------------------------------------- */
.modal {
  width: min(34rem, calc(100vw - 2rem));
  max-height: min(80vh, 46rem);
  padding: 0;
  border: 0;
  border-radius: var(--r-card);
  background-color: var(--c-white);
  color: var(--text-main);
  overflow: hidden;
}
.modal::backdrop { background-color: rgba(74, 40, 39, 0.45); }

.modal__head {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-8) var(--sp-6) var(--sp-5);
  background-color: var(--bg-alt);
}
.modal__title {
  font-size: var(--fs-h3);
  font-weight: var(--fw-bold);
  color: var(--text-heading);
  text-align: center;
}
.modal__close {
  position: absolute;
  top: var(--sp-3);
  right: var(--sp-3);
  width: 2.75rem;          /* 44×44 下限 */
  height: 2.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-field);
  color: var(--text-brand);
}
.modal__close:hover { background-color: rgba(74, 40, 39, 0.08); }
.modal__body { padding: var(--sp-6); overflow-y: auto; }

/* 個人視窗的九欄．一律純文字不做連結——父在正上方、配偶在右側、
   母是父親旁的配偶卡，位置本身就是最好的連結，再做一次跳轉是重複。 */
.detail-list { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-3) var(--sp-5); }
.detail-list dt { font-size: var(--fs-xs); color: var(--text-muted); white-space: nowrap; }
.detail-list dd { margin: 0; font-size: var(--fs-xs); color: var(--text-main); }

@media (max-width: 767px) {
  /* 手機改成全螢幕 sheet */
  .modal {
    width: 100vw;
    max-width: 100vw;
    max-height: 92vh;
    margin: auto 0 0;
    border-radius: var(--r-card) var(--r-card) 0 0;
  }
}

/* --------------------------------------------------------------------------
   11.16 表單擴充（乙軌會員三頁用）
   基礎的 .form／.field 在 §14.1，這裡只加多出來的型別。
   -------------------------------------------------------------------------- */
select.field__control {
  appearance: none;
  padding-right: var(--sp-10);
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B5B59' stroke-width='2' stroke-linecap='round'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat: no-repeat;
  background-position: right var(--sp-4) center;
  background-size: 1.25rem;
}
select.field__control:disabled {
  background-color: var(--bg-alt);
  color: var(--text-muted);
  cursor: not-allowed;
}

/* 單選組（性別）．點擊區靠整塊撐開，不是只有那個圓點 */
.radio-group { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
.radio-group label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 2.75rem;
  padding-inline: var(--sp-5);
  border: 1px solid var(--border-field);
  border-radius: var(--r-field);
  font-size: var(--fs-xs);
  cursor: pointer;
}
.radio-group input { accent-color: var(--c-maroon); width: 1.125rem; height: 1.125rem; }
.radio-group label:has(input:checked) {
  border-color: var(--c-maroon);
  background-color: rgba(74, 40, 39, 0.08);
  font-weight: var(--fw-normal);
}

/* 表單分段：長表單一定要分段，否則長輩會在中途放棄 */
.form-section { border: 0; padding: 0; margin: 0; }
.form-section + .form-section { margin-top: var(--sp-10); }
.form-section__title {
  padding: 0;
  font-size: var(--fs-body);
  font-weight: var(--fw-bold);
  color: var(--text-heading);
}
.form-section__hint {
  margin-top: var(--sp-2);
  font-size: var(--fs-xs);
  font-weight: var(--fw-normal);
  color: var(--text-muted);
}
.form-section__body { margin-top: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-6); }

/* 表單層級的訊息（登入失敗、送出結果）
   ⚠️ 同時用底色＋圖示＋文字三種訊號，不只靠顏色 */
.form-alert {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-radius: var(--r-field);
  font-size: var(--fs-xs);
  line-height: var(--lh-body);
}
.form-alert[hidden] { display: none; }
.form-alert--info  { background-color: var(--bg-alt); color: var(--text-main); }
.form-alert--error { background-color: var(--bg-danger-soft); color: var(--c-danger); }
.form-alert--ok    { background-color: rgba(74, 40, 39, 0.1); color: var(--c-maroon); }
.form-alert svg { flex: none; width: 1.25rem; height: 1.25rem; margin-top: 0.125rem; }

/* 可重複的區塊（我登記的子女，可多筆） */
.repeat-item {
  position: relative;
  padding: var(--sp-5);
  border: 1px solid var(--border-field);
  border-radius: var(--r-field);
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}
.repeat-item + .repeat-item { margin-top: var(--sp-4); }
.repeat-item__remove {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  width: 2.75rem;
  height: 2.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-field);
  color: var(--text-muted);
}
.repeat-item__remove:hover { color: var(--c-danger); background-color: var(--bg-danger-soft); }

/* 表單底部的說明列（忘記密碼、還沒有帳號…） */
.form__foot {
  margin-top: var(--sp-6);
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.form__foot a { color: var(--c-maroon); text-decoration: underline; text-underline-offset: 0.25em; }
/* 「聯絡幹部」的電話：字級比一般說明大——這是忘記密碼又換過信箱的長輩
   唯一的出路，不能跟旁邊的說明文字一樣不起眼（E1 F-3）。 */
.form__foot-tel {
  display: inline-block;
  margin-top: var(--sp-2);
  font-size: var(--fs-lead);
  font-weight: var(--fw-bold);
}

/* 大頭貼與其操作按鈕並排（我的帳號） */
.avatar-row {
  display: flex;
  align-items: center;
  gap: var(--sp-6);
  flex-wrap: wrap;
}
.avatar-row .form__actions { margin-top: 0; }

/* --------------------------------------------------------------------------
   11.18 左右欄分頁版型 .panel-layout（我的帳號用）
   ⚠️ 不把所有設定列在同一頁（PM 2026-08-10）——長輩一次只處理一件事，
      一頁到底會讓他滑很久還找不到要改的欄位。
   ⚠️ 手機沒有左欄空間：分頁改成橫向可捲的一列，不是塞成上方一大塊。
   -------------------------------------------------------------------------- */
.panel-layout {
  display: grid;
  grid-template-columns: 16rem 1fr;
  gap: var(--sp-10);
  align-items: start;
}

.panel-nav {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-4);
  border-radius: var(--r-card);
  background-color: var(--bg-alt);
}
.panel-nav__btn {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 3.25rem;          /* 點擊區遠高於 44px */
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-field);
  font-size: var(--fs-xs);
  color: var(--text-main);
  text-align: left;
  transition: background-color var(--dur-base) var(--ease);
}
.panel-nav__btn svg { flex: none; width: 1.25rem; height: 1.25rem; }
.panel-nav__btn:hover { background-color: rgba(74, 40, 39, 0.08); }
/* ⚠️ 目前分頁不可只靠顏色區分——加粗體與左側色條，色弱者也分得出來 */
.panel-nav__btn[aria-selected="true"] {
  background-color: var(--c-maroon);
  color: var(--text-inverse);
  font-weight: var(--fw-bold);
}
.panel-nav__sep { height: 1px; margin: var(--sp-2) 0; background-color: rgba(74, 40, 39, 0.12); }

.panel-body > [role="tabpanel"][hidden] { display: none; }

/* 唯讀欄位：看得到、但不能改
   🔴 用 disabled 的 input 而不是純文字，是為了讓「這是一個欄位、只是不能改」這件事
      對報讀器也成立；純文字會讓使用者以為某處還有可編輯的版本。
   🔴 **R10-21／22（PM 2026-08-24）：`.field--locked` 整條規則已刪除。**
      它原本在每個欄位標籤後面用 `::after` 補一段「（鎖住）」字樣，共出現 10 次
      （家庭樹資料 9 ＋ 手機號碼 1，其中 7 次是 `account.js` 迴圈產生的，
      靜態 grep 只看得到 2 處字面——9.12 第三種形式）。
      **PM 指定移除那個詞**，改由「分頁層級的一句說明」講清楚為什麼不能改
      （08-10 定案明文要求介面必須講明，否則使用者改完以為家庭樹也跟著更新了）。
   ⚠️ **下面這條 `:disabled` 規則要保留**——欄位的灰底與 not-allowed 游標靠它，
      「不能改」這件事在視覺與報讀器上都還在，消失的只是那四個字。 */
.field__control:disabled,
.field__control[readonly] {
  background-color: var(--bg-alt);
  color: var(--text-main);
  cursor: not-allowed;
}

@media (max-width: 1023px) {
  .panel-layout { grid-template-columns: 1fr; gap: var(--sp-6); }
  .panel-nav {
    flex-direction: row;
    overflow-x: auto;
    padding: var(--sp-3);
  }
  .panel-nav__btn { min-width: max-content; }
  .panel-nav__sep { display: none; }
}

/* --------------------------------------------------------------------------
   11.17 滾動同意區塊 .consent（隱私權政策）
   🔴 送出鈕不可預設 disabled：HTML 出來就是可按的，JS 初始化時才加鎖。
      反過來寫的話 JS 一掛掉就沒有人能註冊，而且沒有任何錯誤訊息。
      （同第 8 節「進場動畫預設可見、由 JS 反向啟用」的原則。）
   🔴 只監聽 scroll 會鎖死鍵盤與報讀器使用者——他們不觸發 scroll。
      因此區塊本身可聚焦（tabindex="0"）並支援 End 鍵，見 register.js。
   -------------------------------------------------------------------------- */
/* 🔴 高度用 min(24rem, 45dvh)，不是單純加高（R13-01，2026-08-31）。
   原本 16rem＝256px，扣內距只剩 216px、一次看 8.4 行，而全文 3449px＝要捲 16 個螢幕。
   加到 24rem＝384px（13.4 行，多 60%）。
   🔴 但**不可**寫死 24rem：手機螢幕矮，384px 會佔掉近 60% 畫面、
      把下方「我已閱讀」勾選框推出視野——捲完卻找不到勾選框，比看得少更糟。
      min() 讓矮螢幕自動收斂到 45dvh（375×667 約 300px）。
   ⚠️ 用 dvh 不用 vh：iOS Safari 的 vh 含網址列（同 9.32／R11-05）。
   🔴 fallback 用 @supports 包，不可靠「兩行宣告」——式子裡有不認得的單位時，
      後一行會覆蓋前一行然後掉回初始值 none（＝完全不限高），而且不報錯。 */
.consent__scroll {
  max-height: min(24rem, 45vh);
  overflow-y: auto;
  padding: var(--sp-5);
  border: 1px solid var(--border-field);
  border-radius: var(--r-field);
  background-color: var(--c-white);
  font-size: var(--fs-xs);
  /* 🔴 行高用 --lh-prose（1.9）不是 --lh-body（1.6）：這裡放的是整份政策全文，
     與 privacy 頁的 `.prose`（1.8）同性質，1.6 是給短段落用的，中文長文會擠成一團。 */
  line-height: var(--lh-prose);
}
@supports (height: 100dvh) {
  .consent__scroll { max-height: min(24rem, 45dvh); }
}
.consent__scroll:focus-visible { box-shadow: var(--shadow-focus); }

/* 🔴 段距一律用 `> * + *` 給，**不可只寫 `p`**（2026-09-04 修正）。
   第 8 節的 reset 把 `p / ul / ol` 的 margin 歸零、`h1–h6` 也歸零，
   原本這裡只還原了 `p` 與 `h2` → 政策全文裡的 **h3／ul／table 完全沒有間距**，
   小標黏在上一段、表格與清單直接貼著前一句。
   ⚠️ 不會報錯，畫面上只像是「這幾段排版比較擠」（同 R17-01 的項目符號那次）。
   **判準：長文容器一律用 `> * + *` 統一給段距**，與 `.prose > * + *` 一致。 */
.consent__scroll > * + * { margin-top: var(--sp-4); }

/* 🔴 h2 要比內文大一階並帶分隔線。原本 `font-size: var(--fs-xs)` ＝ 與內文同級，
   十四個章節的標題在視覺上與內文沒有差別，捲動時看不出「換一節了」。
   ⚠️ 而 h3 原本沒有任何規則 → 吃瀏覽器預設的 1.17em ≈ 18.7px，**反而比 h2 大**
      （本主題沒有載入 Tailwind preflight，第 8 節那條註解的前提在這裡不成立）。 */
.consent__scroll h2 {
  margin-top: var(--sp-7);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--border-soft);
  font-size: var(--fs-body);
  color: var(--text-heading);
}
.consent__scroll h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.consent__scroll h3 {
  margin-top: var(--sp-5);
  font-size: var(--fs-xs);
  color: var(--text-heading);
}

/* --------------------------------------------------------------------------
   登入頁的圖形驗證碼（連續失敗 5 次之後才出現）
   ⚠️ 圖片本身就是「換一張」的按鈕：多放一顆文字連結，長輩會先問那是什麼。
   -------------------------------------------------------------------------- */
.login-captcha {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

.login-captcha__img {
  flex: 0 0 auto;
  padding: 0;
  border: 1px solid var(--border-field);
  border-radius: var(--r-field);
  background: var(--c-white);
  cursor: pointer;
  line-height: 0;   /* 去掉 inline 圖片下方的空隙 */
}

.login-captcha__img img {
  display: block;
  height: 3rem;
  border-radius: var(--r-field);
}

/* 輸入框吃掉剩下的寬度 */
.login-captcha .field__control {
  flex: 1 1 auto;
  min-width: 0;
}

.consent__status {
  margin-top: var(--sp-3);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}
.consent__status[data-state="done"] { color: var(--c-maroon); }

/* --------------------------------------------------------------------------
   11.18 載入骨架 .skeleton（S4）
   --------------------------------------------------------------------------
   **為什麼要做**：長輩看到空白會以為網站壞了，不會等。
   **為什麼是骨架不是轉圈圈**：骨架會**先把版面高度撐住**，內容到了不會整頁往下跳。

   三處要用：十房卡片、消息卡、家庭樹畫布（⚠️ **家庭樹最需要**——要算 70 個節點的
   佈局，空白時間最長）。✅ 主視覺不必：第一張的圖與文字已直接寫在 HTML。

   🔴 **用法：骨架是「HTML 先有、資料到了才換掉」，不是「JS 先塞骨架」**——
      後者在 JS 沒跑時是一片空白，那正是骨架要解決的問題。
   ⚠️ 因此骨架的 markup 要寫在頁面裡，渲染函式用 `innerHTML` 覆蓋掉它即可。
   -------------------------------------------------------------------------- */
.skeleton {
  position: relative;
  overflow: hidden;
  background-color: var(--bg-alt);
  /* ⚠️ 骨架本身不可有文字——它是「還沒有內容」的佔位，寫字就變成假內容了 */
  color: transparent;
  user-select: none;
  pointer-events: none;
}
/* 掃光：用 ::after 而不是背景動畫，才不會蓋掉 .skeleton 自己的底色 */
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg,
              transparent, rgba(255, 255, 255, 0.55), transparent);
  animation: skeleton-sweep 1.4s ease-in-out infinite;
}
@keyframes skeleton-sweep { to { transform: translateX(100%); } }

/* ⚠️ 尊重系統設定：關掉動畫但**保留底色**——骨架的主要功能是撐住版面，
   不是那道光。整個隱藏會讓版面塌回去。 */
@media (prefers-reduced-motion: reduce) {
  .skeleton::after { animation: none; }
}

/* 常見形狀。⚠️ 高度要對得上真實內容，否則資料到了版面還是會跳。 */
.skeleton--text { height: 1em; margin-block: 0.25em; }
.skeleton--title { height: 1.4em; margin-block: 0.3em; }
.skeleton--media { width: 100%; aspect-ratio: 3 / 3.4; }
.skeleton--canvas { width: 100%; height: 100%; }

/* --------------------------------------------------------------------------
   11.19 照片格線 .gallery ＋ 燈箱 .lightbox
   --------------------------------------------------------------------------
   🔴 **三處共用同一份，不可各寫一份**（PM 2026-08-14）：
     ① 首頁的活動相簿區 ② 活動相簿頁 ③ 家庭樹頁的家庭相簿頁籤

   ⚠️ 燈箱用原生 `<dialog>`（同 §11.15 的 .modal）——**焦點鎖定與 Esc 由瀏覽器處理**，
      不必自己寫，也不會漏掉。
   -------------------------------------------------------------------------- */
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-5);
}
/* ⚠️ 每一格是 `<li><button>`：鍵盤要 Tab 得到、Enter 要能開——用 `<div>` 兩者都沒有。
   `<li>` 只是列表語意，格線的實際格子是裡面那顆 button，所以要撐滿。 */
.gallery > li { display: contents; }
/* 🔴 **空狀態要例外**：它也是 `<li>`，會被上一條 `display: contents` 吃掉——
   contents 的元素不產生框，於是 `.empty-state` 的 `padding`／`text-align` 全部失效，
   裡面的每個 `<p>` 各自變成格線項目、**被分散到不同格子裡**（實測框是 0×0）。
   ⚠️ 而畫面上不是空白、也不報錯，只是「文字散開在奇怪的位置」，
      不會有人聯想到是上一行那條規則造成的。
   ✅ `grid-column: 1 / -1` 讓它橫跨整個格線；輪播那種 flex 情境下這一條無作用、
      但 `display: block` 仍然有效，兩種容器都成立。 */
.gallery > .empty-state {
  display: block;
  grid-column: 1 / -1;
}
.gallery__item {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  background-color: var(--bg-alt);
  cursor: zoom-in;
}
.gallery__item img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s var(--ease); }
.gallery__item:hover img { transform: scale(1.05); }
.gallery__item:focus-visible { box-shadow: var(--shadow-focus); }

@media (max-width: 899px) { .gallery { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 599px) { .gallery { grid-template-columns: 1fr; } }

/* --- 燈箱 --- */
.lightbox {
  width: 100vw;
  max-width: 100vw;
  height: 100vh;
  max-height: 100vh;
  padding: 0;
  border: 0;
  background-color: rgba(46, 25, 25, 0.94);
  color: var(--text-inverse);
}
.lightbox::backdrop { background-color: rgba(46, 25, 25, 0.94); }

/* 舞台：放大後要能拖曳，所以裁切在這一層、位移在內層 */
.lightbox__stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  touch-action: none;      /* 縮放與拖曳由我們接管，不觸發瀏覽器手勢 */
}
.lightbox__stage.is-panning { cursor: grabbing; }
/* transform 是 runtime 才算得出來的值 → 由 JS 寫 inline，這是本案少數合理的 inline style */
.lightbox__img {
  max-width: 92vw;
  max-height: 82vh;
  object-fit: contain;
  transform-origin: center;
  transition: transform var(--dur-base) var(--ease);
}
/* 🔴 拖曳中的三件事，缺一個就會覺得「卡卡的」：
   ① transition 關掉 —— 不關的話每次寫 transform 都在補間，游標會跟不上手
   ② will-change 讓圖片先升上自己的合成層 —— 放大後的大圖若留在主層，
      每移動一格就要重繪一次整張，這是最明顯的掉幀來源
   ③ user-select 關掉 —— 按住拖曳會順手選取到底下的說明文字，
      游標變成選字模式，看起來像拖曳失效
   ⚠️ will-change 只在拖曳時掛：常駐的話那張圖會一直佔著一層記憶體，
      相簿一次開很多張時反而拖累整頁。 */
.lightbox__stage.is-panning .lightbox__img {
  transition: none;
  will-change: transform;
}
.lightbox__stage.is-panning {
  user-select: none;
}
/* 圖片一律不可被瀏覽器原生拖曳（JS 那邊也擋了一道，兩邊都要） */
.lightbox__img {
  -webkit-user-drag: none;
  user-select: none;
}

/* 🔴 五顆按鈕（關閉／上一張／下一張／放大／縮小）全部 ≥44×44px 且明顯可見。
   ⚠️ **放大縮小按鈕是主要途徑，不是輔助**——長輩多半不會用雙指縮放（PM 指定）。
   ⚠️ 關閉鈕要明顯：長輩找不到關閉鈕是彈窗最常見的卡點。 */
.lightbox__btn {
  position: absolute;
  z-index: 2;
  min-width: 3.25rem;
  min-height: 3.25rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background-color: rgba(254, 250, 245, 0.14);
  color: var(--text-inverse);
  transition: background-color var(--dur-base) var(--ease);
}
.lightbox__btn:hover { background-color: rgba(254, 250, 245, 0.28); }
.lightbox__btn:disabled { opacity: 0.35; cursor: not-allowed; }
.lightbox__btn svg { width: 1.75rem; height: 1.75rem; }

.lightbox__close { top: var(--sp-4); right: var(--sp-4); }
.lightbox__prev  { left: var(--sp-4); top: 50%; transform: translateY(-50%); }
.lightbox__next  { right: var(--sp-4); top: 50%; transform: translateY(-50%); }

/* 縮放控制列：放在底部中央，兩顆並排＋目前倍率 */
.lightbox__zoom {
  position: absolute;
  z-index: 2;
  left: 50%;
  bottom: var(--sp-6);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.lightbox__zoom .lightbox__btn { position: static; }
.lightbox__scale {
  min-width: 4rem;
  text-align: center;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;   /* 倍率變動時寬度不跳 */
}

/* 說明文字（alt／第幾張） */
.lightbox__caption {
  position: absolute;
  z-index: 2;
  left: 50%;
  top: var(--sp-4);
  transform: translateX(-50%);
  max-width: 70vw;
  font-size: var(--fs-xs);
  color: var(--text-inverse-soft);
  text-align: center;
}

/* ⚠️ 手機用全螢幕（本來就是），但按鈕要避開瀏覽器的手勢區，往內縮一點 */
@media (max-width: 767px) {
  .lightbox__prev { left: var(--sp-2); }
  .lightbox__next { right: var(--sp-2); }
  .lightbox__img { max-width: 96vw; max-height: 70vh; }
}

/* --------------------------------------------------------------------------
   11.20 分頁 .pager（D3 最新消息列表用）
   --------------------------------------------------------------------------
   ⚠️ 點擊區 ≥44×44px；目前頁不可只靠顏色區分（色弱者與長輩都分不出來），
      同時用底色 ＋ 字重 ＋ `aria-current`。
   -------------------------------------------------------------------------- */
.pager {
  margin-top: var(--sp-14);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--sp-2);
}
.pager__item {
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding-inline: var(--sp-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1.5px solid var(--border-soft);
  background-color: var(--bg-card);
  color: var(--text-heading);
  font-size: var(--fs-sm);
  transition: background-color var(--dur-base) var(--ease),
              border-color var(--dur-base) var(--ease),
              color var(--dur-base) var(--ease);
}
/* 🔴 R14-04：上下頁改用 Lucide（`chevron-left`／`chevron-right`）之後**要限制 svg 尺寸**。
   Lucide 的預設是 24×24，而數字鈕的內容是 `--fs-sm`（14px）的文字。
   **實測（`news.html` 1280px，四顆鈕）**：
     不寫這一條 → 箭頭鈕 **51×44**、數字鈕 44×44（**寬了 7px**）
     寫了 1.25rem → 箭頭鈕 **47×44**（寬 3px，是 `padding-inline` 撐的，一排看起來才齊）
   ⚠️ **差的是「寬」不是「高」**：`min-height: 2.75rem`（44）壓得住 24px 圖示，
      所以高度兩種寫法都是 44——**只看高度會以為不必修**。
   ⚠️ 1.25rem 是全站圖示在按鈕裡的既有尺寸（`.form-alert svg`／`.panel-nav__btn svg`
      ／`.tree-path__close svg` 都是這個值），不是為這裡新取的數字。
   ✅ `disabled` 的灰不必另外寫：svg 走 `currentColor`，`.pager__item:disabled` 的
      `opacity: 0.35` 會把整顆鈕（含圖示）一起淡掉（實測第一頁的「上一頁」= 0.35 ✅）。 */
.pager__item svg { width: 1.25rem; height: 1.25rem; }
.pager__item:hover:not(:disabled):not([aria-current="page"]) { border-color: var(--c-coral); }
.pager__item:hover:not(:disabled):not([aria-current="page"]) { border-color: var(--c-coral); }
.pager__item[aria-current="page"] {
  background-color: var(--c-maroon);
  border-color: var(--c-maroon);
  color: var(--text-inverse);
  font-weight: var(--fw-bold);
}
.pager__item:disabled { opacity: 0.35; cursor: not-allowed; }
/* 省略號不是按鈕，不給邊框也不可聚焦 */
.pager__gap {
  min-width: 2rem;
  text-align: center;
  color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   11.21 文章卡 .news-card
   --------------------------------------------------------------------------
   🔴 **首頁 ⑦ 的輪播與最新消息列表頁用的是同一種卡**，所以樣式在共用元件層。
      ⚠️ 原本寫在 `index.css`（首頁專屬）——**列表頁沒載那支，卡片會整個沒樣式**。
         2026-08-17 建列表頁時發現並抽出來。
      **判準：只要不只首頁會用到，就屬共用元件層**（同 §11.12 那幾個小元件的教訓）。
   ⚠️ **排列方式各自負責**：首頁是輪播（`index.css` 的 `.carousel__track`），
      列表頁是格線（`news.css` 的 `.news-list`）。這裡只管卡片內部長什麼樣。
   -------------------------------------------------------------------------- */
.news-card__media {
  position: relative;
  aspect-ratio: 3 / 2.2;
  overflow: hidden;
  background-color: var(--bg-alt);
}
/* 圖片本身也要能點進內頁——只有標題可點的話，長輩最直覺的「點圖片」會沒反應。
   ⚠️ 這一層 <a> 對輔助科技隱藏（tabindex="-1" + aria-hidden）：
      同一張卡的標題已經是連結，不隱藏的話讀螢幕的人會聽到同一篇兩次。 */
.news-card__media > a { display: block; width: 100%; height: 100%; }
.news-card__media img { width: 100%; height: 100%; object-fit: cover; }

.news-card__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}
.news-card__date { font-size: var(--fs-xs); color: var(--text-muted); }

.news-card__title {
  margin-top: var(--sp-3);
  font-size: var(--fs-lead);
  line-height: 1.5;
  color: var(--text-heading);
}
.news-card__title a:hover { color: var(--c-berry); }

.news-card__summary {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  line-height: var(--lh-prose);
  color: var(--text-main);
}
.news-card__link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 2.75rem;
  margin-top: var(--sp-4);
  color: var(--c-berry);
  font-size: var(--fs-sm);
  transition: gap var(--dur-base) var(--ease);
}
.news-card__link:hover { gap: var(--sp-3); }
.news-card__link .btn__arrow { width: 1.125em; height: 1.125em; }

/* --------------------------------------------------------------------------
   11.22 視差背景 .bg-parallax（2026-08-17 R4d／R4f）
   --------------------------------------------------------------------------
   PM 要「背景圖用 fixed，視窗滾動時有視差效果」。用在**兩個區塊**：
   關於我們的「創辦人的話」（`about.css` 的 `.letter`）與首頁 CTA（`index.css` 的 `.cta`）。
   🔴 **所以它在共用元件層，不在任何一支頁面 CSS 裡**——兩處同一段邏輯，日後改一次就好。
      （這正是「單一來源」在 CSS 上的落點；照抄兩份的話，日後只會改到其中一份。）

   ⚠️ 這個 class **只管「要不要 fixed」**，背景圖是哪一張由各頁自己指定。

   🔴 **預設是 `scroll`，只有「有滑鼠的精準指標裝置」才開 fixed**——不是全域開了讓瀏覽器自己退：

   | 平台 | `background-attachment: fixed` 的實際行為 |
   |---|---|
   | 桌機 | ✅ 正常 |
   | 🔴 **iOS Safari** | **不固定，而且會把背景按「視窗」尺寸重算** → 圖被放大、構圖跑掉、捲動時還會閃 |
   | Android Chrome | 會固定，但滾動時強制重繪整片，低階手機掉幀 |

   ⚠️ **本案第 1 節寫著「多數用手機、常在戶外行動網路」**——也就是說這個效果
      對多數使用者不會出現，**而且在 iOS 上是「壞掉」不是「沒效果」**。

   🔴 **判斷條件用「指標型別」不用 `min-width`**：螢幕寬度判斷不了「這是不是觸控裝置」——
      **iPad Pro 橫向有 1366px 寬，用 `min-width` 會把它算成桌機，而它正是 iOS Safari。**

   🔴 **`prefers-reduced-motion` 那一條不是加分項**：視差是持續性的動態效果，
      對前庭功能敏感者會不適，**而本案受眾年齡偏高，那個比例比一般族群高**。
   -------------------------------------------------------------------------- */
.bg-parallax {
  background-size: cover;
  background-repeat: no-repeat;
  background-attachment: scroll;   /* 預設，所有觸控裝置與不支援的瀏覽器走這條 */
}

@media (hover: hover) and (pointer: fine) {
  .bg-parallax { background-attachment: fixed; }
}

@media (prefers-reduced-motion: reduce) {
  .bg-parallax { background-attachment: scroll; }
}

/* 🔴 **被否決的替代方案：用一個 `position: fixed` 的子元素當背景**（2026-08-18 查證）。
   那個做法需要父層有 `overflow: hidden`／`clip-path` 才能把 fixed 子元素裁在區塊內，
   **而本案兩個要用的區塊都不能那樣設**：
   `.cta` 是 `overflow: visible`（讓 `.cta__decor` 筆刷出血）、
   `.letter` 的兩支裝飾也都刻意跨出區塊邊界。
   → `background-attachment` 是這裡唯一乾淨的做法，**不要為了視差去改 overflow**。
   ⚠️ 也不要為此把 GSAP／ScrollTrigger 加回來（2.1 已移除）——純 CSS 做得到。 */

/* --------------------------------------------------------------------------
   11.23 區塊收尾的按鈕列 .section-foot
   --------------------------------------------------------------------------
   🔴 **原本只寫在 `index.css`**（首頁專屬），但 `album.html` 也用了它、而那一頁沒載 index.css
      → 「回到所有相簿」那顆按鈕**完全沒有上距、緊貼照片牆**（R7-21 就是這個症狀）。
      **判準：只要不只首頁會用到，就屬共用元件層**（同 §11.21 `.news-card` 那次的教訓，第三次踩到）。
   ⚠️ 間距**由這個容器提供**，不要給裡面的按鈕自己加 margin（第 7 節第 2 條）。
   -------------------------------------------------------------------------- */
.section-foot {
  margin-top: var(--sp-14);
  text-align: center;
}

/* --------------------------------------------------------------------------
   11.11 只給螢幕報讀器的文字
   -------------------------------------------------------------------------- */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   11.24 換頁載入指示 .page-loader（2026-09-01）
   --------------------------------------------------------------------------
   本站每次點連結都是一次完整的頁面請求，網路慢時「按下去到新頁畫出來」之間
   畫面完全沒反應。長輩的反應是「是不是沒按到」→ 連按好幾次。這一塊補那個空窗。

   🔴 **顯示要延遲 180ms**（延遲寫在 script.js，不在這裡）：
      本機或有快取時 100ms 內就換好了，立刻顯示只會看到一道灰影閃過，
      比沒有更煩。這個延遲是這個元件好不好用的關鍵，不是可有可無的優化。

   🔴 遮罩**刻意擋住點擊**：導覽已經開始，這時再點別的連結只會讓瀏覽器
      丟掉前一個請求重來，對使用者來說就是「越點越慢」。

   ⚠️ `z-index` 取 2000：要蓋過導覽列（100）與浮動工具（90）。
      SweetAlert2 的彈窗是 1060，也在它之下 —— 這是對的：
      換頁一旦開始，那個彈窗就要跟著整頁被換掉了。
   ⚠️ 背景用奶油白半透明而不是黑色遮罩：整站是淺底，壓黑一層會像出錯。
   -------------------------------------------------------------------------- */
.page-loader {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: grid;
  place-items: center;
  background-color: rgba(254, 250, 245, 0.72);   /* --c-cream 的半透明 */
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease);
}
/* 兩段式：先 `hidden = false`（進到版面），下一幀才加 `.is-on`（淡入）。
   直接顯示會沒有起始狀態可以過渡，遮罩會用跳的（同 initMenu() 的作法）。 */
.page-loader.is-on { opacity: 1; }

/* 頂部細條．**不代表真實進度**（伺服器端渲染拿不到進度），
   它要傳達的只有一件事：系統還活著。所以是來回跑的不定量條。 */
.page-loader__bar {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 0.25rem;
  overflow: hidden;
  background-color: rgba(74, 40, 39, 0.12);
}
.page-loader__bar::before {
  content: "";
  position: absolute;
  inset-block: 0;
  width: 40%;
  background-color: var(--c-maroon);
  animation: page-loader-sweep 1.1s ease-in-out infinite;
}
@keyframes page-loader-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(350%); }
}

/* 文字方塊．🔴 一定要有「載入中…」四個字，不可以只有一個轉圈圈——
   單獨的圈圈對長輩不見得讀得出是「在忙」，也可能被當成壞掉的圖。 */
.page-loader__box {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-6);
  font-size: var(--fs-body);
  color: var(--text-heading);
  background-color: var(--c-white);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
}

.page-loader__spinner {
  width: 1.25rem;
  height: 1.25rem;
  border: 0.1875rem solid rgba(74, 40, 39, 0.2);
  border-top-color: var(--c-maroon);
  border-radius: var(--r-round);
  animation: page-loader-spin 0.8s linear infinite;
}
@keyframes page-loader-spin { to { transform: rotate(360deg); } }

/* 🔴 減少動態效果：**把動畫關掉，但整塊要留著**。
   這裡不是裝飾，是「系統正在忙」的唯一訊息——連它一起藏掉的話，
   最需要明確回饋的那群使用者反而什麼都看不到。
   ⚠️ 轉圈圈停下來會變成一個缺一角的圈，看起來像壞掉 → 改成完整的一圈。 */
@media (prefers-reduced-motion: reduce) {
  .page-loader { transition: none; }
  .page-loader__bar::before { animation: none; width: 100%; }
  .page-loader__spinner {
    animation: none;
    border-color: var(--c-maroon);
  }
}

/* ==========================================================================
   12. 導覽列 .site-header（Figma 12:70）
   --------------------------------------------------------------------------
   ⚠️ 導覽列兩種登入狀態完全一樣（2026-08-06 定案）：未登入也看得到「家庭天地」
      「活動相簿」，點下去彈提示視窗（SweetAlert2），不換頁。
      解決了兩個代價：①登入前後選單一樣，長輩不會以為走錯地方
                     ②選單本身就讓人知道站上有家庭樹與相簿
   ========================================================================== */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background-color: var(--c-white);
  box-shadow: var(--shadow-sm);   /* demo 的 header 有一道很淡的暖色陰影 */
  min-height: var(--header-h);
}
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-6);
  min-height: var(--header-h);
}

/* 站名：純文字，無 logo（客戶沒有 CI）
   桌機「台南鹽埕林姓宗親會」／手機「鹽埕林姓宗親會」——兩段各自控制顯示 */
/* 🔴 2026-08-14 R2m①：兩行之間加間距（PM：現在貼在一起）。
   ⚠️ 用 flex gap 而不是加大 line-height——line-height 會同時撐開兩行「各自」的
      行高，站名只有兩行、每行都是單行，撐開的是上下留白，看起來像整塊往下掉。
      gap 只加在兩行「之間」，正是要調的那一段。
   ⚠️ header 與 footer 共用這個 class，所以兩處一起變（PM 兩處都要改）。 */
.site-brand {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  color: var(--text-brand);
  line-height: 1.1;
}
.site-brand__prefix {
  font-size: var(--fs-body);
  font-weight: var(--fw-normal);
  font-family: var(--font-body);
}
.site-brand__name {
  font-size: var(--fs-brand);
  font-weight: var(--fw-bold);   /* 原本由已拆除的 .font-display 提供，不補站名會變細 */
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-10);          /* demo 的 gap-10 ＝ 40px */
  font-size: var(--fs-body);
}
.site-nav__link {
  position: relative;
  padding-block: var(--sp-3);
  /* demo 的導覽是咖啡棕、hover 轉粉。我方 hover 用莓紅（demo 用 #fc938e，2.08:1 讀不到）。 */
  color: var(--text-heading);
  font-weight: var(--fw-normal);
  transition: color var(--dur-base) var(--ease);
}
.site-nav__link:hover { color: var(--c-berry); }

/* 目前頁面：底線標示。⚠️ 不可只用顏色區分——色弱者與長輩都分不出來 */
.site-nav__link[aria-current="page"] {
  color: var(--c-maroon);
  font-weight: var(--fw-bold);
}

/* 🔴 R6j①（2026-08-18 PM）：底線 3px → **2px**，且 **hover 時也要有底線**。
   ⚠️ **兩個狀態仍然必須分得出來，這不是美感問題**——上面那行註解就是理由：
      不可只用顏色區分。active 用底線正是為了這個，若 hover 給一模一樣的底線，
      「我現在在哪一頁」這個資訊就被稀釋了。
   ✅ **三重區分保留**：
      active ＝ 咖啡棕字 ＋ **粗體** ＋ **珊瑚粉**底線
      hover  ＝ 莓紅字 ＋ **莓紅**底線（**不加粗**）
      → **字重是最可靠的分辨點**（顏色與粗細只差一階時，長輩多半只看得出粗細）。
   ⚠️ 底線用 `opacity` 淡入，**不要用 `width` 從 0 長出來**——那會讓底線在文字下方
      「掃」過去，長輩的視線會被拉走。150ms，符合第 3 節（≤400ms、不串接長 delay）。
   ⚠️ `::after` 同時給兩個狀態用，所以基底寫在 `.site-nav__link` 上、預設透明。 */
.site-nav__link::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 0.125rem;                /* 2px */
  border-radius: var(--r-field);
  background-color: currentColor;  /* 跟著文字色：hover 莓紅、active 由下方覆寫成珊瑚粉 */
  opacity: 0;
  transition: opacity 150ms var(--ease);
}
.site-nav__link:hover::after { opacity: 1; }

.site-nav__link[aria-current="page"]::after {
  background-color: var(--c-coral);
  opacity: 1;
}

/* ⚠️ 「人在關於我們頁、滑鼠又指著關於我們」時兩個狀態會疊加——
   底線維持珊瑚粉（active 的顏色贏），字仍是粗體，所以不會退化成 hover 的樣子。 */
.site-nav__link[aria-current="page"]:hover::after { background-color: var(--c-coral); }

/* --------------------------------------------------------------------------
   子選單 .site-subnav（R6j②，2026-08-18）
   --------------------------------------------------------------------------
   🔴 **展開完全由 CSS 負責（`:hover` ＋ `:focus-within`），JS 只做內容覆寫與 Esc。**
      理由是第 3 節第 3 條的同一條判準：**JS 沒跑會怎樣？** 答案必須是「子選單照樣能用」。
   🔴 **`:focus-within` 就是鍵盤支援**：Tab 進主項 → 子選單展開 → 繼續 Tab 走進每一條，
      焦點離開就收合。不必自己寫方向鍵，也不會與瀏覽器原生的 Tab 行為打架。
   ⚠️ **點主項的行為不變**（PM 明示）：它仍是一個普通連結，點了就進列表頁。

   ⚠️ **滑鼠從主項移到子選單的路上不可以有間隙**——中間斷一下選單就收合，
      而長輩的手比較不穩。所以間距用 `.site-subnav` 自己的 `padding-top` 撐，
      **不要用 `margin-top`**（margin 是空的，游標一進去就算離開了）。
   -------------------------------------------------------------------------- */
.site-nav__item { position: relative; }

/* 🔴 R10-18（PM 2026-08-24）：桌機的下拉主項要有箭頭圖示——原本完全沒有視覺提示，
   使用者不知道「家庭天地」「最新消息」底下還有東西。

   ⚠️ **交辦的規格寫「手機版已經有，用同一個圖示（Lucide `chevron-down`）」，
      那句話的後半是錯的**——實測手機的 `.site-menu__caret::after` 是
      **用 border 畫的 8×8 CSS 箭頭，裡面一個 svg 都沒有**。
      照那句去引 Lucide 會變成同一個視覺元素有兩套來源，而且
      **Lucide 找不到名字時不報錯、就是什麼都不畫**（9.16 踩過）。
      → 這裡沿用手機那一套 border 畫法，角度與 transform 完全相同。

   ✅ **零 HTML 改動**：用 `::before` ＋ `order: 1` 把箭頭排到文字後面，
      不必在 15 頁 × 2 個連結裡各插一個 `<span>`。
      **選擇器用 `.site-nav__item >`（結構）而不是 `[aria-haspopup]`（屬性）**——
      `.site-nav__item` 只存在於桌機導覽列（實測 15 頁各 2 個、手機選單 0 個），
      而屬性哪天被加到手機那邊，箭頭就會多長出來一個。
   ⚠️ `::after` 已經被底線用掉了（R6j①），所以箭頭只能用 `::before`。
      兩者互不影響：底線是絕對定位，箭頭是 flex 排版裡的一個項目。
   ⚠️ 底線會連箭頭一起畫過去——那是刻意的，箭頭屬於這個連結的一部分。 */
.site-nav__item > .site-nav__link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}
.site-nav__item > .site-nav__link::before {
  content: "";
  order: 1;                     /* 排到文字後面 */
  width: 0.5rem; height: 0.5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  /* 與 .site-menu__caret::after 同一組數值，改一邊要回頭改另一邊 */
  transform: rotate(45deg) translateY(-0.125rem);
  transition: transform var(--dur-fast) var(--ease);
}
/* 展開時箭頭轉向（與手機一致）。
   ⚠️ 條件要與子選單的展開條件**完全相同**（`:hover` ＋ `:focus-within`），
      否則會出現「選單開了但箭頭還朝下」或反過來，而且不會報錯。 */
.site-nav__item:hover > .site-nav__link::before,
.site-nav__item:focus-within > .site-nav__link::before {
  transform: rotate(-135deg) translateY(-0.125rem);
}
@media (prefers-reduced-motion: reduce) {
  .site-nav__item > .site-nav__link::before { transition: none; }
}

.site-subnav {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  min-width: 11rem;
  padding: var(--sp-3) 0;
  border-radius: var(--r-card);
  background-color: var(--c-white);
  box-shadow: var(--shadow-md);
  /* 收合時：先淡出，再延遲把 visibility 關掉 → **等於 250ms 的容錯時間**，
     游標中途離開一下不會馬上關。展開時把兩個 delay 都歸零，所以開起來是即時的。 */
  opacity: 0;
  visibility: hidden;
  transition: opacity 150ms var(--ease) 250ms, visibility 0s linear 400ms;
}
.site-nav__item:hover > .site-subnav,
.site-nav__item:focus-within > .site-subnav {
  opacity: 1;
  visibility: visible;
  transition: opacity 150ms var(--ease) 0s, visibility 0s linear 0s;
}

.site-subnav a {
  display: block;
  padding: var(--sp-3) var(--sp-5);
  font-size: var(--fs-sm);
  color: var(--text-heading);
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.site-subnav a:hover { background-color: var(--bg-alt); color: var(--c-berry); }
.site-subnav a:focus-visible { box-shadow: var(--shadow-focus); }

/* ⚠️ `prefers-reduced-motion` 下不要淡入淡出，但**容錯延遲要留著**——
   那是給手不穩的人用的，不是動畫效果。 */
@media (prefers-reduced-motion: reduce) {
  .site-subnav { transition: visibility 0s linear 250ms; }
  .site-nav__item:hover > .site-subnav,
  .site-nav__item:focus-within > .site-subnav { transition: visibility 0s linear 0s; }
}

/* 手機（漢堡選單裡）：原生 <details>，零 JS 就能展開 */
/* 🔴 R7-11：兩層不是三層。summary 這一列裡有**兩個獨立目標**：
   　 左邊的 `<a>` ＝ 進列表頁；右邊的 `.site-menu__caret` ＝ 展開子項。
   ⚠️ summary 自己就是 `<details>` 的開關，所以「箭頭以外的空白處」也會展開——
      那是額外的寬容，不是 bug。真正要保證的是**兩個目標各自 ≥44×44**。
   ⚠️ summary 不設 padding：左右兩個子元素各自撐出自己的點擊區，
      在 summary 上再加 padding 會讓「看起來屬於連結、實際上是展開」的死角變大。 */
.site-menu__sub > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  cursor: pointer;
  list-style: none;             /* 拿掉預設三角形，改用 caret */
}
.site-menu__sub > summary::-webkit-details-marker { display: none; }
.site-menu__sub > summary > .site-nav__link {
  flex: 1;                      /* 文字那一半整條都是連結 */
}
.site-menu__caret {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;               /* 🔴 箭頭的點擊區是 44×44，不是圖示大小 */
  height: 2.75rem;
  flex: none;
  color: var(--text-muted);
}
.site-menu__caret::after {
  content: "";
  width: 0.5rem; height: 0.5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translateY(-0.125rem);
  transition: transform var(--dur-fast) var(--ease);
}
.site-menu__sub[open] > summary .site-menu__caret::after {
  transform: rotate(-135deg) translateY(-0.125rem);
}
@media (prefers-reduced-motion: reduce) {
  .site-menu__caret::after { transition: none; }
}
/* 🔴 R8-15（PM 2026-08-19）：**縮排只給子項，主項不縮排。**
   ⚠️ 原本的選擇器是 `.site-menu__sub a`，而 R7-11 把主項的 `<a>` 搬進了 `<summary>` 裡
      → 那條規則**連主項一起縮排了**，於是「家庭天地／最新消息」看起來比
      「首頁／關於我們／活動相簿」低一階，但它們其實是同層級。
   🔴 **加 `ul` 一個字就是全部的修法**——但要先看懂為什麼會這樣，
      否則很容易改成「在主項上補一條 padding-left: 0」，那是用第二條規則去抵銷第一條。 */
.site-menu__sub ul a {
  display: block;
  padding: var(--sp-3) 0;
  /* ⚠️ 這一行**已經被下方 R10-20 那組覆寫成 0**（同選擇器、同權重、後者勝）。
     留著是為了保住上面那段「為什麼只有子項縮排」的來龍去脈；
     🔴 **層級現在由底色表達（第一層白、第二層米色），不再靠縮排**——
        因為 R10-20 要求第二層文字置中，而置中與縮排在視覺上互相抵消。
     ⚠️ 要改子項的左內距請改下面那一處，改這裡不會有任何效果，而且不會報錯。 */
  padding-left: var(--sp-8);
  font-size: var(--fs-sm);
  color: var(--text-heading);
}

/* 漢堡按鈕（Figma 未畫，必做） */
.site-header__toggle {
  display: none;
  width: 2.75rem;            /* 44×44 下限 */
  height: 2.75rem;
  align-items: center;
  justify-content: center;
  color: var(--text-brand);
}

/* 漢堡 → X 的變形。
   ⚠️ 三條 bar 用絕對定位而不是 flex 排列：要做旋轉合併成 X，
      上下兩條必須能移動到「同一個位置」再各轉 45°，
      而 flex 會一直把它們推開，位移只做得到一半。
   ⚠️ 外框給固定尺寸（20×14）：bar 是絕對定位、不撐開父層，
      不給尺寸的話按鈕裡會是空的。 */
.site-header__burger {
  position: relative;
  display: block;
  width: 20px;
  height: 14px;
}

.site-header__burger > span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform var(--dur-base) var(--ease), opacity var(--dur-fast) var(--ease);
}

.site-header__burger > span:nth-child(1) { top: 0; }
/* 中間那條用 translateY 置中而不是 top: 50%——後面要疊加 scaleX 動畫，
   兩個 transform 寫在同一個屬性裡才不會互相覆蓋 */
.site-header__burger > span:nth-child(2) { top: 50%; transform: translateY(-50%); }
.site-header__burger > span:nth-child(3) { bottom: 0; }

/* 展開時：上下兩條移到中線再交叉，中間那條收掉 */
.site-header__toggle[aria-expanded="true"] .site-header__burger > span:nth-child(1) {
  transform: translateY(6px) rotate(45deg);
}
.site-header__toggle[aria-expanded="true"] .site-header__burger > span:nth-child(2) {
  /* 直接 opacity: 0 會留下一條看不見卻仍在動的線；scaleX(0) 讓它從中間收起來 */
  transform: translateY(-50%) scaleX(0);
  opacity: 0;
}
.site-header__toggle[aria-expanded="true"] .site-header__burger > span:nth-child(3) {
  transform: translateY(-6px) rotate(-45deg);
}

@media (prefers-reduced-motion: reduce) {
  .site-header__burger > span { transition: none; }
}

/* 手機選單：預設收合。
   🔴 用 [hidden] 而非 JS 加 class 控制顯示，JS 沒跑時它就是關著的正常狀態。
   🔴 R7-13 開合動畫：**不是拿 display 做動畫**（那沒有中間狀態）。
      `[hidden]` 仍然留著給無 JS 的情況，JS 分兩步走：
        開 → 先移除 hidden，下一幀才加 `.is-open`（才有起始狀態可以過渡）
        關 → 先移除 `.is-open`，等過渡結束才補回 hidden
   ⚠️ 用 `grid-template-rows: 0fr → 1fr` 而不是 max-height：
      max-height 要猜一個夠大的值，猜太大會讓過渡的前半段看起來沒動靜。 */
.site-menu[hidden] { display: none; }
/* ⚠️ 底色／邊線／內距一律掛在 __inner，不可留在 .site-menu 上——
   外殼收到 0fr 時高度是 0，但 padding 與 border 不受 grid 列高影響，
   會留下一條「關著卻還看得到」的白邊，而且只在收合的那一瞬間出現。 */
.site-menu__inner {
  padding: var(--sp-4) var(--container-pad) var(--sp-6);
  border-top: 1px solid rgba(74, 40, 39, 0.12);
  background-color: var(--c-white);
}
.site-menu .site-nav__link {
  padding-block: var(--sp-4);   /* 手機每一項 ≥44px 點擊區 */
  font-size: var(--fs-body);
  /* 🔴 R8-15：漢堡選單裡所有項目的文字左右置中。 */
  text-align: center;
}

/* 🔴 R10-20（PM 2026-08-24）：漢堡選單的 hover／active 改成**珊瑚粉底 ＋ 咖啡棕字**
   （與 hero 按鈕同一組），第二層另外給頁面米色底、文字也置中。

   ✅ **對比度**：咖啡棕 `#4A2827` on 珊瑚粉 `#FFAEA1` ＝ **7.28:1**（3.1 已量過）✅
   ⚠️ 這是「珊瑚粉不得承載文字」那條鐵則的**正確用法**：粉在**底下**、字在上面。
      反過來（粉色的字）只有 2.08:1，長輩讀不到。

   🔴 **底線在手機要移除、桌機要保留**（PM 明示）。做法是把 `::after` 的 `content`
      設成 `none`，**不是 `display: none` 也不是 `opacity: 0`**——
      前兩者留著一個畫不出來的偽元素，日後有人改 `.site-nav__link::after`
      會以為手機這裡也跟著變，`content: none` 才是真的不存在。
   ⚠️ **移除底線之後，「我現在在哪一頁」不可以只剩顏色**——那條規則（不可只用顏色區分）
      沒有放寬。手機的 active 現在是**粉底 ＋ 咖啡棕字 ＋ 粗體**，字重仍在，
      而 hover 是粉底但**不加粗** → 兩個狀態仍分得出來。 */
.site-menu .site-nav__link::after { content: none; }

/* 🔴 R11-04（PM 2026-08-25）：**帶子選單那兩項的粉底要與其他項同寬。**
   **PM 指出**：家庭天地／最新消息的粉色背景比其他選項窄（右邊空一塊給箭頭）。
   **實測 375px**（`.site-menu__inner` 內容寬 320）：
     一般項目（首頁／關於我們／活動相簿）**320**；
     帶子選單那一項的 `<a>` 只有 **232**（左邊 44 的配重 `::before` ＋ 右邊 44 的箭頭）；
     第二層 `ul` 的米色底 **320**。→ 三種寬度，其中一種是粉底。

   🔴 **根因是兩條既有定案疊加，不是誰寫錯**：
     ① **R7-11**：文字與箭頭是**兩個獨立的觸控目標**，各自 ≥44×44 → 它們是兩個並排的元素
     ② **R10-20**：hover／active 改成粉底 → 背景只掛在「文字」那一個元素上

   ✅ **修法：把背景改掛到 `<summary>`**——它**本來就是**那個「文字 ＋ 箭頭」的 flex 容器，
      不必再包一層 div。`<summary>` 的寬度就是 320，與其他項目自動一致。
   🔴 **R7-11 完全沒有被破壞**：`<a>` 與 `.site-menu__caret` 仍是兩個獨立的點擊目標
      （點文字進列表頁、點箭頭展開），**只是視覺上共用一塊背景**。 */

/* 第一層的一般項目（**不含 summary 裡的那個 `<a>`**）與第二層子項。
   ⚠️ 選擇器從 `.site-menu .site-nav__link` 收緊成 `.site-menu__inner > .site-nav__link`——
      不收緊的話它會連 summary 裡的 `<a>` 一起命中，粉底又變回 232 寬，
      **而且兩塊粉底會疊在一起，畫面上只看得出「圓角怪怪的」**。
   ⚠️ 特異性沒有變（兩者都是 0,3,0），所以 `.site-menu__cta.btn` 的
      hover 行為與改動前完全相同。 */
.site-menu__inner > .site-nav__link:hover,
.site-menu__inner > .site-nav__link[aria-current="page"],
.site-menu__sub ul a:hover,
.site-menu__sub ul a[aria-current="page"] {
  background-color: var(--c-coral);
  color: var(--text-on-warm);   /* ＝咖啡棕，暖底上的文字色統一走這個（6.1） */
  border-radius: var(--r-field);
}

/* 帶子選單那一列：背景掛在整條 summary 上。
   🔴 **`:hover` 與 `:has()` 一定要分成兩條規則寫，不可併成選擇器清單**——
      不支援 `:has()` 的瀏覽器會把**整條規則**丟掉，連 hover 那半也一起消失
      （同 `family-tree.css` 裡 `::details-content` 那條的理由）。
   ⚠️ `:has()` 不支援時退回「目前所在頁沒有粉底」，仍有粗體可辨（見下方 active 那條），
      不會變成看不出來。 */
.site-menu__sub > summary:hover {
  background-color: var(--c-coral);
  border-radius: var(--r-field);
}
.site-menu__sub > summary:has(> .site-nav__link[aria-current="page"]) {
  background-color: var(--c-coral);
  border-radius: var(--r-field);
}
/* 🔴 文字色要另外指定給 `<a>`：`.site-nav__link` 自己宣告了 `color`，
   **不會繼承 summary 的顏色**——少了這兩條就是「粉底配原本的字色」，
   而那個對比只有 2 點多，長輩讀不到，畫面上卻只像「顏色有點淡」。 */
.site-menu__sub > summary:hover > .site-nav__link,
.site-menu__sub > summary > .site-nav__link[aria-current="page"] { color: var(--text-on-warm); }
/* 箭頭在粉底上也要換色：`--text-muted` 壓在珊瑚粉上只有 2.4:1。 */
.site-menu__sub > summary:hover .site-menu__caret,
.site-menu__sub > summary:has(> .site-nav__link[aria-current="page"]) .site-menu__caret {
  color: var(--text-on-warm);
}

/* 🔴 **箭頭區單獨再深一階**——PM 一併點出的疑慮：「滑到箭頭上時整列都變粉，
   會不會讓人以為點哪裡都一樣。」整列同色確實會這樣讀，而這一列**真的有兩個目標**。
   → 滑到箭頭上時，只有那 44×44 再壓一層咖啡棕的半透明，
     **兩塊顏色深淺不同 ＝ 看得出這是兩個可以分別點的區域**。
   ⚠️ 用半透明疊加而不是另調一個色值：粉底本身在 hover／active 兩種狀態下都是同一色，
      疊加的結果自動跟著走，不會有第二個要維護的色票。
   ⚠️ 只加在 `:hover` 上，不加在 active——active 是「你在這一頁」的狀態標示，
      不是可按的提示。 */
.site-menu__caret:hover { background-color: rgba(74, 40, 39, 0.12); border-radius: var(--r-field); }

/* active 仍然加粗——見上方「不可只用顏色區分」。 */
.site-menu__inner > .site-nav__link[aria-current="page"],
.site-menu__sub > summary > .site-nav__link[aria-current="page"],
.site-menu__sub ul a[aria-current="page"] { font-weight: var(--fw-bold); }

/* 🔴 第二層：頁面米色底，與第一層（白）分開；文字一併置中。
   ⚠️ **置中就不能再靠縮排表達層級**——R8-15 當初用 `padding-left: --sp-8` 讓子項
      看得出低一階，那與置中直接衝突（置中之後縮排只會讓文字偏右，看起來像沒對齊）。
      **層級改由底色承擔**：第一層白、第二層米色，是整片的差異，比縮排更明顯，
      對長輩也更好認。→ 所以下面要把那條 `padding-left` 歸零。
   ⚠️ 底色掛在 `ul` 上而不是每個 `a` 上：掛在 `a` 上時項目之間的 `gap` 會露出白縫，
      看起來像沒鋪滿。 */
.site-menu__sub ul {
  background-color: var(--bg-alt);
  border-radius: var(--r-field);
}
.site-menu__sub ul a {
  padding-left: 0;              /* R8-15 的縮排由底色取代，見上 */
  padding-inline: var(--sp-3);
  text-align: center;
}
/* 🔴 帶圖示的那兩項（登入／註冊、我的帳號）是 flex，`text-align` 對它們無效——
   **要改 `justify-content`**。少改這一條的話那兩項會維持靠左，
   而畫面上只看得出「有兩項沒對齊」，不會有人聯想到是 flex 與 text-align 的差別。 */
.site-menu .site-nav__link--icon,
.site-menu__cta.btn { justify-content: center; }

/* 🔴 summary 那一列要「連箭頭一起算」才是真的置中：
   右邊有一個 44px 的箭頭，只把中間的連結置中，文字會偏左 22px。
   → 左邊補一個等寬的空位把它推回來。
   ⚠️ 這個 `::before` 純粹是配重，不可拿來放任何東西（它沒有點擊行為）。 */
.site-menu__sub > summary::before {
  content: "";
  width: 2.75rem;
  flex: none;
}

/* R7-13：起始狀態（收合）與過渡 */
.site-menu {
  display: grid;
  grid-template-rows: 0fr;
  /* 🔴 `overflow: hidden` 不可省。0fr 讓「列高」變 0，但 __inner 的 padding 與
     border **不受列高影響**（padding 不是內容高度）——實測收合時仍留下 41px
     的白色橫條，而它看起來就像「選單關不乾淨」，不像 CSS 寫錯。 */
  overflow: hidden;
  opacity: 0;
  transition: grid-template-rows var(--dur-base) var(--ease),
              opacity var(--dur-fast) var(--ease);
}
.site-menu.is-open { grid-template-rows: 1fr; opacity: 1; }
.site-menu > .site-menu__inner {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-height: 0;                /* 0fr 才壓得下去 */
  overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
  .site-menu { transition: none; }
}

/* 🔴 R7-12：手機選單裡的「登入／註冊」與「我的帳號」＝咖啡棕底白字，與桌機那顆一致。
   ⚠️ 2026-09-02 由 `.site-menu__account` 改名為 `.site-menu__cta`：這一組樣式
      本來就是給「帶圖示的那兩項」用的（見上方 `justify-content` 那條的註解），
      名字綁著 account 會讓人以為只有登入後那顆能用。
   ⚠️ 它同時掛著 `.site-nav__link`（要那條 ≥44px 的 padding）與 `.btn`，
      而 `.btn` 的 `justify-content: center` 在手機斷點被改成滿版置中——
      這裡要**靠左**，跟其他選單項對齊，否則只有這一項的文字跑到中間。 */
.site-menu__cta.btn {
  /* 🔴 color 要在這裡再寫一次：`.site-menu .site-nav__link` 是兩個 class（0,2,0），
     壓得過 `.btn--primary` 的一個 class（0,1,0）→ 不補的話會變成
     **咖啡棕底配咖啡棕字**（對比 1:1，等於整顆看不到字），而且不會報錯。 */
  color: var(--text-inverse);
  /* R8-15：改為置中（原本靠左，與其他項對齊）。**圖示與文字一起置中**，
     所以是 `justify-content` 而不是 `text-align`——上面那條已經設了，這裡不重複。 */
  width: 100%;
  padding-inline: var(--sp-5);
  font-size: var(--fs-body);
}

/* ==========================================================================
   13. 頁尾 .site-footer（2026-08-13 R1b 改用 demo 的四欄咖啡棕版）
   --------------------------------------------------------------------------
   ✅ 這順帶結掉了 CLAUDE.md 6.2 那個 🟡「三欄＋聯絡我們尚未實作，待 PM 裁決」——
      demo 的頁尾就是四欄（品牌／聯絡我們／快速連結／社群），既然視覺來源改成 demo，
      它就是規格，不必再裁決。
   ⚠️ 頁尾快速連結**全列**（含家庭天地、活動相簿），與導覽列同步——
      2026-08-06 定案：兩種登入狀態的選單完全一樣，未登入點下去彈提示視窗。
   ========================================================================== */
.site-footer {
  background-color: var(--bg-footer);   /* 咖啡棕 */
  padding-block: var(--sp-16) 0;
  color: var(--text-inverse);           /* 奶油白 on 咖啡棕 12.44:1 ✅ */
}

/* 四欄：站名較寬，其餘三欄等寬 */
.site-footer__grid {
  display: grid;
  grid-template-columns: 1.5fr 1.1fr 1fr 1.2fr;
  gap: var(--sp-10);
  padding-bottom: var(--sp-16);
}

/* --- ① 站名欄 --- */
/* 站名在頁尾一律兩行（桌機、手機都是）。
   ✅ 2026-08-18 R7-16 起頁首也是全名兩行，兩處終於一致——
      原本這裡註明「不套手機縮寫規則」，那條規則與 `.site-brand__short` 已一併刪除。 */
.site-footer__logo { color: var(--c-white); }
.site-footer__brand p {
  margin-top: var(--sp-5);
  max-width: 22rem;
  font-size: var(--fs-xs);
  line-height: 1.7;
  color: var(--text-inverse-soft);      /* 9.31:1 ✅ */
}

/* ⚠️ ~~.site-footer__social~~ 2026-08-14 R2k 移除：**客戶沒有經營社群**，
   PM 定案不放社群圖示。移除後正好符合他的四欄排版示意圖。
   🔴 但 **Simple Icons 這個來源不可跟著廢止**——第 7 節第 3 條的例外與 9.16 那個坑
      都要留著：E1b（LINE 綁定與登入）還沒做，屆時需要 LINE 的官方圖示，
      而 Lucide 早就沒有品牌圖示了（寫了會靜默渲染成空白）。 */

/* --- ②③④ 三個連結欄 --- */
.site-footer__col h2 {
  font-size: var(--fs-body);
  color: var(--c-white);
}
/* 🔴 R2m②：四欄的「標題 → 第一個連結」間距一律 --sp-5，不可只改網站導覽那一欄。
   ⚠️ 網站導覽那一欄多包了一層 .site-footer__cols2，原本在它身上把 margin-top 歸零，
      結果只有那一欄貼在標題下面。改成由 cols2 自己承接間距（見下方）。 */
.site-footer__col ul {
  margin-top: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
}
.site-footer__col li {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  /* 🔴 demo 這裡寫 `color: rba(255,255,255,.85)`——拼字錯，整條規則靜默失效。
     不要照抄；而且我方一律走 token，不寫 rgba 字面。 */
  font-size: var(--fs-xs);
  color: var(--text-inverse-soft);
}
.site-footer__col li svg,
.site-footer__col li i { flex: none; width: 1.125rem; height: 1.125rem; margin-top: 0.15em; }
.site-footer__col a { color: var(--text-inverse-soft); }
.site-footer__col a:hover { color: var(--c-white); text-decoration: underline; }

/* ② 網站導覽的兩個子欄並排（正好是導覽列六項） */
.site-footer__cols2 {
  margin-top: var(--sp-5);   /* 與其他三欄的 ul 同一個值，四欄才對得齊 */
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-4);
}
.site-footer__cols2 ul { margin-top: 0; }   /* 間距由上層的 cols2 提供，子欄不自帶 */

/* 🔴 2026-08-14 R2m③：電話與同欄的地址、Email **同級**（PM：現在特別大）。
   原本吃了三重強調：--fs-lead（平移後 22px）＋ --fw-bold ＋ 純白，三個都拿掉。
   🔴 **原註解寫「（既有定案）」是錯的——PM 2026-08-14 明確表示他沒說過這條。**
      那是我方自己加的約束，卻被寫在定案段落裡，於是後來每個人都當成 PM 的要求。
      ⚠️ 這是第二次「我方判斷混進定案」（第一次是「按鈕不隨輪播換」，見 R2g）。
   ✅ tel: 連結與地址連地圖**保留**——那是實用性，與字級無關。 */
.site-footer__phone { font-size: inherit; }
.site-footer__contact li { align-items: center; }

/* --- 底部版權列 --- */
/* 🔴 demo 是 0.8rem ＝ 12.8px，**低於本案硬下限**，一律走 --fs-xs。
   🔴 demo 的字色是 rgba(255,255,255,0.7)——透明度會隨底色漂移，換底就得重驗一次。
      改用實色 token。 */
.site-footer__legal {
  border-top: 1px solid rgba(254, 250, 245, 0.2);
  padding-block: var(--sp-4);
  /* R2m④：版權宣告不是常常需要知道的訊息，比其他小字再低一階（見第 3 節 --fs-fine） */
  font-size: var(--fs-fine);
  font-weight: var(--fw-normal);
  color: var(--text-inverse-soft);
  text-align: center;
}
.site-footer__legal a:hover { text-decoration: underline; }

/* ⚠️ 四欄要堆疊，不可硬塞。1023 以下先收成兩欄，599 以下才單欄——
   單欄在平板會拉出很長的一條，反而更難掃。 */
@media (max-width: 1023px) {
  .site-footer__grid { grid-template-columns: 1fr 1fr; gap: var(--sp-10) var(--sp-8); }
}
@media (max-width: 599px) {
  .site-footer__grid { grid-template-columns: 1fr; gap: var(--sp-8); }
  .site-footer__brand p { max-width: none; }
}

/* ==========================================================================
   14. 🔴 首頁給不出的三種版型（CLAUDE.md 第 3 節要求在 B1 一併定）
   --------------------------------------------------------------------------
   首頁沒有輸入欄位、沒有長文、沒有應用程式式版型。不先定，做到那些頁面時
   會現場發明，全站就分叉了。
   PM 2026-08-06：Figma 上的表單只是「示意這裡有表單」，尺寸由我方統一。
   ========================================================================== */

/* --------------------------------------------------------------------------
   14.1 版型一：表單（登入／註冊／我的帳號／聯絡我們．共 8 張，見 E1）
   -------------------------------------------------------------------------- */
.form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-6);            /* 欄位之間固定 24px */
  width: 100%;
  max-width: 30rem;            /* 480px．單欄表單（登入、忘記密碼） */
  margin-inline: auto;
}
.form--wide { max-width: 45rem; }   /* 720px．註冊等長表單 */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);            /* 標籤／欄位／說明／錯誤，四者之間一律 8px */
}

/* 標籤一律在欄位上方
   🔴 不用浮動標籤（floating label）——欄位裡有字時，長輩分不清那是提示還是自己填過的內容。
      這是近年常見做法，但對本案的使用者是災難。 */
.field__label {
  font-size: var(--fs-xs);
  font-weight: var(--fw-normal);
  color: var(--text-brand);
}

/* 必填靠星號有無表達
   🔴 不寫「可留空」「可先不管」「略過」——那類措辭會被客戶抓語病、把責任推給我們。 */
.field__label[data-required]::after {
  content: " *";
  color: var(--c-danger);
}

.field__control {
  width: 100%;
  min-height: 3.5rem;          /* 56px．點擊區遠高於 44px 下限 */
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border-field);
  border-radius: var(--r-field);
  background-color: var(--c-white);
  font-family: inherit;
  font-size: var(--fs-body);
  color: var(--text-main);
  transition: border-color var(--dur-base) var(--ease),
              box-shadow var(--dur-base) var(--ease);
}
.field__control:focus {
  outline: none;
  border-color: var(--c-maroon);
  box-shadow: var(--shadow-focus);
}
textarea.field__control {
  min-height: 9rem;
  resize: vertical;
  line-height: var(--lh-body);
}

/* 說明文字：講用途與填了的好處
   文案公式：①這欄是什麼／出現在哪 → ②不填會怎樣 → ③要填什麼 → ④建議怎麼設定 */
.field__hint {
  font-size: var(--fs-xs);
  font-weight: var(--fw-normal);
  color: var(--text-muted);
}

/* 錯誤訊息貼在該欄位下方
   🔴 不可只在表單最上方列一整串——長輩不會回頭找是哪一欄錯。
   ⚠️ 語氣要說明「怎麼改」，不說「格式錯誤」。例：「手機號碼請填 10 碼數字」。 */
.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--c-danger);
}
.field[data-invalid] .field__control {
  border-color: var(--c-danger);
  background-color: var(--bg-danger-soft);
}
/* ⚠️ 錯誤狀態同時用「邊框變色 ＋ 底色 ＋ 文字訊息」三種訊號，
   不只靠顏色——色弱者只看顏色分不出來。 */

/* 密碼欄的「顯示密碼」眼睛圖示（長輩打錯率高，這是必要項不是加分項） */
.field__control-wrap { position: relative; }
.field__control-wrap .field__control { padding-right: 3.5rem; }
.field__reveal {
  position: absolute;
  right: var(--sp-2);
  top: 50%;
  transform: translateY(-50%);
  width: 2.75rem;
  height: 2.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  border-radius: var(--r-field);
}

/* 手機號碼欄的國碼下拉：多包一層 .field__phone 讓 select 與 input 並排，
   不直接把兩顆 .field__control 塞進 .field，是因為 .field 是直向 flex，
   需要一個橫向容器才能左右排列，且日後若要加驗證提示也有地方可以掛。 */
.field__phone {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
}
.field__phone-code {
  flex: 0 0 auto;
  /* ⚠️ 不要再寫死 rem 寬度：--fs-body 18px 起跳，無障礙字級（data-fs）還會放大到 20／22px，
     固定寬度會讓「中國大陸 +86」這類長選項鑽到右側箭頭底下。交給瀏覽器依最長選項算寬，
     max-width 只用來擋後台新增超長國名時把號碼欄擠爆。 */
  width: auto;
  max-width: 14rem;
  text-overflow: ellipsis;
}
.field__phone-number {
  flex: 1 1 auto;
  min-width: 0;
}

.field--inline {
  flex-direction: row;
  align-items: center;
  gap: var(--sp-3);
}
.field--inline .field__control {
  width: 1.375rem;
  height: 1.375rem;
  min-height: 0;
  flex: none;
}
/* checkbox／radio 的點擊區靠 label 撐開，不是靠方框本身 */
.field--inline .field__label {
  font-size: var(--fs-body);
  padding-block: var(--sp-3);
  cursor: pointer;
}

.form__actions {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  margin-top: var(--sp-2);
}
.form__actions .btn { width: 100%; justify-content: center; }

/* 送出中：按鈕 disabled ＋ 文字改「處理中…」，避免長輩連按多次 */
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}

/* --------------------------------------------------------------------------
   14.2 版型二：長文內文（消息內頁、關於我們、隱私權政策）
   -------------------------------------------------------------------------- */
.prose {
  max-width: 45rem;            /* 720px．中文一行約 30–36 字，長輩讀起來不吃力 */
  margin-inline: auto;
  font-size: var(--fs-body);
  line-height: 1.8;            /* 比一般內文的 1.5 再寬——長文需要更多呼吸 */
  color: var(--text-main);
}
.prose > * + * { margin-top: var(--sp-6); }     /* 段距一律 24px */
.prose h2 {
  margin-top: var(--sp-12);
  font-size: var(--fs-h3);
  color: var(--text-heading);
}
.prose h3 {
  margin-top: var(--sp-8);
  font-size: var(--fs-lead);
  color: var(--text-heading);
}
.prose img,
.prose figure { border-radius: var(--r-card); overflow: hidden; }
.prose figcaption {
  margin-top: var(--sp-3);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  text-align: center;
}
/* 長文開頭的引言句：比內文大一階，用來把讀者帶進段落。
   ⚠️ 不用 --fs-h3（40px）——那是章節標題的級距，會蓋過真正的標題。 */
.prose__lead {
  font-size: var(--fs-lead);
  line-height: var(--lh-lead);
  color: var(--text-heading);
}

/* 置中版：403／404 與「理念」那類短而重的段落用。
   ⚠️ 只用在**短內容**上——長文置中會讓每一行的起點都不一樣，中文尤其難讀。 */
.prose--center { text-align: center; }

.prose ul, .prose ol { padding-left: var(--sp-6); }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li + li { margin-top: var(--sp-3); }

/* 🔴 R17-01（2026-08-31）：**同意區塊要跟著還原項目符號，不能只還原 `.prose`。**
   第 8 節的 reset 把全站的 `list-style` 關掉（那是給導覽列與卡片列表用的），
   `.prose` 有還原、**`.consent__scroll` 沒有** → 註冊頁同意區塊裡
   第七、九、十、十一節的項目符號**一直是看不見的**，
   ⚠️ 而且不會報錯，畫面上只像是「那幾段排版比較擠」。
   **判準：凡是「放長文的容器」都要還原，不是只還原其中一個。** */
.consent__scroll ul, .consent__scroll ol { padding-left: var(--sp-6); }
.consent__scroll ul { list-style: disc; }
.consent__scroll ol { list-style: decimal; }
.consent__scroll li + li { margin-top: var(--sp-3); }

/* --- 長文裡的表格（隱私權政策的四張「左欄名稱／右欄說明」）---
   ⚠️ 兩欄一律「左邊是名稱、右邊是說明」，左欄用 <th scope="row">。
      不是裝飾——報讀器靠它把每一格的說明對回該列的名稱。
   ⚠️ 不設 table-layout: fixed：左欄長度差很多（「管理員」三個字 vs
      「親屬資料（父／母／配偶姓名、房別、代數）」），固定比例一定有一邊擠。
   🔴 R17-01：**兩個容器都要寫**——政策長文走 `.prose`（privacy），註冊頁的同意
      區塊走 `.consent__scroll`，**兩邊載入的是同一份 HTML**。只寫一個的話，
      註冊頁那份表格會退回瀏覽器預設（無框線、內距 1px），整列黏成一行。 */
.prose table,
.consent__scroll table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
}
.prose thead th,
.consent__scroll thead th {
  background: var(--c-sand);
  color: var(--text-heading);
  font-weight: var(--fw-bold);
  text-align: left;
}
.prose th,
.prose td,
.consent__scroll th,
.consent__scroll td {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border-soft);
  vertical-align: top;
}
.prose tbody th,
.consent__scroll tbody th {
  color: var(--text-heading);
  font-weight: var(--fw-bold);
  text-align: left;
  white-space: nowrap;
}
/* 🔴 左欄的 nowrap 只在放得下的時候才成立。窄螢幕會撐破版面，
      所以下面 767px 起改成上下堆疊，這條也要一併解除。 */
/* 🔴 `:not(.btn)` 不可省。
   `.prose a` 的特異性（0,1,1）壓過 `.btn--primary`（0,1,0），
   於是 `.prose` 裡的按鈕會被染成咖啡棕字＋底線——
   **而主按鈕本來就是咖啡棕底，文字直接變成看不見。**
   實測踩到（403 頁的「登入」按鈕變成一塊沒有字的深色方塊），
   而且不報錯：DevTools 看得到 `.btn--primary` 那條規則，只是輸給了 `.prose a`。 */
.prose a:not(.btn) {
  color: var(--c-maroon);
  text-decoration: underline;
  text-underline-offset: 0.25em;
}
/* ⚠️ 長文裡的連結一律加底線——只靠顏色區分，色弱者與長輩都看不出那是連結 */

/* --------------------------------------------------------------------------
   11.13 家庭樹（C0／C1）
   --------------------------------------------------------------------------
   結構規則來自 A1，不要重新發明：
     ・節點只有本家血親；配偶不是節點，是掛在血親右側的附屬卡片
     ・子女一律從血親本人下方延伸，不從夫妻之間拉
     ・多位配偶依婚姻先後往右排（第一段最靠近血親）——與子女由左至右的出生順序同向，
       分組標籤帶的連線才不會交叉
     ・卡片只放三件事：姓名・排行稱謂・生卒年
       （房別在麵包屑、代數在左側標尺，寫在卡片上是重複 N 次的零資訊，
        而單代 20+ 人時寬度是最稀缺的資源）
   -------------------------------------------------------------------------- */
:root {
  --tree-card-w:   11.25rem;  /* 180px．🔴 不可動：橫向是硬約束 */
  /* 🔴 R13-03（PM 2026-08-26）：200px → 214px。
     「查看詳情」從 30.8px 加高到 44px（實測值，+13.2）→ 卡片跟著加 14px。
     🔴 **`--tree-card-w` 一律不動**（橫向是硬約束），只往下長。
     ⚠️ **這個值在兩個地方各寫了一份**（本行 ＋ `assets/js/tree.js` 的 `CARD_H`）——
        CSS 畫卡片、JS 算座標，**改一邊要回頭改另一邊**，
        只改一邊的話卡片會與連線／標尺錯開，而且不會報錯。 */
  --tree-card-h:   13.375rem; /* 214px．見上方註解 */
  --tree-gap-x:    0.75rem;   /* 同一家庭單位內，血親與配偶之間 */
  --tree-gap-unit: 2rem;      /* 兄弟姊妹的家庭單位之間 */
  /* 🔴 R13-03：88px → 120px，**因為徽章長高了**（28 → 44）。
     徽章掛在卡片下緣 +8，所以它現在佔到 +52；子女連線的水平段走在 `GAP_Y / 2`。
     88 之下水平段落在 +44 ＝ **被徽章整個蓋住**（連線看起來斷在徽章裡）。
     120 讓水平段回到 +60，與徽章維持**改前相同的 8px 淨距**。
     ⚠️ 同樣是兩地一值（本行 ＋ `tree.js` 的 `GAP_Y`）。 */
  --tree-gap-y:    7.5rem;    /* 120px．代與代之間（連線走這裡） */
}

.tree-canvas {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-color: var(--bg-alt);
  touch-action: none;      /* 雙指縮放由我們接管，不觸發瀏覽器縮放 */
  cursor: grab;
  user-select: none;
}
.tree-canvas.is-panning { cursor: grabbing; }

/* 被拖曳／縮放的那一層。transform 是 runtime 才算得出來的值，
   這是本案唯一合理的 inline style（見 CLAUDE.md 第 7 節第 2 條）。 */
.tree-stage { position: absolute; top: 0; left: 0; transform-origin: 0 0; }

.tree-links { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
.tree-links path {
  fill: none;
  stroke: rgba(74, 40, 39, 0.28);
  stroke-width: 1.5;
}
.tree-links path.is-lineage { stroke: var(--c-maroon); stroke-width: 2.5; }

/* --- 節點卡 --- */
.tree-node {
  position: absolute;
  width: var(--tree-card-w);
  height: var(--tree-card-h);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.125rem;
  padding: var(--sp-3);
  border-radius: var(--r-field);
  background-color: var(--c-white);
  text-align: center;
  transition: box-shadow var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

/* 🔴 R8-06（PM 2026-08-19）：**現任配偶的卡片與血親完全一致**——虛線與異色底都拿掉。
   ~~2026-07-31 定案「血親卡＝實心邊框、配偶卡＝淺色虛線邊框，不可只靠稱謂文字區分」~~
   **已被推翻**。PM 的理由：配偶也是家族成員，虛線淺色看起來像外人。

   ⚠️ **原定案的顧慮沒有被忽略，是被換了一種方式解決**：
      它擋的是「只靠稱謂**文字**區分」（長男／長媳字級小，長輩讀不出差異），
      而現在區分的是**顏色**——稱謂那一行血親是內文灰、配偶是莓紅 `--c-berry`（4.89:1）。
      🔴 **字重刻意不加重**（PM 明示「用顏色區分就好」）。

   🟡 **前配偶維持虛線**（`--ex`）：虛線在這裡表達的是「**關係已結束**」，不是「外人」，
      那個語意仍然需要。PM 上一批也明說「前配偶可保持現在的卡片樣式」。 */
.tree-node .avatar { margin-bottom: var(--sp-2); }
.tree-node--blood,
.tree-node--spouse { border: 2px solid var(--c-maroon); }
.tree-node--ex {
  border: 1.5px dashed rgba(74, 40, 39, 0.45);
  background-color: var(--bg-page);
}

.tree-node__name {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--text-heading);
  line-height: 1.25;
  word-break: break-word;
}
.tree-node--ex .tree-node__name { font-weight: var(--fw-normal); color: var(--c-maroon); }

.tree-node__rank,
.tree-node__years {
  font-size: 0.8125rem;          /* 12px：樹上的次要資訊，不是頁面可見文字的下限對象 */
  color: var(--text-muted);
  line-height: 1.3;
}
/* 🔴 R9-04（PM 2026-08-19）：~~`.tree-node__rank--rel` 莓紅稱謂~~ **整條已刪除**（連 class 一起）。
   ⚠️ **這推翻了 R8-06 的莓紅定案**（那是我方建議、PM 上一批採用的）。
   **PM 的新理由比顏色可靠**：「反正看連接線也會知道是配偶，因為配偶上方不會接到其他成員，
   唯一接線只有來自左邊的血親成員卡。」——它用**連線的拓樸**取代**視覺標記**，
   而結構不會因為縮放、色弱、列印而失效。
   🔴 **刪掉的是 class 不是只有色值**：留一條「與 `--text-muted` 相同」的規則，
      下一個人只會覺得它多餘、或反過來以為某處還在依賴它。
   ⚠️ 前配偶的「前配偶」字樣同樣是一般色——它的區分由 `--ex` 的虛線卡框承擔。 */
/* ⚠️ 這兩行刻意小於 14px 的地板：它們是密集圖表裡的輔助標示，
   而且卡片可縮放（使用者能放大到看得清為止）。姓名本身仍走 --fs-xs。 */

/* 🔴 R7-06：第 N 代 ＋ 查看詳情。兩者都只往下長，`--tree-card-w` 一律不動。
   PM 的理由之一是「縮放後看不到最左邊的代數標尺」——所以代數要**寫在卡片上**，
   標尺仍然保留（沒縮放時它比較好掃視）。 */
.tree-node__gen {
  font-size: 0.8125rem;
  color: var(--text-eyebrow);
  line-height: 1.3;
}

/* 🔴 **只有這顆按鈕會開彈窗**（卡片本身不再是 button）——這樣拖曳畫布不會誤觸。

   🔴 R13-03（PM 2026-08-26）：**真的畫成 44px 高，不再用 `::after` 撐隱形點擊區。**
   ~~原本的做法是「視覺 31px、`::after` 撐出隱形的 44×44」，理由是不想再吃掉卡片 13px~~
   ——**PM 已推翻**（原話：「一起加高」）。

   **為什麼隱形點擊區在這裡不夠**：它讓「點得到」成立，卻讓「**看得到**」不成立。
   本案受眾年齡偏高，31px 高的按鈕在 180px 寬的卡片上本來就不顯眼，
   而**看不出那是可以按的東西，點擊區再大也沒有用**。
   ⚠️ 這與 `.tree-expand` 是**同一次改動**——只改一邊就是日後不一致的來源（R13-03 明示）。
   ⚠️ 代價已知並接受：卡片 200 → 214px（`--tree-card-h`），樹的總高增加約 15%。 */
.tree-node__more {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: 2.75rem;   /* 🔴 44px．CLAUDE.md 第 1 節「點擊區塊最小 44×44px」 */
  margin-top: var(--sp-1);
  padding: 0.25rem var(--sp-3);
  border: 1px solid var(--border-soft);
  border-radius: var(--r-field);
  background-color: var(--bg-page);
  font-size: 0.8125rem;
  color: var(--text-heading);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* ⚠️ 原本這裡有一條 `.tree-node__more::after`（隱形的 44×44 觸控區），R13-03 已刪除
   ——按鈕本體現在就是 44px 高，留著那條只會讓下一個人以為視覺尺寸還是小的。 */
.tree-node__more:hover { background-color: var(--c-maroon); color: var(--text-inverse); }
.tree-node__more:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* 🔴 R9-01（PM 2026-08-19）：篩選時**不符合條件的卡片變淡**。
   ⚠️ **這推翻了 R8-04 的「未命中維持原樣」**（那是我方建議，理由是「70 張卡有一半變淡
      會像網站壞了」）。**PM 的判斷是對的，而且複查量出了理由**：勾一個代數會標示
      26／37 張（70%）——**當七成的卡都被「加強」時，加強就等於沒有加強**。

   🔴 **`--fade`（篩選未命中）與 `--dim`（點人高亮直系時的非直系）是兩個 class，不可合併**：
      兩種狀態會同時存在（篩完再點某個人），共用一個 class 就分不出誰造成的，
      而且 `focusPerson()` 每次都會把 `--dim` 整批重設 → 篩選的變淡會被它清掉。
   ⚠️ **`--fade` 寫在 `--dim` 之前**：兩者同權重，兩個 class 同時掛上時由後者勝出。
      直系高亮是使用者「剛剛主動做的動作」，讓它的 0.32 壓過篩選的 0.4 才對。
   ⚠️ **0.4 是刻意不再更淡**：太淡會讓長輩以為那些人被刪掉了（規格建議 0.35–0.45）。
   🔴 **不勾任何條件時一張都不可變淡**——「不勾＝不篩選」是 R8-04 定的語意，
      判斷在 `family-tree.js` 的 `readFilter()`（回傳 null 就整批移除這個 class）。 */
.tree-node--fade  { opacity: 0.4; }
.tree-node--focus { box-shadow: 0 0 0 4px rgba(255, 174, 161, 0.75); }
.tree-node--dim   { opacity: 0.32; }
.tree-node:hover  { box-shadow: var(--shadow-hover); }

/* --------------------------------------------------------------------------
   🔴 R12-01（PM 2026-08-25）：**登入的會員自己那一張卡**——「這是我」。
   --------------------------------------------------------------------------
   ✅ 補做既有定案（第 3 節 2026-08-03「登入後在家庭樹上點亮自己的位置」），不是新功能。

   🔴 **刻意不再用框線／光暈／透明度**——那三種語彙已經被用光了，而且它們的性質不同：

     | class            | 語彙            | 什麼時候出現       |
     |------------------|-----------------|--------------------|
     | `--match`        | 框色＋4px＋外光暈 | 篩選命中（暫時）   |
     | `--focus`        | 外光暈           | 點人高亮直系（暫時）|
     | `--dim`／`--fade`| 透明度           | 上述兩者的反面（暫時）|
     | **`--me`**       | **底色＋角落徽章** | **永遠成立**      |

   **理由**：前三種都是「操作才出現」的臨時狀態，而「這是我」是**永遠成立**的。
   用同一種語彙表達，操作時會互相蓋掉——篩選一勾，我的框就被 `--match` 的 4px 框吃掉，
   **而畫面上只看得到「我的卡不見了」**，不會有人聯想到是兩種狀態撞在一起。
   ⚠️ 反過來說，**四種狀態要能同時成立**：篩選命中的我 ＝ 莓紅粗框（match）
      ＋ 淡粉底＋角落徽章（me）；被篩掉的我 ＝ 0.4 透明度 ＋ 底色與徽章一起變淡。
      **底色與徽章不會被 opacity 以外的任何一種蓋掉。**

   ⚠️ **底色必須放在 `--ex` 之後**（前配偶用 `--bg-page`）：會員本人也可能是某人的前配偶，
      同權重時後者勝出，`--me` 要贏。
   ✅ **對比度實測（#FFF1EE 底）**：標題 11.74／次要文字 5.84／代數莓紅 4.62——三者都過 AA。
      徽章的咖啡棕字 on 珊瑚粉 ＝ 7.28。 */
.tree-node--me { background-color: #FFF1EE; }

/* 角落徽章。⚠️ **不是按鈕**：它陳述身分，不是操作入口——
   「捲到自己」由左欄的「找到我」負責（那裡才有 ≥44px 的點擊區）。
   ⚠️ 放左上角不是右上角：右上角在橫向樹上緊鄰 12px 的配偶間距與婚姻連線，
      左上角是卡片內完全沒有東西的位置（內容全部水平置中）。 */
.tree-node__me {
  position: absolute;
  top: var(--sp-2);
  left: var(--sp-2);
  min-width: 1.5rem;
  padding: 0.0625rem var(--sp-2);
  border-radius: var(--r-field);
  background-color: var(--c-coral);
  font-size: 0.8125rem;
  font-weight: var(--fw-bold);
  line-height: 1.4;
  color: var(--c-maroon);
  text-align: center;
}

/* --------------------------------------------------------------------------
   節點下方的展開／收合徽章。
   🔴 R12-03（PM 2026-08-25）：**它現在是雙向的**——
      收合中顯示「+N」（點了展開）／展開中顯示「−」（點了收合）。
      ~~原本只有收合中才畫~~，於是預設展開的那幾代**完全沒有收合入口**。
   ⚠️ 文案只給數字，不寫「N 個子女」：這個數字是「已登錄的人數」，
      不等於這個人有幾個孩子。UI 與文案都不能傳達「這個人沒有後代」。
   -------------------------------------------------------------------------- */
/* 🔴 R13-04（PM 2026-08-26）：**徽章加上可見文字**——「+4 展開」／「− 收合」。
   **PM 原話**：「只有符號和數字，對長輩來說不清楚這個按鈕可以幹嘛。」
   ⚠️ 文字是 `tree.js` 產的（`+N 展開` / `− 收合`），這裡只負責放得下：
      最長的「+12 展開」約 75px，`--tree-card-w` 是 180px，**綽綽有餘**。
   🔴 **`aria-label` 仍然是完整句（含人名）**，不可改成與可見文字相同——
      可見文字沒有人名，報讀器使用者需要知道那是誰的。見 `tree.js`。

   🔴 R13-03：36×28 → **≥44×44**，且**不再用 `::after` 撐隱形點擊區**。
   ~~原本的註解寫「真的畫成 44px 會吃掉代與代之間 88px 的一半，連線就沒地方走了」~~
   ——**那個顧慮是真的，解法是把 `--tree-gap-y` 一併加到 120px**（見該 token 的註解），
     不是把按鈕畫小。⚠️ 與 `.tree-node__more` 是同一次改動，只改一邊就會不一致。 */
.tree-expand {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  min-width: 2.75rem;
  min-height: 2.75rem;   /* 🔴 44px．CLAUDE.md 第 1 節「點擊區塊最小 44×44px」 */
  padding-inline: var(--sp-3);
  border: 1.5px solid var(--c-maroon);
  border-radius: var(--r-field);
  background-color: var(--c-maroon);
  color: var(--text-inverse);
  font-size: 0.8125rem;
  font-weight: var(--fw-bold);
  box-shadow: 0 2px 6px rgba(74, 40, 39, 0.2);
}
/* ⚠️ 原本這裡有一條 `.tree-expand::after`（隱形的 44×44 觸控區），R13-03 已刪除
   ——徽章本體現在就是 44px 高。**位置關係要一起記著**：
   徽章掛在卡片下緣 +8，佔到 +52；子女連線的水平段走在 `--tree-gap-y / 2` ＝ +60，
   兩者淨距 8px（與改前相同）。**動 `--tree-gap-y` 或那個 +8 之前先回來看這段。** */
.tree-expand:focus-visible { outline: none; box-shadow: var(--shadow-focus); }

/* 🔴 **展開中的「−」刻意比「+N」低調**（白底細框，不是實心）。
   **兩者要傳達的事情不同**：「+N」代表「**下面還有你看不到的人**」——
   那是資訊，必須顯眼；「−」只是「你可以把這一段收起來」——那是選項。
   ⚠️ 預設全展開之後樹上同時有二十幾個徽章，全部畫成實心咖啡棕會蓋過成員卡本身，
      讓長輩第一眼看到的是一堆按鈕而不是家人的名字。
   ✅ 對比度：咖啡棕字 on 白底 ＝ 13.4，遠高於 AA。 */
.tree-expand--open {
  background-color: var(--c-white);
  color: var(--c-maroon);
  box-shadow: 0 1px 4px rgba(74, 40, 39, 0.14);
}
/* 🔴 R12-04（PM 2026-08-25）：hover 色從 `#223829` 換成莓紅。
   `#223829` 是**深綠色票的殘留**，而那整組色 2026-08-13 就已作廢（3.1）——
   徽章底是咖啡棕、hover 卻變成深綠，是兩套色票撞在一起。
   ⚠️ **同類已掃全專案（`grep -rn 223829`，排除 `dist/`）：只有這一處。**

   **為什麼選莓紅 `--c-berry` 而不是「把咖啡棕加深」**：
   ① 它是色票裡既有的一階，不必新調一個色（3.1 只有五個色，多一個就是分叉）。
   ② 亮度是咖啡棕的 **5.02 倍**，hover 一眼看得出來——加深咖啡棕在 36×28 的小徽章上
      幾乎看不到變化，而這一頁的使用者年齡層偏高。
   ✅ 白字 on 莓紅 ＝ **5.09**，過 AA。

   🔴 **兩種狀態 hover 到同一個色**（`+N` 實心咖啡棕 → 莓紅；`−` 白底 → 莓紅實心）：
      它們是同一顆控制項的兩個狀態，hover 反饋分成兩色會讓人以為是兩種不同的東西。 */
.tree-expand:hover {
  background-color: var(--c-berry);
  border-color: var(--c-berry);
  color: var(--text-inverse);
}

/* 婚姻連接線（血親與配偶之間的短橫線） */
.tree-marriage {
  position: absolute;
  height: 1.5px;
  background-color: rgba(74, 40, 39, 0.45);
}
.tree-marriage--ended {
  background-image: linear-gradient(90deg, rgba(74, 40, 39,.45) 50%, transparent 50%);
  background-size: 6px 100%;
  background-color: transparent;
}

/* 多段婚姻的「○○○ 所生」標籤．單配偶時不出現——標記只在需要區分時才出現 */
/* 🔴 R9-03（2026-08-19）：**每一張血親子女卡的正上方各自一個，寬度＝那張卡的寬（180px）**。
   ⚠️ **這推翻了 R8-07 的「橫跨整組子女的水平範圍」**——那個做法會把夾在子女之間的
      **配偶卡也框進來**（PM 截圖 76），而配偶不是「誰生的」，那是錯誤的斷言。
   ⚠️ **`width` 仍然由 JS 給**（現在給的是 `CARD_W`），這裡不可寫死寬度，
      也不可用 `translateX(-50%)` 置中——位置是 runtime 才知道的值。
   ⚠️ 高度要與 `tree.js` 的 `GROUP_LABEL_H` 對得上（兩地一值，改一邊要回頭改另一邊）。
   ⚠️ 因此 `text-overflow: ellipsis` 現在是會用到的：母親名字長時會在 180px 內省略，
      **這是刻意的**（`CARD_W` 是硬約束，不可為了標籤加寬）。 */
.tree-group-label {
  position: absolute;
  box-sizing: border-box;
  padding: 0.125rem var(--sp-2);
  border-radius: var(--r-field);
  background-color: var(--c-coral);
  color: var(--text-on-warm);        /* 🔴 白字 on 珊瑚粉只有 1.78:1，走 --text-on-warm */
  font-size: 0.8125rem;
  line-height: 1;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 每一列左側的代數標尺 */
.tree-gen-ruler {
  position: absolute;
  left: 0;
  display: flex;
  align-items: center;
  height: var(--tree-card-h);
  padding-inline: var(--sp-3);
  border-radius: var(--r-field);
  background-color: rgba(255, 255, 255, 0.9);
  font-size: 0.8125rem;
  color: var(--text-muted);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   C2 手機聚焦模式：以某人為中心的三代視圖
   --------------------------------------------------------------------------
   🔴 **這不是加分項，是這個寬度下唯一可用的手機方案**（9.8）——
      橫向樹寬 7364px，fit 進手機畫面後姓名只有 2.3px。
   ⚠️ **刻意用一般流式排版，不是絕對定位的畫布**：三列、十幾張卡，
      手機直接上下捲就好，**不需要拖曳也不需要縮放**。
      在手機上要長輩拖一張 7364px 的畫布，正是這個模式要避免的事。
   -------------------------------------------------------------------------- */
.tree-focus {
  padding: var(--sp-5);
  background-color: var(--bg-alt);
}
.tree-focus[hidden] { display: none; }

.tree-focus__hint {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  text-align: center;
}

.tree-focus__body {
  margin-top: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-8);
}

/* 每一列之間畫一條連線，讓「上一代 → 本人 → 下一代」的血緣關係看得出來。
   ⚠️ 少了這條線，三列會看起來像三個不相干的清單。 */
.tree-focus__row { position: relative; }
.tree-focus__row + .tree-focus__row::before {
  content: "";
  position: absolute;
  left: 50%;
  top: calc(var(--sp-8) * -1);
  width: 2px;
  height: var(--sp-8);
  background-color: rgba(74, 40, 39, 0.28);
}

.tree-focus__label {
  margin-bottom: var(--sp-3);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  text-align: center;
}

.tree-focus__cards {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-3);
}

/* 🔴 覆寫桌機樹卡的絕對定位——同一個 .tree-node 在這裡是流式排版的一員。
   ⚠️ 寬度不綁 --tree-card-w（180px 是橫向樹的硬約束，這裡沒有那個約束），
      改成兩張並排放得下即可。 */
.tree-focus__card {
  /* 🔴 R12-01：`static` → `relative`。版面行為完全相同（都參與正常流），
     但 `.tree-node__me` 是絕對定位的——`static` 之下它的基準會跳到
     `.tree-focus__row`（那一列是 `position: relative`），
     徽章會飄到整列的左上角去，**而且不會報錯**（CLAUDE.md 9.22 的同一個病）。 */
  position: relative;
  width: calc(50% - var(--sp-3) / 2);
  max-width: 11rem;
  height: auto;
  min-height: var(--tree-card-h);
}
/* 本人那一張加重，一眼看得出「現在以誰為中心」 */
.tree-focus__card.is-center { box-shadow: 0 0 0 3px var(--c-coral); }

/* 可以移動中心的卡片才有指標樣式——配偶不是節點，點了無處可去（A1）。
   🔴 **R11-02（2026-08-25）：選擇器從 `button.tree-focus__card` 改成 `[data-focus]`。**
      聚焦模式的卡片改用共用的 `cardHTML()` 之後**一律是 `<div>`**，不再是 `<button>`——
      因為卡片裡面現在有一顆「查看詳情」按鈕，**button 不可以包 button**（HTML 無效，
      瀏覽器會自己拆掉標籤，而且不報錯）。
   ⚠️ 沒改這條的話就是「可以點、但沒有手指游標」——**沒有任何錯誤訊息**，
      只是長輩看不出那張卡可以點。
   🟡 **已知代價（要讓 PM 知道）**：卡片不再是 `<button>` ＝ 鍵盤 Tab 不會停在整張卡上，
      鍵盤使用者要換中心得走左欄的搜尋結果（那些仍是 button，且在聚焦模式下直接換中心）
      或「回到始祖」。**桌機的樹卡從 R7-06 起就已經是這個狀態**，兩種模式現在一致。 */
.tree-focus__card[data-focus] { cursor: pointer; }
.tree-focus__card[data-focus]:hover { box-shadow: var(--shadow-md); }

.tree-focus__actions {
  margin-top: var(--sp-8);
  text-align: center;
}

/* --- 畫布控制項 --- */
.tree-controls {
  position: absolute;
  right: var(--sp-4);
  bottom: var(--sp-4);
  display: flex;
  gap: var(--sp-2);
}
.tree-controls button {
  width: 2.75rem;                  /* 44×44 點擊區 */
  height: 2.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-field);
  background-color: var(--c-white);
  color: var(--c-maroon);
  box-shadow: var(--shadow-hover);
}
.tree-controls button:hover { background-color: var(--bg-alt); }

/* 路徑麵包屑：點某人之後顯示他從始祖下來的世系
   ⚠️ 這比高亮更有價值——它直接回答「我跟這個人是什麼關係」 */
/* 🔴 貼在畫布「上緣」，不是下緣。
   原本放 bottom，畫布高度一長，這行字就被推到視窗外（實測 top: 866px、視窗高 878px，
   底部超出 33px 要捲動才看得到）。而它是點節點之後**唯一的文字回饋**——
   其餘變化都只是「變淡」，看不到它就會被讀成「點了沒反應」。 */
/* 🔴 R9-07（PM 2026-08-19）：**加寬到撐滿畫布可用寬** ＋ 右側一顆 ✕。
   ~~`right: 5rem`~~ → `right: var(--sp-4)`，與左緣對稱。
   ⚠️ 原本那 5rem 是留給右上角的，但畫布右上角**什麼都沒有**（縮放鈕在右下，
      見 `.tree-controls` 的 `bottom`）——那塊留白因此純粹是空的（PM 截圖 79）。
   ⚠️ 文字放內層的 `.tree-path__text` 並讓它 `min-width: 0`：
      不給 `min-width: 0` 的話 flex 子元素的最小尺寸是內容寬度，
      **世系一長就會把 ✕ 擠出容器外**，而不是自己省略號。 */
.tree-path {
  position: absolute;
  left: var(--sp-4);
  top: var(--sp-4);
  right: var(--sp-4);
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-4);
  border-radius: var(--r-field);
  background-color: rgba(255, 255, 255, 0.94);
  box-shadow: var(--shadow-hover);
  font-size: var(--fs-xs);
  color: var(--text-main);
}
.tree-path__text { flex: 1; min-width: 0; }
/* 🔴 ≥44×44 的點擊區（既有通則）——長輩點不準一個小叉叉。
   ⚠️ `flex-shrink: 0`：不加的話長世系會把它壓扁成一條線，看起來仍在、卻按不到。 */
.tree-path__close {
  flex-shrink: 0;
  width: 2.75rem;
  height: 2.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-field);
  color: var(--c-maroon);
  cursor: pointer;
}
.tree-path__close:hover { background-color: var(--bg-alt); }
.tree-path__close:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.tree-path__close svg { width: 1.25rem; height: 1.25rem; }
.tree-path[hidden] { display: none; }

/* --------------------------------------------------------------------------
   14.3 版型三：家庭樹頁（全站唯一的應用程式式版型）
   桌機＝左欄固定操作區（搜尋＋篩選）＋右側可拖曳縮放的樹畫布。
   ⚠️ 手機沒有左側空間：改為頂部固定一列搜尋框，篩選收進抽屜。
      不可把左欄硬塞成手機版的上方一大塊，那會把樹擠到看不見。
   -------------------------------------------------------------------------- */
.tree-layout {
  display: grid;
  grid-template-columns: 22.5rem 1fr;   /* 360px 左欄 */
  gap: var(--sp-6);
  min-height: 70vh;
}
.tree-layout__side {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  padding: var(--sp-5);
  border-radius: var(--r-card);
  background-color: var(--bg-alt);
}
.tree-layout__canvas {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-card);
  background-color: var(--bg-alt);
  touch-action: none;          /* 讓雙指縮放由我們接管，不觸發瀏覽器縮放 */
  cursor: grab;
}
.tree-layout__canvas.is-panning { cursor: grabbing; }

/* 🔴 R12-02（PM 2026-08-25）：斷點 **1023 → 1024**。
   **PM 原話**：「**1024px** 時就把篩選放上面、家庭樹在下方，就像 768px 那樣排版。」
   ⚠️ **`1023` 會剛好在 PM 指名的那個寬度不生效**——而畫面看起來與「沒改」一模一樣
      （CLAUDE.md 9.30 ①）。1024 是 iPad 橫向的常見寬度，那正是他在看的東西。
   ⚠️ **全站其他地方的 1023／899／767 不要跟著改**：那些是導覽收合等各自的需求，
      不可預設它們涵蓋得到 PM 指名的觀測寬度。 */
@media (max-width: 1024px) {
  .tree-layout {
    grid-template-columns: 1fr;
  }
  .tree-layout__side {
    position: sticky;
    top: var(--header-h);
    z-index: 20;
  }
}

/* ==========================================================================
   15. 響應式
   --------------------------------------------------------------------------
   Figma 只有 1920 桌機稿，手機版由我們延伸。
   ⚠️ 字級改的是變數不是個別規則——所有元件自動跟著縮，不必逐個開 media query。
   ⚠️ 內文在最小斷點仍維持 16px（CLAUDE.md：客戶年齡層偏高，比一般案子的 14px 再大一級）。
   ========================================================================== */

/* 平板 */
@media (max-width: 1199px) {
  :root {
    /* 大字級三階一起降一級——桌機值在平板寬度下會撐破卡片標題。
       ⚠️ 三檔的相對關係要維持，不可只降預設檔：只降預設的話，
          按下「大」反而會跳回桌機尺寸，看起來像 bug。
       🔴 2026-08-14 R2h：桌機平移一階後，這裡**也要跟著平移**，
          否則 1199／1200 之間會出現一階以上的落差（45 vs 34），
          在平板轉直橫向時會看到字忽然縮一大截。
          ⚠️ 只有**手機**的覆寫不跟著平移——那是被 9.7 綁死的（見下方手機段）。 */
    --fs-hero:  2.8125rem; /* 45 */
    --fs-h2:    2.375rem;  /* 38 */
    --fs-h3:    2.125rem;  /* 34 */
    --container-pad: var(--sp-6);
  }
  :root[data-fs="mid"]   { --fs-hero: 3.125rem;  --fs-h2: 2.625rem;  --fs-h3: 2.375rem;  }  /* 50/42/38 */
  :root[data-fs="large"] { --fs-hero: 3.5rem;    --fs-h2: 3rem;      --fs-h3: 2.625rem;  }  /* 56/48/42 */
  .site-nav { gap: var(--sp-6); }
  .branch-grid { grid-template-columns: repeat(2, 1fr); gap: var(--sp-6); }
}

/* 🔴 導覽列的收合斷點獨立在 1023px，不跟其他手機樣式綁在 767px。
   實測 768px 時「站名 ＋ 五個選項 ＋ 登入按鈕」放不下，站名被擠成三行。
   平板一樣走漢堡選單。 */
@media (max-width: 1023px) {
  .site-header__toggle { display: inline-flex; }
  .site-header__inner > .site-nav,
  .site-header__inner > .btn { display: none; }
}

/* 手機 */
@media (max-width: 767px) {
  :root {
    /* 手機只覆寫「會撐破版面的大字級」五個；body／sm／xs 與桌機相同（18／17／16），
       它們已經是內文與最小級距，不再往下縮。

       🔴 --fs-hero 三檔只從 24 走到 26，幅度刻意很小。
          約束來自 D1 8.7：三張輪播的主標行數必須一致。手機拿掉硬斷行後，
          比的是三張的總字數。字級一超過某個值，最長那張就變三行、另兩張仍是兩行 → 不一致。
       ⚠️ 這不是「放大按鈕在手機沒用」——真正需要放大的是內文，
          body 仍完整走 16／18／20 三檔。大標本來就已經是全站最大的字。
       🔴 2026-08-13 換成襯線體後，這三個值已重新實測（新文案 11／9／10 字，
          比舊文案短很多）。驗法：320／375 兩個寬度下，**三張都要量**
          （非作用中的 slide 是 visibility:hidden，仍有版面尺寸，量得到）。

       🔴 2026-08-14 R2h：**只有 `--fs-hero` 不跟著平移**（被上面那條 9.7 的約束綁死）。
          其餘四個必須平移，否則會出現層級反轉——
          **平移前 `--fs-lead` 是 17px，而 `--fs-body` 已經變成 18px**，
          於是主視覺內文與卡片標題比一般內文還小。那不是「保守」，是壞掉。 */
    --fs-hero:  1.5rem;    /* 24 —— 🔴 不平移，見上方 */
    --fs-h2:    1.875rem;  /* 30 */
    --fs-brand: 1.375rem;  /* 22 */
    --fs-h3:    1.5625rem; /* 25 */
    --fs-lead:  1.1875rem; /* 19 */

    --container-pad: var(--sp-5);
    --header-h: 4.5rem;    /* 72px */
  }
  :root[data-fs="mid"] {
    --fs-hero:  1.5625rem; /* 25 —— 🔴 不平移 */
    --fs-h2:    2.0625rem; /* 33 */
    --fs-brand: 1.5rem;    /* 24 */
    --fs-h3:    1.75rem;   /* 28 */
    --fs-lead:  1.3125rem; /* 21 */
  }
  :root[data-fs="large"] {
    --fs-hero:  1.625rem;  /* 26 —— 🔴 不平移 */
    --fs-h2:    2.25rem;   /* 36 */
    --fs-brand: 1.625rem;  /* 26 */
    --fs-h3:    1.875rem;  /* 30 */
    --fs-lead:  1.4375rem; /* 23 */
  }

  /* 🔴 R7-16（PM 2026-08-18）：手機也顯示全名「台南鹽埕林姓宗親會」，
     **推翻 2026-08-04 的「桌機全名、手機短版」定案**（CLAUDE.md 第 3 節已同步更新）。
     ⚠️ 當初縮短的理由是 320px 排不下——實測「台南鹽埕」與「林姓宗親會」是**兩行**
        （.site-brand 本來就是 column），所以最長的一行只有 5 個字，320px 綽綽有餘。
        真正會擠的是 `--fs-brand` 在「大」檔＝40px 時，5 字 ＝ 200px，仍然放得下。 */

  .section-head {
    grid-template-columns: 1fr;
  }
  .section-head__action { grid-column: 1; }

  .btn {
    width: 100%;
    justify-content: center;
    gap: var(--sp-3);
  }

  .floating-tools { right: var(--sp-2); bottom: var(--sp-2); }

  .branch-grid { grid-template-columns: 1fr; }
  .branch-card__actions { grid-template-columns: 1fr; }

  /* --- 長文表格：窄螢幕改成上下堆疊 ---
     🔴 兩欄在手機放不下。隱私權政策右欄最長那格 60 幾個字，
        硬維持兩欄的話左欄會被壓成一行一個字，長輩根本讀不了。
     ⚠️ 表頭列在堆疊後沒有意義（每一列自己的左欄就是標籤），所以隱藏它。
        用 display:none 而不是 visually-hidden：display:block 一下去，
        表格語意本來就已經從無障礙樹上消失，留著只會讓報讀器多念兩個沒有對象的詞。
        堆疊後的閱讀順序是「名稱 → 說明」，對兩欄對照表反而比表格清楚。
     ⚠️ 左欄的 white-space: nowrap 一定要解除，否則長標籤會撐破容器。 */
  .prose table,
  .prose tbody,
  .prose tr,
  .prose th,
  .prose td,
  .consent__scroll table,
  .consent__scroll tbody,
  .consent__scroll tr,
  .consent__scroll th,
  .consent__scroll td { display: block; }

  .prose thead,
  .consent__scroll thead { display: none; }

  .prose tr,
  .consent__scroll tr {
    margin-top: var(--sp-4);
    border: 1px solid var(--border-soft);
  }

  .prose th,
  .prose td,
  .consent__scroll th,
  .consent__scroll td { border: 0; }

  .prose tbody th,
  .consent__scroll tbody th {
    white-space: normal;
    background: var(--c-sand);
    padding-bottom: var(--sp-2);
  }

  .prose tbody td,
  .consent__scroll tbody td { padding-top: var(--sp-2); }
}

/* 🔴 極窄螢幕（iPhone SE 1 代等 320px）只有「大」檔需要再降一級。
   26px 在 320px 下，第一張主標（17 字）會因為中文避頭尾提前斷行而變三行，
   另兩張（16／15 字）仍是兩行 → 違反 D1 8.7「三張行數必須一致」。
   ⚠️ 只降 --fs-hero，其餘字級照常放大——真正需要放大的是內文，不是大標。
   ⚠️ 不把一般手機的大檔一起降：360px 以上 26px 是一致的，
      為了極窄機犧牲所有手機的放大幅度並不划算。 */
@media (max-width: 359px) {
  :root[data-fs="large"] { --fs-hero: 1.5rem; }   /* 26 → 24 */
}

/* 手機號碼欄的國碼下拉：窄螢幕改上下堆疊，避免「台灣 +886」這類文字被壓縮到看不清楚 */
@media (max-width: 767px) {
  .field__phone {
    flex-direction: column;
  }
  .field__phone-code {
    width: 100%;
    max-width: none;
  }
}

/* ==========================================================================
   99. 🔴 [hidden] 一律有效（2026-08-24 R10 收工時補，修 forgot.html 的實際 bug）
   --------------------------------------------------------------------------
   **問題**：`[hidden] { display: none }` 是**瀏覽器預設樣式**，
   任何作者寫的 `.form { display: flex }` 都贏得過它 → `el.hidden = true` 安靜失效。
   🔴 `el.hidden` 讀回來仍然是 `true`，**console 乾淨、完全不報錯**，
      畫面上只看得到「該藏的東西還在」，不會有人聯想到是 CSS 權重問題。

   **實際後果**（2026-08-24 執行期掃 15 頁抓到，非本輪造成）：
   `forgot.html` 的三個步驟（`#step-phone`／`#step-code`／`#step-reset`）
   **同時堆在畫面上**，三顆提交按鈕全部可見，還沒送出就顯示
   「我們已經把驗證碼寄到 —。」，`#code-error` 也一直掛著。頁面總高 1968px。

   **為什麼要寫成一條通則，而不是再補一個 `.form[hidden]`**：
   在這之前站上已經有 **8 處**逐個補的 `.xxx[hidden] { display: none }`
   （`.to-top`／`.form-alert`／`[role=tabpanel]`／`.site-menu`／`.tree-focus`／
   `.tree-path`／`.tree-layout__side`／`.tree-mode`）——
   **同一個坑被踩過八次，每次都只補當下那一個**，而第九次（`.form`）就漏掉了。
   逐個補是「要有人記得維護」的防線，寫成通則才不需要任何人記得任何事。

   ⚠️ **選擇器刻意重複寫兩次 `[hidden][hidden]`**：那是把權重提到 (0,2,0)，
      壓得過所有單一 class 的 `display` 宣告。**不是筆誤，不要「順手清掉一個」。**
      🔴 提權重是正解，`!important` 不是（第 7 節第 2 條禁用，且它會連 JS 也蓋不掉）。
   ⚠️ 放在檔案最末：同權重時後者勝，這裡要確保它排在所有作者規則之後。
   ⚠️ **上面那 8 處逐個補的規則現在是多餘的，但本輪刻意沒刪**（PM 沒有交辦，
      而它們是無害的重複、不是分叉）。🔴 **重點是不要再加第九個**——
      新元件要能被 `hidden` 藏起來時，這一條已經涵蓋，什麼都不必做。
   ⚠️ `hidden="until-found"`（可被頁內搜尋展開）**本站 0 處使用**，已 grep 確認；
      日後若要用，這條會把它一起藏死，屆時要改成
      `[hidden][hidden]:not([hidden="until-found"])`。
   ========================================================================== */
[hidden][hidden] { display: none; }
