  :root{
    /* 🔴 田字格的大小只有這一個數字。練習畫布跟查筆順的預覽都讀它。
       原本兩邊各寫死一個（練習 260、查筆順 200），結果查筆順的字
       莫名其妙比練習的小一圈 —— 那不是設計，是兩個地方沒對齊。
       它們是同一種東西（同一個田字格語彙），就該是同一個數字。 */
    --char-box:260px;
    --paper:#FAF3E7;
    --paper-deep:#F0E6D2;
    --ink:#211C18;
    --ink-soft:#7A6F61;
    /* 🔴 說明文字專用的中間調（2026-09-10）。
       --ink 太重：說明跟標題同色，整頁看不出層次。
       --ink-soft 太淡：配米底只有 4.5:1 左右，剛好卡在及格線，
       對正在學認字的眼睛太吃力。
       📌 規則跟字級一樣 —— **說明類全 App 只有一個顏色**，就是這個。
          --ink-soft 只留給標籤與數字（排序、範圍選單）。 */
    --ink-mid:#574C42;
    --coral:#E8704A;
    --coral-soft:#F4C9B8;
    --seal:#B8412F;
    --seal-wash:#F1DAD2;
    --sage:#7A9B76;
    --sage-soft:#D9E6D4;
    --line:#DDD0B4;
    /* 分隔線帶一點珊瑚色，跟品牌紅點與「繼續練習」同一個色系。
       原本 #E6D9BD 是很淡的沙色，在 #FAF3E7 的紙底上幾乎看不見，
       整頁的橫帶就像沒畫完 —— 分隔線看得見，版面才讀得出結構。 */
    /* 🔴 分隔線用 2px 不是 1px：瀏覽器會把邊框對齊到實體像素，
       1.5px 在 devicePixelRatio 1.25 的螢幕上會被截成 1 個實體像素（＝0.8 CSS px），
       改了等於沒改。2px 才穩定跨過門檻。 */
    --hairline:#E8704A;
    --radius:18px;

    /* ---- 字級三級（2026-09-09 Steven 定案）----
       主要用戶是小學一年級（6–7 歲）。原本內文預設 13px 是成人資訊密度的尺寸，
       一年級生讀不動。業界兒童 App 內文普遍 18–20px（Apple HIG 內文預設 17pt、
       Android 建議 16sp，兒童向再往上一級）。

       🔴 分級的依據是「**誰在讀**」，不是「這是標題還是內文」。
          而且 App 內文案**一律對小孩講** —— 大人看得懂小孩的話，反過來不行。
       ⚠️ 例外（維持原樣、刻意長）：歡迎頁那 294 字是交接前說服家長用的，
          還有設定內頁、隱私權、購買條款。那些是真正的成人介面。
       📌 字數和字級是同一個問題：話寫太多，字就放不大。改字級前先砍字。 */
    --fs-kid:1.25rem;     /* 20px —— 小孩讀的：提示、泡泡、主要按鈕 */
    --fs-body:1rem;       /* 16px —— 一般內文 */
    --fs-label:0.875rem;  /* 14px —— 標籤註記的下限，不准再小 */

    /* 導覽時小毫的邊長。JS 用它算泡泡要離目標多遠，改這裡兩邊一起動。 */
    --mascot-size:160px;
    --serif:-apple-system,"PingFang TC","Microsoft JhengHei","Noto Sans TC",sans-serif;
  }
  *{box-sizing:border-box;}

  /* 🔴 這一行是「不要做什麼」的宣告，不是設定值 —— 100% 就是瀏覽器預設，
     寫不寫算出來都一樣。留著是為了擋住未來有人寫成 html{font-size:16px}。

     全站 77 個字級都是 rem（2026-09-09 從 px 轉，值零誤差），所以根字級是
     整個 App 唯一的字級總開關。一旦被寫死 px：
     - 網頁版使用者調瀏覽器預設字級 → 失效
     - 將來想在設定裡做「字級大小」→ 沒有旋鈕可轉

     ⚠️ 不要寫成 62.5%（那個「1rem=10px 好心算」的老派寫法）—— 它同樣是
     寫死，只是換個數字，一樣把使用者的設定蓋掉。

     📌 padding／border／radius／--char-box 刻意維持 px：
     字放大時書寫面不該跟著大（2026-09-07 Steven 定案），細線放大會糊。 */
  html{ font-size:100%; }

  html,body{
    margin:0;padding:0;height:100%;
    background:var(--paper);
    color:var(--ink);
    font-family:-apple-system,"PingFang TC","Microsoft JhengHei","Noto Sans TC",sans-serif;
    -webkit-tap-highlight-color:transparent;
    overscroll-behavior:none;
    /* 長按不要跳出「選取／拷貝」，圖片也不要跳出系統選單。
       ⚠️ 只有 user-select 不夠：iOS 上長按圖片仍會跳選單，要多一個
       -webkit-touch-callout（2026-08-29 iPad 實測回報）。 */
    user-select:none;
    -webkit-user-select:none;
    -webkit-touch-callout:none;
    /* 擋掉雙擊放大與雙指縮放。

       ⚠️ <meta viewport user-scalable=no> 在 iOS 上是無效的——Apple 從 iOS 10 起
       基於無障礙考量刻意忽略它，所以 Android 擋得住、iPad 擋不住。

       🔴 2026-08-29 從 manipulation 改成 pan-y。原因：
       - iPad 實測回報「連續點『再看一次』或『開始練習』有時會雙擊放大」，
         也就是 manipulation 沒有真的擋住（守護寶貝在 iPad iOS 18.4 也得到
         同樣結論，見 PROPOSALS.md #9）
       - 同一則提案裡 Steven 自己在 iPad 實測過 pan-y 擋得住

       判準是「這一頁有沒有東西要捲」（Steven.md 檢查表）：
       .sheet 是 max-height:78vh + overflow-y:auto，**有東西要捲** → 用 pan-y，
       不能用 none（none 會把面板的捲動一起關掉）。

       日文練習畫布另外設 touch-action:none（那裡連捲動都不能有，見 kanjiEngine）。 */
    touch-action:pan-y;
  }
  #app{
    /* 🔴 iOS Safari 的 `100vh` 是「工具列隱藏時」的高度 —— 但工具列平常是**顯示**的，
       所以 100vh **比真實可視區還高**，頁面永遠多出一截可以捲，而那一截是空的
       ——「明明下面沒東西卻有捲軸」就是這個（2026-09-09 Steven 在 iPad 上發現）。
       ⚠️ 桌機 Chromium 的 100vh ＝ 真實可視高度，所以**桌機永遠重現不了**。
       `100dvh` 是動態視窗高度，會跟著工具列縮放，才是真正的可視高度。
       兩行都留：舊瀏覽器吃不懂 dvh 會忽略第二行、沿用 100vh。
       📌 順帶好處：頁面不再多出可捲的空白 → 手掌碰到格子外時比較不會觸發捲動手勢，
          也就比較不會把手指正在寫的那一筆取消掉（見「iPad 手掌」那條）。 */
    min-height:100vh;
    min-height:100dvh;
    display:flex;
    flex-direction:column;
    align-items:center;
    /* 底部 32 → 24px（2026-09-10）。首頁內容在某些字數下會剛好比螢幕高幾 px，
       捲一點點看起來像故障（Steven：「首頁會移動」，實測多 7px）。
       ⚠️ 這只是把門檻往後推 —— 字多了照樣會捲，那是正常的，不是 bug。
       📌 底部現在有小毫佔著，本來也不需要留那麼多白。 */
    padding:20px 16px 24px;
  }

  #lang-screen{
    position:fixed;
    inset:0;
    background:var(--paper);
    z-index:999;
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    gap:12px;
    padding:24px;
  }
  .lang-card{
    width:100%;
    max-width:300px;
    display:flex;
    flex-direction:column;
    gap:12px;
  }
  .lang-icon{
    width:56px;height:56px;
    border-radius:16px;
    background:var(--coral);
    display:flex;align-items:center;justify-content:center;
    margin:0 auto 8px;
  }
  .lang-option{
    width:100%;
    padding:14px 18px;
    background:#fff;
    border:1.5px solid var(--line);
    border-radius:16px;
    color:var(--ink);
    font-size:0.9375rem;
    cursor:pointer;
    text-align:left;
    display:flex;
    align-items:center;
    gap:14px;
  }
  .lang-flag{ font-size:1.375rem; }
  .lang-name{ font-weight:700; }
  .lang-app-name{ font-size:0.75rem; color:var(--ink-soft); margin-top:2px; }

  /* ---- Header ---- */
  header{
    width:100%;
    max-width:420px;
    display:flex;
    align-items:center;
    justify-content:space-between;
    padding-bottom:14px;
    margin-bottom:20px;
    /* 沒有底線：下面「接下來練習」卡片本身就有外框，
       兩條線隔著 20px 平行擺著是雜訊（2026-08-31 Steven 指出）。 */
  }
  /* 🔴 練習畫面換成**短版**標題，不是藏起來（2026-09-10 修正）。

     一開始是整個藏掉，結果 header 只剩一個返回鍵加一顆孤零零的紅點，
     看起來像沒載入完（Steven 回報）。品牌該留，長度要收。
     長版「ZenoWorks 筆順練習」＋返回鍵＋兩個圖示在 360px 塞不下會斷成兩行；
     短版「ZenoWorks 筆順」約 116px，剩餘空間 208px，很寬鬆。 */
  #brand-title-short{ display:none; }
  body.in-practice #brand-title-text{ display:none; }
  body.in-practice #brand-title-short{ display:inline; }

  .brand{
    font-family:var(--serif);
    font-size:calc(1.125rem*var(--s,1));
    font-weight:700;
    letter-spacing:1px;
    color:var(--ink);
    display:flex;
    align-items:center;
    gap:calc(8px*var(--s,1));
  }
  /* 小紅點同時是語言選擇的隱藏入口（連點 5 下）。
     ⚠️ 7px 的東西手指點不到 —— 用透明外框把命中範圍撐到 24px，外觀不變。 */
  .brand .dot{
    width:calc(7px*var(--s,1));height:calc(7px*var(--s,1));border-radius:50%;
    background:var(--seal);
    display:inline-block;
    box-sizing:content-box;
    padding:8px;
    margin:-8px -4px -8px -8px;
    background-clip:content-box;
    cursor:default;
  }
  .icon-btn{
    width:calc(36px*var(--s,1));height:calc(36px*var(--s,1));
    border-radius:50%;
    border:1px solid var(--line);
    background:#fff;
    display:flex;align-items:center;justify-content:center;
    cursor:pointer;
    color:var(--ink-soft);
  }
  .icon-btn svg{width:calc(18px*var(--s,1));height:calc(18px*var(--s,1));}

  #home-view,
  /* 🔴 #practice-view 原本完全沒有樣式。#app 是 flex column + align-items:center，
     沒有寬度的子項會縮到內容大小 —— .stage-wrap 的 width:100% 因此是對著一個
     「跟內容一樣寬」的父層算，等於沒有作用，卡片永遠只有 296px，
     max-width:420 從來沒有生效過。 */
  #practice-view{
    width:100%;
    max-width:420px;
  }

  /* 手機：包裝層不存在，子元素直接是 #home-view 的小孩 */
  .home-col{ display:contents; }

  /* ---- 今日字卡：簽名元素，像一張落了印章的紙條 ---- */
  /* 📌 2026-09-09：這裡試過改成「單欄置中 + 全寬大按鈕」，Steven 否決
     （「繼續練習真的不用那麼大，之前的排版就不錯」）—— 已退回原本的配置。
     原配置沒有問題，有問題的只是 CTA 看不出能按；那個用底色解決就夠了，
     不需要動版面。**不要再改成全寬按鈕。** */
  .today-stamp-card{
    position:relative;
    background:#fff;
    border:1px solid var(--line);
    /* 🔴 6px → 16px（2026-09-10）。底下每一列都是 14px 圓角，只有這張卡是 6px，
       所以它在一堆圓角裡看起來像沒做完的方框 —— 它明明是首頁的主角。 */
    border-radius:16px;
    /* 收緊過一次：原本 24/22/22 + margin 22，整張 178px 太重（見上方說明） */
    padding:calc(16px*var(--s,1)) calc(20px*var(--s,1)) calc(16px*var(--s,1));
    margin-bottom:calc(16px*var(--s,1));
    cursor:pointer;
    overflow:hidden;
  }
  .stamp-eyebrow{
    font-size:calc(0.6875rem*var(--s,1));
    letter-spacing:2px;
    color:var(--ink-soft);
    text-transform:uppercase;
    margin-bottom:calc(8px*var(--s,1));
  }
  .stamp-char{
    font-family:var(--serif);
    /* 78 → 56（下方 media query）、64 → 48：這個字是預覽不是主角，
       縮小之後卡片仍然是首頁最醒目的東西，只是不再誇張。 */
    /* 48 → 60px（2026-09-10）。卡片的主角就是這個字，其他東西都是配角。 */
    font-size:calc(3.75rem*var(--s,1));
    font-weight:600;
    color:var(--ink);
    line-height:1;
    margin-bottom:calc(12px*var(--s,1));
  }
  /* 🔴 **主要按鈕**（每頁只有一個）。2026-09-09 改成實心。

     原本它是一段珊瑚色的文字加箭頭，可按的唯一提示是外層 .today-stamp-card
     的 cursor:pointer —— **觸控螢幕沒有游標，等於完全沒有提示**。
     桌機開發時滑鼠移過去手指會變，所以這個問題在開發機上永遠看不見。
     實測有使用者在這裡愣住（Steven 回報）。

     下緣 3px 實心陰影不是裝飾：它讓按鈕看起來有厚度＝可以壓下去。 */
  /* 🔴 只做到「看得出能按」為止，不放大。

     ⚠️ **不要 width:100%、不要 --fs-kid、不要下緣陰影** —— 都試過，Steven 否決：
        「繼續練習真的不用那麼大」。實心底色本身就足夠當可按的訊號，
        再加大小與陰影，它就會壓過卡片真正的主角（要練的那個大字）。
     ⚠️ inline-flex + fit-content：右邊是放大鏡、右下是統計列，全寬會蓋住它們。 */
  .stamp-cta{
    display:inline-flex;
    width:fit-content;
    align-items:center;
    gap:6px;
    /* 內距 6×12（2026-09-10 Steven 指定，推翻 9×14）。
       ⚠️ 這樣按鈕高只有約 34px，低於 Apple 44pt／Android 48dp 的觸控目標建議。
          外層 .today-stamp-card 整張卡片本身就是同一顆按鈕（點卡片＝進練習），
          所以實際可點範圍遠大於這顆藥丸 —— 按滑掉的風險由那張卡接住。 */
    padding:calc(6px*var(--s,1)) calc(12px*var(--s,1));
    border-radius:10px;
    background:var(--coral);
    color:#fff;
    font-size:calc(var(--fs-body)*var(--s,1));
    font-weight:700;
  }
  .stamp-cta svg{ width:calc(15px*var(--s,1)); height:calc(15px*var(--s,1)); }

  /* ---- 統計細線列 ---- */

  /* ---- 文字連結式入口（小測驗） ---- */
  /* 🔴 **次要按鈕**。2026-09-09 從「純文字列」改成有底色與邊框。

     原本 background:none + border:none，長得跟旁邊的說明文字一模一樣，
     可按提示只有 cursor:pointer —— 觸控螢幕上等於零。
     「小測驗」那一條尤其嚴重：它是入口，卻完全看不出來能按。 */
  .text-link-row{
    position:relative;
    width:100%;
    background:var(--paper-deep);
    border:1.5px solid var(--line);
    border-radius:14px;
    margin:2px 0 8px;
    /* 🔴 左右內距都 14px。右邊那個 › 要跟「我的收藏」的 › 對齊在同一條直線上，
       所以這裡的右內距必須等於 .library-section 的右內距（也是 14px）。
       ⚠️ 兩邊要一起改，改一邊就會歪掉（原本一個 10px、一個 4px，差 6px）。 */
    /* 左 24px：紅槓在 14px（見下面的 ::before），槓寬 3px、槓後留 7px ——
       跟 .library-title 的 inline 槓完全同一組數字，文字才會落在同一條線上。 */
    padding:calc(13px*var(--s,1)) calc(14px*var(--s,1)) calc(13px*var(--s,1)) calc(24px*var(--s,1));
    display:flex;
    align-items:center;
    justify-content:space-between;
    /* 15px：跟 .library-title（我的收藏／獎勵／字庫）同一個數字。
       這幾條在畫面上是同一層級的入口，之前小測驗獨自停在 14px，
       只因為 .row-primary 另外覆寫成 15 —— 是覆寫造成的漂移，不是刻意的階層。
       ⚠️ 「新增今天的字」跟其他列的區別靠底色、圓形圖示與邊框，不靠字級。 */
    /* 🔴 跟 .library-title（我的收藏／獎勵／字庫）同一個數字 —— 18px。
       它們在畫面上是同一層級的入口，Steven 指定要一樣大。
       ⚠️ 改這裡就要改 .library-title，兩邊永遠是同一個值。 */
    font-size:calc(1.125rem*var(--s,1));
    font-weight:600;
    color:var(--ink);
    cursor:pointer;
  }
  /* 按下去要有反應 —— 觸控沒有 hover，:active 是唯一的即時回饋。 */
  .text-link-row:active,
  .today-stamp-card:active,
  .library-more:active,
  .tappable-row:active{ filter:brightness(0.95); }
  .text-link-row svg{ width:calc(15px*var(--s,1)); height:calc(15px*var(--s,1)); color:var(--ink-soft); }

  /* 📌 這裡原本有 `.row-primary > svg{ margin-right:-10px }`（2026-09-10 移除）。

     它是在補償舊的不對稱內距：那時 .text-link-row 的右內距是 4px、
     .row-primary 是 14px，靠負邊距把箭頭拉回來對齊。
     現在兩邊的右內距都統一成 14px 了，這條補償反而讓「新增今天的字」的箭頭
     **多跑出去 10px**（實測 339 vs 其他兩個 329）—— 補償的對象消失了，補償也要跟著拿掉。
     ⚠️ 箭頭對齊現在**只靠內距一件事**：三個容器的 padding-right 都是 14px。 */
  .row-chevron{
    width:calc(15px*var(--s,1));
    height:calc(15px*var(--s,1));
    color:var(--ink-soft);
    margin-left:auto;
    flex:none;
  }

  /* 連結列也帶同一條橘槓 —— 首頁每一塊都要有標記，少一個就會看起來漏掉。
     ⚠️ 必須絕對定位：這一列是 space-between 的 flex，
        用 inline 的 ::before 會變成第三個項目，把文字擠到中間。
     ⚠️「新增今天的字」不用，它已經有橘色圓形加號，兩個標記會打架。 */
  /* 🔴 left 從 4px 改成 14px（2026-09-10）。
     「我的收藏／獎勵／字庫」那三條槓是 inline 的，落在 .library-section 的
     左內距（14px）上；這一條是絕對定位，寫死 4px —— 兩者差 10px，
     所以「小測驗」的槓永遠比下面三條往左凸出去。
     ⚠️ 這個 14 必須等於 .library-section 的 padding-left，一起改。 */
  .text-link-row:not(.row-primary)::before{
    content:'';
    position:absolute;
    left:calc(14px*var(--s,1));
    top:50%;
    transform:translateY(-50%);
    width:3px;
    height:11px;
    border-radius:2px;
    background:var(--coral);
  }

  /* 家長每天的主要動作，重一階。跟「繼續練習」不衝突 ——
     那張卡片是小孩要按的，這一條是大人要按的，兩者的重量不該一樣。 */
  /* 「小測驗」不用粗體（2026-09-10 Steven 指定）。同一列的「新增今天的字」
     是主要動作（有珊瑚底＋圓形圖示），小測驗是次要的，字重上讓一階。 */
  #home-quiz-btn{ font-weight:500; }

  .row-primary{
    background:var(--seal-wash);
    border-color:var(--coral-soft);
    border-radius:14px;
    /* 左 14px：它沒有紅槓（用橘色圓形加號代替），但圓圈的**左緣**要落在
       其他三條紅槓的同一條線上，否則整排入口的左邊會參差。 */
    padding:calc(14px*var(--s,1));
    margin:2px 0 6px;
    /* 字級不在這裡覆寫 —— 跟 .text-link-row 同一個值，留兩份會漂移
       （這一次就是這樣漂出來的：基礎改了、覆寫沒跟著改）。 */
  }
  .row-icon{
    display:flex;
    align-items:center;
    justify-content:center;
    width:calc(26px*var(--s,1));
    height:calc(26px*var(--s,1));
    border-radius:50%;
    background:var(--coral);
    flex:none;
  }
  .row-icon svg{ width:calc(15px*var(--s,1)); height:calc(15px*var(--s,1)); color:#fff; }
  .row-primary:active{ transform:scale(0.99); }

  /* ---- 我的字庫：填補首頁下方，也讓家長一眼看到學過的字 ---- */
  /* 動物收藏／我的獎勵／我的字庫共用。跟上面兩條連結列用同一套節奏：
     上分隔線 + 12px 上內距 + 4px 左右縮排，整頁才會是整齊的橫帶。 */
  .library-section{
    /* 🔴 左右 14px：跟 .text-link-row 同一個數字，三個 › 才會對齊在同一直線上。
       （原本是 4px —— 加了外框之後也顯得太擠。） */
    /* 上下 16/14 → 12/10（2026-09-10）。三個區塊疊起來省 24px，
       剛好讓「全部 N 個字」那顆按鈕出現時首頁仍然裝得下（實測超出 20px）。
       ⚠️ 這是**在 360×800 上**讓它不捲；更矮的機器還是會捲，那沒辦法也不該硬撐。 */
    padding:calc(12px*var(--s,1)) calc(14px*var(--s,1)) calc(10px*var(--s,1));
    /* 🔴 透明外框：只有「我的收藏」是可按的、有 1.5px 實線外框，
       另外兩塊沒有 —— 於是它們的內容比「我的收藏」往左多 1.5px，
       整排標題的紅槓會差 1~2px（實測 31 vs 30）。
       補一條同寬的透明外框，三塊的內部幾何就一致了。 */
    border:1.5px solid transparent;
  }
  /* 區塊標題（動物收藏／我的獎勵／我的字庫共用）。
     加粗、顏色加深：原本是淺灰細字，在一堆內容中間分不出哪裡是段落的開頭。 */
  /* 區塊標題左邊的短豎線：分區靠它，不靠橫線。 */
  .library-title::before{
    content:'';
    display:inline-block;
    width:3px;
    height:calc(11px*var(--s,1));
    margin-right:7px;
    vertical-align:-1px;
    border-radius:2px;
    background:var(--coral);
  }
  /* 🔴 15px 不是「大一點」，是跟 .text-link-row（小測驗／新增今天的字）對齊 ——
     這四個標題跟那兩條是同一層級的入口（整區可點、有箭頭），只是先前被當成
     區塊上方的小標籤（kicker）設計，12px 比自己底下的說明字 13px 還小。
     要改的時候兩個數字一起看：同層級用同一個值，不要各寫各的。（2026-09-09） */
  .library-title{
    font-size:calc(1.125rem*var(--s,1));
    font-weight:700;
    /* 1.5px 是 12px 小標籤的字距，套到 15px 標題會鬆散。 */
    letter-spacing:0.5px;
    color:var(--ink);
    text-transform:uppercase;
    margin-bottom:calc(10px*var(--s,1));
  }
  .library-grid{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .library-chip{
    width:calc(48px*var(--s,1));
    height:calc(48px*var(--s,1));
    display:flex;
    align-items:center;
    justify-content:center;
    background:#fff;
    border:1px solid var(--line);
    border-radius:8px;
    font-size:calc(1.375rem*var(--s,1));
    font-weight:600;
    color:var(--ink);
    /* 🔴 外觀刻意**完全不分**能點與不能點（2026-09-12 Steven 定）。
       第一排以外點下去只是小毫講一句話，不是錯誤、也不是壞掉 ——
       把鎖住的字畫成淡的或虛線，等於在自己的字庫裡標出一堆「你不能用」，
       那是他親手學會的字。分界靠**位置**（第一排）就夠清楚了。 */
    cursor:pointer;
    user-select:none;
    -webkit-user-select:none;
    -webkit-touch-callout:none;
  }
  .library-chip:active{ background:var(--paper); }
  /* 「全部 N 個字」。分隔線在上面，讓它讀起來是「這一區還有更多」，
     不是「另一個功能」。 */
  .library-more{
    display:flex;
    align-items:center;
    justify-content:space-between;
    width:100%;
    margin-top:calc(10px*var(--s,1));
    padding:calc(10px*var(--s,1)) 0 0;
    background:none;
    border:none;
    border-top:1px solid var(--line);
    font-family:inherit;
    font-size:calc(var(--fs-body)*var(--s,1));
    font-weight:600;
    color:var(--coral);
    cursor:pointer;
  }
  .library-more svg{ width:calc(15px*var(--s,1)); height:calc(15px*var(--s,1)); }

  /* 說明字必須小於標題（先前 13px > 標題 12px，階層是反的）。 */
  .library-empty{
    /* 🔴 12px → --fs-body。這幾句（「學會一個字，就能轉一次扭蛋」
       「先學一些字，這裡就會出現你的收藏」）是講給小孩聽的，
       卻曾經是全首頁最小的字 —— 漏網的，不是刻意的階層。 */
    font-size:calc(var(--fs-body)*var(--s,1));
    color:var(--ink-mid);
    padding:8px 0;
  }

  /* ---- Practice stage ---- */
  .stage-wrap{
    width:100%;
    max-width:420px;
    background:#fff;
    border-radius:24px;
    padding:22px 18px 18px;
    box-shadow:0 2px 0 var(--line), 0 14px 30px -18px rgba(43,38,34,0.25);
    display:flex;
    flex-direction:column;
    align-items:center;
  }
  .progress-row{
    width:100%;
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:14px;
    font-size:0.8125rem;
    color:var(--ink-soft);
  }
  .streak{
    display:flex;align-items:center;gap:5px;
    font-weight:600;
    color:var(--coral);
  }
  .streak svg{width:14px;height:14px;flex-shrink:0;}

  /* ---- 田字格：練習跟查筆順共用的同一個框 ----
     🔴 這兩個框是同一種東西，所以只有一份樣式。原本各寫各的，
        結果一個 260 一個 200、邊框 2px 對 1px、圓角 16 對 12，
        連底色都差一階（#FFFDF9 對 #FFFDF8）—— 那不是設計，是各寫各的。
        查筆順甚至整個少了中間那兩條輔助線。
     📌 框是共用的，**裡面的字不是**：createWriter() 依目前的資料庫決定
        中文走 HanziWriter、日文走 KanjiEngine（KanjiVG）。共用的是這個
        田字格，不是筆順資料 —— 兩邊的資料本來就各有各的來源。 */
  .char-box{
    background:
      linear-gradient(var(--paper-deep) 1px, transparent 1px) 0 0/100% 50%,
      linear-gradient(90deg, var(--paper-deep) 1px, transparent 1px) 0 0/50% 100%;
    background-color:#FFFDF9;
    border:2px solid var(--line);
    border-radius:16px;
    box-sizing:border-box;
  }
  .canvas-stage{
    position:relative;
    width:100%;
    max-width:var(--char-box);
    aspect-ratio:1/1;
    margin:0 auto;
  }
  /* 讀音側欄開著時，「字格＋側欄」是一組（比例 1 : 0.4）。
     🔴 窄螢幕整組按比例縮小（字格不小於 200px，見 layoutWriteArea）—— 台灣規格的注音格
        是國字格的 0.4 倍，260px 的字格要配 104px，375px 的手機放不下（2026-09-16 Steven 定）。
     沒開側欄時 .write-row 只有字格，維持原本 260px。 */
  .write-row{ display:flex; gap:8px; align-items:flex-start; justify-content:center; }
  /* 字格與側欄是一組（.write-core）；兩旁是小圖示鍵的欄位。 */
  .write-core{ display:flex; gap:8px; align-items:flex-start; }
  /* 🔴 字格不能被 flex 壓縮 —— 壓縮之後它比側欄矮，兩個框高度對不上（Steven 2026-09-16 看到）。 */
  .write-row .canvas-stage{ flex:0 0 var(--char-box); width:var(--char-box); }
  /* 橫帶（拼音）：側欄在字格**上面**，所以 write-core 變直的、由下往上排。 */
  .write-row.is-above .write-core{ flex-direction:column-reverse; align-items:center; }
  .strip-col{ display:flex; flex-direction:column; gap:8px; align-items:center; }
  .strip-col[hidden]{ display:none; }
  /* 兩旁的工具欄：沒有按鈕時完全不佔寬度（gap 也不會冒出來，因為欄本身是空的）。 */
  .tool-col{ display:flex; flex-direction:column; gap:8px; }
  .tool-col:empty{ display:none; }
  .tool-row{ display:flex; gap:8px; }
  .tool-row:empty{ display:none; }
  /* 🔴 iPad 長按會跳出系統的選字泡泡／放大鏡（2026-09-16 Steven 實機回報）——
     小孩寫慢一點、或停在原地想下一筆，就被系統當成「要選取文字」。
     兩個寫字的框都要關掉 callout 與選取（touch-action 只擋手勢，擋不掉這個）。 */
  /* 🔴 練習畫面**整片**都不准選取（2026-09-16 Steven：「手指連點兩下、第二下不放，會叫出放大鏡」）。
     那個放大鏡是系統的選字鏡 —— 只要畫面上有「可以被選起來的東西」，長按就會叫出它。
     原本只擋兩個寫字框，但提示文字、「這個字已經寫了 N 次」那行也都選得起來，
     手指壓在旁邊照樣跳出來，而且鏡頭會蓋住小孩正在寫的格子。
     練習畫面沒有任何需要選取或複製的內容，整片關掉最乾淨。
     ⚠️ 只限 #practice-view —— 別處（說明、設定、輸入框）要保留選取。 */
  #practice-view, #practice-view *{
    -webkit-touch-callout:none;
    -webkit-user-select:none;
    user-select:none;
  }
  .phonetic-strip{
    flex:0 0 auto;
    width:calc(var(--char-box) * 0.4);
    height:var(--char-box);
    touch-action:none;
  }
  /* 拼音的橫帶是 0.5（四線三格 0.9cm : 田字格 ~1.8cm），直條是 0.4 —— 見 practice.js 的 STRIP_RATIO_*。 */
  .write-row.is-above .phonetic-strip{ width:var(--char-box); height:calc(var(--char-box) * 0.5); }
  .phonetic-strip[hidden]{ display:none; }
  /* ↶ 復原：放在田字格**正下方**那一列，不放兩側 —— 兩側都要留給讀音側欄
     （2026-09-16 Steven；側欄寬度是字格的 0.4 倍，擠不下按鈕）。
     這一列平常是空的、不佔高度；有按鈕時才撐開。 */
  /* ↶ 復原：擺進按鈕列當一顆方形小鍵，**不佔額外高度**（矮螢幕會擠到小毫）。
     flex:0 0 auto 讓它不跟主要按鈕搶寬度。 */
  .undo-stroke-btn{
    flex:0 0 auto; width:48px; height:48px;
    border-radius:14px; border:1.5px solid var(--line); background:#fff; color:var(--ink);
    display:flex; align-items:center; justify-content:center; cursor:pointer;
  }
  .undo-stroke-btn[hidden], .btn[hidden]{ display:none; }
  /* 沒有讀音格時 ↶ 住在下面那一列：高度跟旁邊的按鈕一樣高（見 practice.js 的 placeTools）。 */
  .controls .undo-stroke-btn{ height:auto; align-self:stretch; }
  /* 🔴 **沒作用時留空位，不要消失**（2026-09-16 Steven：「這個要跳來跳去嗎？」）。
     3–8 歲記的是按鈕的**位置**不是字 —— 按鈕一移動，熟練度就歸零。
     `hidden` 用在「這個模式根本沒有這顆」（小測驗沒有復原、沒開讀音格就沒有那兩顆），
     `.is-idle` 用在「這顆現在按了沒用」（還沒寫任何一筆）—— 後者只是隱形，格子還在。 */
  .undo-stroke-btn.is-idle{ visibility:hidden; }
  .undo-stroke-btn svg{ width:22px; height:22px; }
  /* 🔴 練習畫布連捲動都不能有：body 是 pan-y（要留給面板捲動），但在畫布上
     由上往下寫一筆會被當成捲頁面，字就寫不出來。
     日文引擎自己設了 svg.style.touchAction=none，中文用的是 HanziWriter 產生的
     SVG、它不會設 —— 所以在容器這一層統一擋掉，兩種資料庫都涵蓋。
     （touch-action 不會繼承，父層設了不代表子元素有。） */
  /* 🔴 touch-action 不會繼承 —— 而且瀏覽器判斷手勢時看的是「手指實際命中的那個
     元素」，不是它外面那顆按鈕。

     2026-08-29 iPad 實機回報：改成在 button 上設 pan-y 之後，「開始練習」正常，
     但「再看一次」連續點還是偶爾會放大 —— 因為那顆按鈕裡面有 <svg> 圖示和
     <span>，手指點在圖示上時命中的是 <svg>，它自己仍然是 auto。
     「偶爾」正是落點決定的：點到按鈕邊緣沒事，點到圖示就放大。

     所以改用 * 當底線。列清單的寫法每加一個新元件就會再漏一次，而漏掉的症狀
     是「偶爾放大」—— 最難查的那種。* 是唯一不會漏的寫法，而且它是最不specific
     的選擇器，任何寫得更明確的規則（例如下面畫布的 id）都蓋得過它。
     （麻將接龍在 PROPOSALS.md #9 的回應裡用的是同一招，已實測。） */
  *{ touch-action:pan-y; }

  /* 🔴 練習中：把整個 App 區域的瀏覽器手勢**全部關掉**（2026-09-09，iPad 實測）。
     症狀：一隻手指按在格子外，就「怎麼寫都寫不對」——因為第二個觸點會讓 Safari
     判定成多點手勢（捲動／捏合），於是把畫布上正在寫的那隻指標搶走或取消，
     每一筆都因此壞掉。畫布自己雖然已經是 touch-action:none，但**手指按的是框外**，
     命中的是別的元素，所以擋不到 —— 要連框外那片區域一起關才有用。
     🟢 練習畫面本來就不需要捲動（版面固定、不長），關掉的代價幾乎是零。
     ⚠️ 面板 `.sheet-overlay` 是 `#app` 的**兄弟節點**，不在這個範圍內，照樣能捲。
     ⚠️ 連子元素一起寫死：瀏覽器判斷手勢時看的是「手指實際命中的那個元素」，
        只設在 #app 上擋不住（同上方那條註解的教訓）。
     ⚠️ 桌機滑鼠只有一個指標，這個 bug 桌機永遠重現不了。 */
  #app.writing, #app.writing *{ touch-action:none; }

  /* 按鈕裡的圖示與文字不吃事件 —— 手指碰到它們時，瀏覽器直接把命中目標算成
     按鈕本身。這是比 touch-action 更根本的一層：裝飾元素本來就不該是事件目標，
     順便讓「點了圖示卻沒反應」這類問題不可能發生。 */
  button svg,
  button span{ pointer-events:none; }

  #char-canvas-holder,
  #char-canvas-holder svg{ touch-action:none; }

  /* 🔴 田字格長什麼樣只寫在 .char-box 一個地方（見上面 :root 的說明）。
     這裡只留兩件外觀管不到的事：填滿 .canvas-stage、寫字時不准捲動。 */
  #char-canvas-holder{
    width:100%;
    height:100%;
  }

  /* 漫畫式對話框（共用語言）。
     用例：小毫在練習畫面說的話（.hint-banner）、導覽泡泡（#tour-bubble）、喚醒提醒。
     📌 2026-09-10 之前還有一個 .mistake-bubble，已併進小毫的泡泡。
     🔑 尖角一定要指向某個東西 —— 浮在空白處、尖角指著空氣的泡泡看起來像沒做完。 */
  .speech-bubble{
    position:relative;
    background:#fff;
    border:1.5px solid var(--coral-soft);
    color:var(--ink);
    border-radius:16px;
    padding:10px 14px 8px;
    box-shadow:0 6px 16px -8px rgba(43,38,34,0.3);
    /* 泡泡裡講話的是小毫，聽的是小孩 → 用 --fs-kid。原本 13px。 */
    font-size:var(--fs-kid);
    line-height:1.5;
  }
  /* 🔴 只給日文：日文可以在假名之間任意斷行，泡泡裡就把「ごほうび」
     從中間切成「ごほ／うびは」（2026-09-12 實測）。keep-all 讓它只在
     空格和頓號處斷，斷點就落在詞的邊界。
     ⚠️ **不能全域套** —— 中文漢字之間沒有空格，keep-all 會讓整句不能斷、
        直接溢出泡泡。所以綁在 html[lang="ja"] 上（見 app.js 的 useLang）。 */
  html[lang="ja"] .speech-bubble{
    word-break:keep-all;
  }

  .speech-bubble::after{
    content:'';
    position:absolute;
    width:10px;height:10px;
    background:#fff;
    border-right:1.5px solid var(--coral-soft);
    border-bottom:1.5px solid var(--coral-soft);
    transform:rotate(45deg);
  }

  /* 📌 喚醒提醒的浮動泡泡（.revive-bubble）與 .bubble-close 已移除（2026-09-15），
     改由首頁小毫的泡泡問（見 .home-bubble.is-asking）。.bubble-cta 導覽還在用。 */
  .bubble-cta{
    color:var(--coral);
    font-weight:600;
    cursor:pointer;
  }

  /* 📌 這裡原本有 .mistake-bubble 的整套樣式（2026-09-10 移除）——
     寫錯的安慰話改走小毫的泡泡（.hint-banner），不再另開一個。 */
  .controls{
    width:100%;
    display:flex;
    gap:10px;
    margin-top:18px;
  }
  .btn{
    flex:1;
    border:none;
    border-radius:14px;
    padding:13px 10px;
    font-size:0.9375rem;
    font-weight:600;
    cursor:pointer;
    display:flex;align-items:center;justify-content:center;gap:6px;
  }
  .btn-primary{
    background:var(--coral);
    color:#fff;
  }
  .btn-secondary{
    background:var(--paper-deep);
    color:var(--ink);
  }
  /* 停用時要「看起來就是不能按」。原本只有面板裡的按鈕有停用樣式，
     練習畫面這顆沒有——重複練習階段它是停用的，但看起來是完全正常的橘色按鈕，
     小孩會一直按、一直沒反應，不知道是壞了還是自己按錯。 */
  .btn:disabled{
    background:var(--paper-deep);
    color:var(--ink-soft);
    cursor:not-allowed;
  }
  .btn:disabled:active{ transform:none; }
  /* 「再看一次」的短暫鎖定用 class，不用 disabled ——
     停用的按鈕收不到事件，連點期間就看不到使用者還在點（見 app.js tapReplay）。 */
  .btn.is-locked{
    background:var(--paper-deep);
    color:var(--ink-soft);
    /* 只換文字顏色的話，這顆本來就是淺底的次要按鈕看起來幾乎沒變 ——
       連點時要一眼看得出「現在鎖住了」，所以再壓一次透明度。 */
    opacity:.55;
  }
  .btn.is-locked:active{ transform:none; }
  .btn:active{ transform: scale(0.97); }

  /* 練習提示：漫畫式對話框，**浮在畫布右上角、不佔版面**。
     說話的是「這個字」本身，所以尖角朝左下指向它。

     ⚠️ 一度做成排版內、尖角朝上的橫幅 —— 那不對：它佔位置，而且尖角朝上
        等於是 App 在說話。這裡的語意是字自己在講自己的狀況。

     📌 跟 .mistake-bubble 同一個角落，z-index 比它低 ——
        小孩寫錯時那顆會蓋過來，那是對的：緊急的訊息本來就該蓋過環境訊息。
        刻意不做互斥邏輯（多一段狀態就多一個會忘記重設的地方，見踩過的坑 #10）。 */
  /* 🔴 2026-09-10 從「浮在田字格右上角的小標籤」改成「小毫說的話」。

     原本它絕對定位在田字格上緣外側、寬度限 58%、12.5px 淡灰 ——
     小孩寫字時眼睛要在「田字格」和「角落的小字」兩處來回，而且那行字
     根本沒人在講，就是一段旁白。
     現在改成畫面下方的浮動泡泡，尾巴指著左下角的小毫 —— 話有人講了。
     ⚠️ fixed 不是 absolute：它要浮在畫面上，不跟著練習卡片捲動。
     ⚠️ 擋到一點內容沒關係（Steven 定案），所以不必替它保留版面空間。 */
  .hint-banner{
    /* 🔴 位置錨在**練習卡片的下緣**（--practice-anchor，由 showPracticeView 量），
       不是釘在視窗底部。釘視窗底部時卡片和泡泡中間會空一大塊，兩者看起來
       不相干（Steven 回報：「改成這樣幹嘛」）。
       ⚠️ 寬度**不要撐滿整行** —— 對話框撐滿就不像有人在講話，像系統橫幅。 */
    position:fixed;
    left:calc(var(--practice-left, 0px) + var(--mascot-size) * 0.62);
    /* 🔴 泡泡要**包住文字**，不是撐到右邊（2026-09-10）。

       原本是 right:14px（+ max-width 上限）：上限只讓它停在 406px，
       短短一句「看完動畫，輪到你畫畫看」照樣被拉成一長條（Steven 回報兩次）。
       改成 width:max-content 才是「有人在說話」的形狀。
       ⚠️ right 必須是 auto —— 留著 right:14px 會逼它兩端對齊，max-content 失效。

       上限同時擋兩件事：太長的句子（日文最長），以及撞到右邊界。
       算式＝視窗寬 − 小毫佔掉的左邊 − 右邊留白。 */
    right:auto;
    width:max-content;
    max-width:min(406px, calc(100vw - var(--practice-left, 0px) - var(--mascot-size) * 0.62 - 14px));
    /* 🔴 泡泡的底要落在小毫**頭頂上方**，不能壓到他。

       圖檔是 300px 見方、角色置中，四周有透明留白 —— 角色的頭頂大約在
       圖高的 13% 處，不是 0%。所以他的頭頂 ≈ 底部 6px + size × 0.87。
       原本寫 0.62 是照「整張圖」估的，結果泡泡壓在他的筆尖上（Steven 回報）。
       ⚠️ 這個 0.87 綁定圖檔的構圖。之後換圖如果角色框得更滿，要重新量。 */
    /* 🔴 泡泡往右上移、**疊在寫字卡片上**（2026-09-10 Steven 指定，
       「疊到寫字框不會怎樣」）。用 bottom 定位讓它往上長 ——
       文字變多時往卡片方向長，不會把下面的小毫推走。
       算式：他的頭頂在 anchor + size×0.13 附近，泡泡底就停在那條線上方一點。 */
    top:auto;
    /* 🔴 用量出來的 --practice-bottom，**不要寫 calc(100vh - anchor)**。
       anchor 是真實座標、100vh 是「工具列隱藏時」的高度，兩個座標系相減
       會在每台裝置差一個工具列的高度（手機／電腦／iPad 各跑掉不同的量）。
       ⚠️ 凡是要跟量出來的座標相加減，就不要混任何 vh。 */
    bottom:calc(var(--practice-bottom, 20vh) - var(--mascot-size) * 0.20);
    z-index:41;
    /* 右上角整塊讓給泡泡了（「N 個字已學會」已移除，字庫就看得到那個數字）。
       但寬度仍然要收 —— 蓋住太多字，小孩就看不到自己要寫什麼。 */
    /* 🔴 放寬到 72%：字級從 12.5px 拉到 --fs-kid(20px)，58% 會斷成太多行。
       這是小孩寫字時**唯一**的指示，卻曾經是全 App 最小的字之一。 */
    text-align:left;
    font-size:var(--fs-kid);
    line-height:1.4;
    padding:8px 12px;
    /* 顏色從 --ink-soft 改成 --ink：指示要看得清楚，不是註腳。 */
    color:var(--ink);
    transition:opacity .2s;
  }
  .hint-banner:empty{ display:none; }
  /* 尾巴在左下角朝下，指著站在左下方的小毫。
     邊框沿用 .speech-bubble::after 的預設（右+下轉 45°＝朝下），不要覆寫。 */
  .hint-banner::after{ bottom:-6px; left:16px; }

  /* 🔴 首頁的小毫：固定在右下角、身體被畫面邊緣切掉一截（探頭感）。

     為什麼是探頭不是站好：探頭佔的面積最小、陪伴感最強，而且不管首頁
     捲到哪裡他都在。站好的話要嘛佔掉一大塊，要嘛捲到底才看得到。
     ⚠️ pointer-events:none —— 他是裝飾，小孩點到他必須毫無反應
        （導覽那層踩過同一個坑）。
     ⚠️ 練習畫面自己有一個小毫，兩個同時出現會很怪 —— 用 body.in-practice 藏掉。
        那個旗標是今天為了藏品牌字加的，剛好共用。
     📌 圖用揮手那張（master 本人），不是任何一個表情圖 —— 首頁沒有特定情境，
        中性的打招呼最耐看。 */
  /* 裁切層：跟視窗一樣大、overflow:hidden。小毫超出的部分被它切掉，
     不會撐大文件高度（見 index.html 的說明）。 */
  .mascot-layer{
    position:fixed;
    top:0;
    bottom:0;
    /* 🔴 跟著**內容欄**走，不是跟著視窗（2026-09-10）。

       原本 inset:0＝整個視窗，手機上內容本來就滿版所以看不出問題；
       桌機／寬螢幕的內容是置中的 620px 欄，小毫就被留在視窗最右下角，
       離他要陪伴的內容十萬八千里（Steven 用瀏覽器開發現）。
       ⚠️ 跟練習畫面那個小毫**同一個坑**：貼視窗還是貼內容。
          浮動元素預設貼視窗，但這個 App 的內容是置中固定寬的。

       652 = 內容 620 + #app 左右內距各 16。窄螢幕上 652 比視窗寬，
       max-width 自動退回 100%，行為跟以前一模一樣。 */
    left:50%;
    transform:translateX(-50%);
    width:100%;
    max-width:652px;
    overflow:hidden;      /* 小毫超出的部分被切掉 → 探頭感，也不會撐大文件 */
    pointer-events:none;
    z-index:5;
  }
  /* 首頁小毫講話用的泡泡。平常 display:none，要說話時才出現（app.js 的 sayOnHome）。
     📌 貼在小毫**左上方** —— 他站在右下角，泡泡往左上長才不會被裁切層切掉。
     ⚠️ .mascot-layer 是 pointer-events:none，所以這顆泡泡不會擋到底下的字庫。 */
  .home-bubble{
    position:absolute;
    /* 小毫站在右下角（right:-26px、bottom:-18px、寬 132）。
       泡泡放在他的**左上方**，尾巴從右下角斜指過去（Steven 2026-09-12 指定）。 */
    right:104px;
    bottom:92px;
    width:max-content;
    max-width:230px;
    z-index:6;
  }
  /* 尾巴在泡泡的右下角，朝右下方指著小毫的頭。
     共用的 ::after 用 border-right + border-bottom 轉 45°，本來就是指右下。 */
  /* 小毫在問問題（喚醒提醒）：泡泡裡有兩顆按鈕，要收得到點擊。
     平常講一句話時維持 pointer-events:none（沿用外層），不擋底下的字庫。 */
  .home-bubble.is-asking{ pointer-events:auto; max-width:250px; }
  .home-ask-actions{ display:flex; justify-content:flex-end; gap:8px; margin-top:8px; }
  .home-ask-actions button{
    border:none; border-radius:99px; padding:7px 14px; cursor:pointer;
    font-size:0.875rem; font-weight:600; font-family:inherit;
  }
  .home-ask-actions .is-yes{ background:var(--coral); color:#fff; }
  .home-ask-actions .is-no{ background:var(--paper-deep); color:var(--ink); }
  .home-bubble::after{
    right:-6px;
    bottom:14px;
  }
  #home-mascot{
    position:absolute;
    /* 🔴 小孩一定會去戳他，所以他要收得到點擊（外層 .mascot-layer 是 pointer-events:none，
       那是為了不擋住底下的內容；這裡單獨開回來）。點一下他就講一句使用說明。 */
    pointer-events:auto;
    cursor:pointer;
    right:-26px;          /* 往外推，讓身體被邊緣切掉一截 */
    bottom:-18px;
    width:132px;
    height:auto;
    opacity:.92;
    /* 📌 這裡試過加「呼吸」動畫（3.4s 上下 3px），Steven 否決 ——
       首頁本來就有很多東西要看，多一個一直在動的元素只是干擾。
       **不要再加回來。** */
  }
  /* 練習畫面自己有小毫；導覽進行中泡泡裡也有一個 —— 兩種情況都先收起來，
     不然畫面上會同時有兩個他。導覽結束（endTour）右下角這個才出場。 */
  body.in-practice #home-mascot,
  body.in-intro #home-mascot{ display:none; }

  /* 🔴 「這一趟的收穫」：抽到的圖案排在小毫右邊、泡泡下面那塊空區
     （實測 360×800 上有 200×128px）。

     為什麼不放小毫下面：那裡只有 88px，而且很多 Android 有手勢列會切到。
     放右邊還有一個好處 —— 小毫在左邊站著看，收穫排在他旁邊，讀起來是一組。 */
  .practice-haul{
    position:fixed;
    /* 🔴 起點要在小毫**右邊**（0.82，不是 0.62）。

       0.62 是泡泡的起點 —— 泡泡在他頭頂上方，跟他不同高度，重疊沒關係。
       但這條收穫跟他**同高**，用 0.62 的話前一格半會被他擋住：
       只抽到一隻時那一隻整個藏在他身後，畫面上什麼都沒有（Steven 回報
       「沒有出現河馬」）。抽多隻時後面幾隻排到他右邊才看得到，
       所以**測 5 隻、12 隻都會過，只有 1 隻才會現形** —— 我漏掉的就是這個。
       0.82 ≈ 圖檔裡角色身體的右緣（四周有透明留白，不是 1.0）。
       ⚠️ z-index 提到 42（小毫 40、泡泡 41）：萬一之後尺寸再調而重疊，
          寧可蓋住小毫，也不要讓獎勵消失。 */
    left:calc(var(--practice-left, 0px) + var(--mascot-size) * 0.82);
    top:calc(var(--practice-anchor, 60vh) + var(--mascot-size) * 0.20 + 12px);
    width:max-content;
    max-width:min(406px, calc(100vw - var(--practice-left, 0px) - var(--mascot-size) * 0.82 - 14px));
    display:flex;
    flex-wrap:wrap;
    gap:6px;
    z-index:42;
    pointer-events:none;
  }
  .practice-haul:empty{ display:none; }
  /* 🔴 跟首頁「我的收藏」完全同一套外觀（48px 圓底＋白底＋細框）。
     兩處是同一種東西（收集到的圖案），長得不一樣的話小孩要重新認一次。
     ⚠️ 改這裡就要改 .collection-thumb，兩邊永遠是同一組數字。 */
  .practice-haul .haul-cell{
    flex:0 0 auto;
    width:calc(48px*var(--s,1));
    height:calc(48px*var(--s,1));
    border-radius:50%;
    background:#fff;
    border:1px solid var(--line);
    overflow:hidden;
    animation:haulPop .32s cubic-bezier(.3,1.4,.6,1);
  }
  .practice-haul .haul-cell img{ width:100%; height:100%; display:block; }
  @keyframes haulPop{ from{ transform:scale(0.4); opacity:0; } to{ transform:none; opacity:1; } }
  /* 超過 8 個就不再往下長，最後一格改成「+N」—— 再多會擠出畫面。 */
  .haul-more{
    display:flex;
    align-items:center;
    justify-content:center;
    width:calc(48px*var(--s,1));
    height:calc(48px*var(--s,1));
    border-radius:50%;
    background:var(--paper-deep);
    border:1px solid var(--line);
    color:var(--ink-mid);
    font-size:var(--fs-label);
    font-weight:700;
  }

  /* 練習畫面的小毫。z-index 40：在內容之上、在提示泡泡(41)之下。 */
  #practice-mascot{
    position:fixed;
    left:var(--practice-left, 0px);
    top:var(--practice-anchor, 60vh);
    width:var(--mascot-size);
    height:auto;
    z-index:40;
    pointer-events:none;   /* 不能擋到手指寫字 */
    display:none;          /* 只有練習畫面顯示，見 body.in-practice */
  }
  body.in-practice #practice-mascot{ display:block; }

  .practice-record{
    margin-top:10px;
    width:100%;
    text-align:center;
    font-size:0.75rem;
    color:var(--ink-soft);
    min-height:16px;
  }

  /* ---- Today list FAB & panel ---- */
  .sheet-overlay{
    position:fixed;inset:0;
    background:rgba(43,38,34,0.4);
    display:none;
    align-items:flex-end;
    justify-content:center;
    z-index:100;
  }
  .sheet-overlay.open{display:flex;}
  /* 底部不留內距：關閉按鈕是sticky釘在最下面的，只要面板底下還有內距，
     那條縫就會透出正在捲動的內容（用box-shadow去補會蓋到相鄰按鈕，不可靠）。
     讓按鈕直接貼齊面板底緣，縫就不存在。 */
  .sheet{
    width:100%;
    max-width:460px;
    background:var(--paper);
    border-radius:24px 24px 0 0;
    padding:22px 20px 0;
    /* 跟 #app 同一個 iOS 陷阱：78vh 是「工具列隱藏時」的 78%，實際可視區更小，
       面板會比預期高、底部可能被工具列蓋住。補 dvh（舊瀏覽器忽略、沿用 vh）。 */
    max-height:78vh;
    max-height:78dvh;
    overflow-y:auto;
  }
  /* 扭蛋機面板的按鈕一開始是隱藏的（要轉完才出現），
     底部沒內距的話文字會貼到螢幕邊緣，這裡補回來 */
  .gacha-sheet{ padding-bottom:24px; }
  .sheet h3{
    margin:0 0 4px;
    font-size:1.125rem;
  }
  .sheet p.sub{
    margin:0 0 16px;
    font-size:0.8125rem;
    color:var(--ink-soft);
  }
  /* 說明面板的段落：五段各自摺疊，點標題才展開。
     內容是純文字（textContent），換行靠 pre-wrap 保留 ——
     用 <br> 就得改走 innerHTML，四種語言的文案裡任何一個 < 都會變成破版。 */
  /* 練習畫面的「複習」標記。刻意做得小而安靜 —— 它是說明不是警告，
     用醒目的顏色會讓小孩以為自己做錯了什麼。 */
  /* 徽章可以點開看那一輪的收藏。游標與輕微放大是唯一的可點提示 ——
     再多加邊框或箭頭會把徽章牆變得吵雜，那面牆的重點是成果不是操作。 */
  .badge-clickable{ cursor:pointer; transition:transform .15s ease; }
  .badge-clickable:active{ transform:scale(0.94); }
  /* 正在收的這一輪：黑色剪影，排在已拿到的徽章後面。
     🔴 要夠黑。第一版用 opacity(.22)，出來是一個很淡的灰圓餅 ——
     看起來像圖沒載出來，不像剪影。深淺在這裡不是美感問題，是語意問題：
     讀得出「這是一個還沒填的位置」才算數。
     📌 徽章可以直接 brightness(0)，收藏圖不行 —— 徽章 SVG 的畫布是透明的，
     塗黑就是形狀；收藏圖的紙圓被烤進檔案裡，塗黑會連圓一起黑成一坨。
     ⚠️ 不給 cursor:pointer —— 它不可點，游標不能暗示相反的事。 */
  .badge-in-progress{ filter:brightness(0) opacity(0.8); }


  /* 徽章說明現在有兩行（已完成 N 輪＋點徽章可以看），要保留換行 */
  .badge-caption{ white-space:pre-line; }

  /* 清除學習紀錄：跟「全部清除」一樣是逃生門，不是主要動作。
     多加一點上緣間距，跟上面的獎勵設定分開，避免手指滑過去誤觸。 */
  .reset-data-link{ margin-top:18px; }

  /* 範圍篩選：沿用設定裡遍數選項的樣式（.repeat-options），不新增設計語言。
     四顆會比三顆擠，字級略縮。 */
  /* 標題列上的下拉：視覺重量要跟旁邊的「最近學會的」一樣輕，
     它只是篩選，不該比主要區塊還醒目。 */
  .inline-select{
    border:none;
    background:transparent;
    color:var(--ink-soft);
    font-family:inherit;
    /* 🔴 --fs-body（16px）：跟說明文字同一個尺寸（2026-09-10 Steven 指定）。
       「全部 ∨」「最近學會的」是家長每次看字庫都會用到的控制項，
       不是註腳 —— 註腳才用 --fs-label。 */
    font-size:calc(var(--fs-body)*var(--s,1));
    padding:2px 2px 2px 6px;
    margin-left:auto;
    margin-right:10px;   /* 跟旁邊的排序切換分開，不然兩個小字會黏在一起 */
    cursor:pointer;
  }
  /* 自訂日期：用瀏覽器內建的 <input type="date">，iOS 會跳原生滾輪，
     不用寫日曆、不用外部套件（符合本專案零相依的原則）。 */
  .range-custom{
    display:flex;
    align-items:center;
    gap:8px;
    margin-bottom:12px;
  }
  .range-custom input{
    flex:1;
    min-width:0;
    padding:8px 10px;
    border:1px solid var(--paper-deep);
    border-radius:10px;
    background:#fff;
    color:var(--ink);
    font-size:0.8125rem;
    font-family:inherit;
  }
  .range-dash{ color:var(--ink-soft); }

  /* 卡片右側的「查筆順」入口：一個放大好幾倍的放大鏡圖示。

     不做成有邊框的小膠囊 —— 那樣它跟旁邊的印章、進度擠成一團小東西。
     圖示本身夠大就看得懂，也填得住卡片右邊那塊空白。
     ⚠️ 它疊在一張本身可點的卡片上，事件冒泡要擋（見 app.js）。 */
  .card-lookup-btn{
    position:absolute;
    right:22px;
    /* 垂直置中，跟左邊那個大字對齊 —— 靠上時它跟進度擠在同一側的上下，
       置中之後左右各一個主體（字、放大鏡），版面才平衡。
       ⚠️ 進度在卡片底部，所以置中時要避開它：位移一點點往上。 */
    /* 📌 2026-09-10 從 -58% 改回 -50%：那 8% 是為了避開卡片右下角的進度列，
       而進度列已經整條拿掉了，偏移沒有理由再留著。 */
    top:50%;
    transform:translateY(-50%);
    display:flex;
    align-items:center;
    justify-content:center;
    /* 🔴 給它一個圓框（2026-09-10）。兩個理由：
       ① 原本是一個沒有邊界的 60px 大圖示，跟左邊的大字互相搶主角；
          縮小＋收進圓框之後，主角只剩那個字。
       ② 裸圖示看不出能按 —— 圓框是圖示按鈕的通用語彙，而且不靠游標。 */
    width:calc(76px*var(--s,1));
    height:calc(76px*var(--s,1));
    padding:0;
    border:1.5px solid var(--line);
    border-radius:50%;
    background:var(--paper);
    color:var(--coral);
    cursor:pointer;
  }
  /* 60px → 24px：外面那個 46px 的圓框才是按鈕的大小，圖示只是內容。 */
  .card-lookup-btn svg{ width:calc(38px*var(--s,1)); height:calc(38px*var(--s,1)); }
  .card-lookup-btn:active{ transform:translateY(-58%) scale(0.94); }   /* 按下去的縮放要保留置中的位移 */

  /* ---- 速查表 ---- */
  .lookup-stage{
    margin-top:14px;
    text-align:center;
  }
  /* ⚠️ 不要寫死像素（原本是 200×200，面板最寬有 420，字擠在中間一小塊）。
     吃滿寬度、用 max-width 收邊、aspect-ratio 保持正方形。外觀在 .char-box。 */
  #lookup-canvas{
    width:100%;
    max-width:var(--char-box);
    aspect-ratio:1/1;
    margin:0 auto;
  }
  /* 🔴 這兩行要**先佔好位置**，即使還沒查任何字。
     面板是靠底部對齊的，內容一長高，整張就往上跳 —— 實測打完字之後
     跳 60px（字元標籤 36 + 筆數 17 + 間距）。使用者正在打字，畫面卻在動，
     那是最容易讓人失去方向的一種抖動。
     空的時候撐住高度，字填進來時版面完全不動。 */
  #lookup-chips{ min-height:36px; }
  .lookup-progress{ min-height:17px; }

  .lookup-progress{
    margin:10px 0 8px;
    font-size:0.8125rem;
    color:var(--ink-soft);
  }
  .lookup-controls{
    display:flex;
    gap:8px;
    margin-bottom:4px;
  }
  /* 三顆控制鍵比練習畫面的主要按鈕小一階 —— 這是工具不是主要流程 */
  .lookup-controls .btn{ padding:10px 6px; font-size:0.875rem; }

  /* 小孩自己挑筆色與粗細。放在原本「第 N 個字」那一行，左粗細、右顏色。
     🔴 **整排跟著這一列的寬度縮放**（2026-09-19 Steven：「粗細、顏色那一條沒有隨螢幕大小調整」）。
        原本全是寫死的像素（圖示 18px、間距 8px），一排 11 顆約 370px ——
        375px 手機上卡片裡只有 ~307px，最右邊的綠色會被切掉；iPad 上又小得像芝麻。
        現在所有尺寸都是 --pen-u 的倍數，--pen-u 由 practice.js 的 sizePenPicker() 照這一列的寬度算
        （夾在 13～26px）。18px 是還沒量到寬度之前的預設值。 */
  .progress-row{ --pen-u:18px; }
  .pen-widths, .pen-colors{ display:flex; align-items:center; gap:calc(var(--pen-u) * 0.44); }
  .pen-widths button, .pen-colors button{
    border:none; background:none; padding:calc(var(--pen-u) * 0.22); cursor:pointer;
    display:flex; align-items:center; justify-content:center;
    border-radius:50%;
  }
  /* 粗細：五條長度一樣、粗細不同的短線（線的粗細由 JS 照 --pen-u 等比給） */
  .pen-widths i{ display:block; width:var(--pen-u); border-radius:99px; background:var(--ink-soft); }
  .pen-widths button.on i{ background:var(--ink); }
  .pen-widths button.on{ background:var(--paper-deep); }
  /* 顏色：五顆圓點，選中的那顆外面套一圈 */
  .pen-colors i{ display:block; width:var(--pen-u); height:var(--pen-u); border-radius:50%;
                 box-shadow:inset 0 0 0 1.5px rgba(33,28,24,.18); }
  .pen-colors button.on{ box-shadow:0 0 0 2px var(--ink); }
  /* 留下我寫的筆跡：跟粗細、顏色同一行，選中時跟粗細一樣墊一塊底色 */
  .my-stroke-toggle{ border:none; background:none; padding:calc(var(--pen-u) * 0.22); cursor:pointer; border-radius:50%;
                     display:flex; align-items:center; justify-content:center; color:var(--ink-soft); }
  .my-stroke-toggle svg{ width:calc(var(--pen-u) * 1.1); height:calc(var(--pen-u) * 1.1); display:block; }
  .my-stroke-toggle.on{ background:var(--paper-deep); color:var(--ink); }
  /* 習字簿：全螢幕，一頁剛好一個螢幕大，多頁上下滑、一次停一頁。
     z-index 高過面板（100）、低過星星特效（200）。 */
  .writing-book-overlay{ position:fixed; inset:0; z-index:150; background:#fff; display:none; }
  .writing-book-overlay.open{ display:block; }
  /* 換頁是**左右翻**，一次停一頁。直式本子的容器是 dir=rtl（第一頁在最右邊、往左翻），
     ⚠️ 頁面本身要改回 ltr —— SVG 的 text-anchor 會跟著 direction 反過來，表頭會跑位。 */
  .writing-book-pages{ position:absolute; inset:0; display:flex; overflow-x:auto; overflow-y:hidden;
                       scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch; scrollbar-width:none; }
  .writing-book-pages::-webkit-scrollbar{ display:none; }
  .writing-book-page{ flex:0 0 100%; height:100%; scroll-snap-align:start; direction:ltr; }
  .writing-book-page svg{ display:block; }
  .writing-book-turn{ position:absolute; bottom:max(2.5vmin, env(safe-area-inset-bottom));
                      width:max(44px, 9vmin); height:max(44px, 9vmin); border-radius:50%;
                      border:1.5px solid var(--line, #E3D7C2); background:#fff; color:var(--ink);
                      font-size:max(26px, 6vmin); line-height:1; cursor:pointer; }
  .writing-book-turn.is-left{ left:4vmin; }
  .writing-book-turn.is-right{ right:4vmin; }
  .writing-book-turn:disabled{ opacity:.25; cursor:default; }
  /* 「好了」浮在頁尾那一段留白裡（版面有預留，不蓋到格子） */
  .writing-book-close{ position:absolute; left:50%; transform:translateX(-50%);
                       bottom:max(2.5vmin, env(safe-area-inset-bottom)); padding:1.6vmin 9vmin;
                       border:none; border-radius:99px; background:var(--coral); color:#fff;
                       font-size:max(15px, 3.6vmin); font-weight:700; cursor:pointer;
                       box-shadow:0 2px 8px rgba(43,38,34,.2); }


  .info-section{
    border-bottom:1px solid var(--paper-deep);
  }
  .info-section:last-of-type{ border-bottom:none; }
  .info-section summary{
    /* 拿掉瀏覽器預設的三角形，自己畫一個右側的箭頭 ——
       預設樣式各家長得不一樣，而且跟這個 App 的紙感調性不合。 */
    list-style:none;
    cursor:pointer;
    padding:13px 24px 13px 0;
    position:relative;
    font-size:0.875rem;
    font-weight:600;
    color:var(--ink);
  }
  .info-section summary::-webkit-details-marker{ display:none; }
  .info-section summary::after{
    content:'';
    position:absolute;
    right:6px;
    top:50%;
    width:7px; height:7px;
    border-right:2px solid var(--ink-soft);
    border-bottom:2px solid var(--ink-soft);
    transform:translateY(-70%) rotate(45deg);
    transition:transform .18s ease;
  }
  .info-section[open] summary::after{ transform:translateY(-30%) rotate(-135deg); }
  /* 付費牆的小字功能列。跟說明面板的段落長得像，但不共用 class ——
     說明面板那個現在是可摺疊的 <details>，共用會讓付費牆莫名多一條框線。 */
  .paywall-features{
    margin:0 0 4px;
    font-size:0.8125rem;
    line-height:1.75;
    color:var(--ink-soft);
    /* 文案裡的 \n 要真的換行。沒有這行的話「包含…」和「⚠️ 不含收藏組」
       會被摺成同一段，兩件相反的事讀起來像同一句。 */
    white-space:pre-line;
  }
  .info-section p{
    margin:0 0 16px;
    font-size:0.8125rem;
    line-height:1.75;
    color:var(--ink-soft);
    /* pre-wrap 不是 pre-line：授權那段的第二行是縮排的續行
       （字型來源、網址），pre-line 會把行首空白吃掉，續行就跟項目對齊了。 */
    white-space:pre-wrap;
  }
  .input-row{
    display:flex;
    gap:8px;
    margin-bottom:16px;
  }
  .input-row input{
    -webkit-appearance:none;
    appearance:none;
    flex:1;
    border:1.5px solid var(--line);
    border-radius:12px;
    padding:11px 14px;
    font-size:1rem;
    background:#fff;
    color:var(--ink);
    font-family:inherit;
  }
  .input-row input:focus{ outline:2px solid var(--coral-soft); border-color:var(--coral); }
  .input-row button{
    border:none;
    background:var(--ink);
    color:#fff;
    border-radius:12px;
    padding:0 18px;
    font-size:0.9375rem;
    font-weight:600;
    cursor:pointer;
  }
  .chip-list{
    display:flex;
    flex-wrap:wrap;
    gap:8px;
    margin-bottom:18px;
  }
  .chip{
    display:flex;align-items:center;gap:6px;
    background:#fff;
    border:1.5px solid var(--line);
    border-radius:20px;
    padding:7px 8px 7px 14px;
    font-size:0.9375rem;
  }
  .chip button{
    border:none;background:var(--paper-deep);
    width:20px;height:20px;border-radius:50%;
    font-size:0.8125rem;line-height:1;
    color:var(--ink-soft);
    cursor:pointer;
  }
  /* 「今天教的字」清單：跟首頁「我的字庫」同一種方塊（2026-09-15 Steven：字的大小還沒優化）。
     原本是 15px 的小膠囊，同一個字在字庫是 48px 方塊、在這裡小一大截，對不起來；
     ✕ 也只有 20px，手指很容易刪錯字。
     - 方塊 48px、字 1.375rem 粗體 —— 跟 .library-chip 一樣（面板不吃首頁的 --s，所以寫死）。
     - ✕ 移到右上角變小圓章：看起來 18px，**點擊範圍 28px**（外面一圈透明的 padding）。
     📌 只套在 #today-chip-list；查筆順、小測驗的選字 chip 維持原樣。 */
  #today-chip-list{ gap:14px; padding-top:8px; }   /* ✕ 小圓章凸出 12px，間距小於它會壓到隔壁那顆 */
  #today-chip-list .chip{
    position:relative; width:48px; height:48px; padding:0; justify-content:center;
    border:1px solid var(--line); border-radius:8px;
    font-size:1.375rem; font-weight:600; color:var(--ink);
  }
  #today-chip-list .chip button{
    position:absolute; top:-12px; right:-12px;
    width:28px; height:28px; padding:5px; background:transparent;
    display:flex; align-items:center; justify-content:center;
  }
  #today-chip-list .chip button::before{
    content:'✕'; display:flex; align-items:center; justify-content:center;
    width:18px; height:18px; border-radius:50%;
    background:var(--paper-deep); border:1px solid var(--line);
    font-size:0.625rem; font-weight:700; color:var(--ink-soft);
  }
  .chip.selectable{
    cursor:pointer;
    border-color:var(--line);
  }
  .chip.selectable.selected{
    background:var(--coral);
    border-color:var(--coral);
    color:#fff;
  }
  /* 成對出現時的次要按鈕：維持米色，讓旁邊那顆橘的是唯一的主要動作。 */
  .close-sheet.is-cancel{
    background:var(--paper-deep);
    color:var(--ink);
  }

  .btn-primary-sheet{
    background:var(--coral) !important;
    color:#fff !important;
    margin-bottom:8px;
  }
  .btn-primary-sheet:disabled{
    background:var(--paper-deep) !important;
    color:var(--ink-soft) !important;
    cursor:not-allowed;
  }
  .quiz-summary-item{
    display:flex;
    align-items:center;
    justify-content:space-between;
    padding:12px 14px;
    background:#fff;
    border:1.5px solid var(--line);
    border-radius:12px;
    margin-bottom:8px;
    font-size:0.9375rem;
  }
  .quiz-summary-item .status{
    font-size:0.8125rem;
    color:var(--sage);
    font-weight:600;
  }
  .quiz-summary-item.needs-practice .status{
    color:var(--coral);
  }

  /* 全部清除：刻意做成低調的文字連結，不是按鈕。
     它是逃生門（家長貼錯東西時一次清掉重來），不是主要動作，
     不該跟「加入」「完成」搶注意力。 */
  /* 練習畫面的範例開關。跟 .clear-all-link 同一種調性（輔助動作、不搶注意力），
     但置中而不是靠右——它在提示列正下方，靠右會跟上面置中的提示文字錯開。 */
  .outline-link{
    display:block;
    margin:8px auto 0;
    padding:6px 10px;
    background:none;
    border:none;
    font-size:0.8125rem;
    color:var(--ink-soft);
    text-decoration:underline;
    cursor:pointer;
    touch-action:pan-y;
  }

  .clear-all-link{
    display:block;
    margin:-8px 0 18px auto;
    padding:6px 2px;
    background:none;
    border:none;
    font-size:0.8125rem;
    color:var(--ink-soft);
    text-decoration:underline;
    cursor:pointer;
    touch-action:pan-y;
  }

  .empty-note{
    font-size:0.8125rem;
    color:var(--ink-soft);
    background:#fff;
    border:1px dashed var(--line);
    border-radius:12px;
    padding:14px;
    text-align:center;
  }
  .onboarding-hint{
    font-size:0.8125rem;
    color:var(--seal);
    text-align:center;
    margin-top:-4px;
    margin-bottom:12px;
  }
  /* 獎勵的提示。灰的、小的、不加框 —— 它是一個建議，不是待辦事項，
     做得醒目會變成在推銷。 */
  .reward-tip{
    font-size:0.78125rem;
    line-height:1.6;
    color:var(--ink-soft);
    margin:10px 2px 0;
  }
  .repeat-options{
    display:flex;
    gap:10px;
    margin-bottom:20px;
  }
  .repeat-option{
    flex:1;
    border:1.5px solid var(--line);
    background:#fff;
    color:var(--ink);
    border-radius:14px;
    padding:14px 8px;
    font-size:1rem;
    font-weight:600;
    cursor:pointer;
  }
  .repeat-option.active{
    background:var(--coral);
    border-color:var(--coral);
    color:#fff;
  }
  /* 練習中鎖住的選項（例如「文字」資料庫）：灰掉但**看得見**，旁邊的說明會寫「練完再改」。
     藏起來的話家長會以為壞了，找不到路。 */
  .repeat-option:disabled{ opacity:0.4; cursor:default; }
  /* 個人資料四格：一格一行，標籤用 placeholder（多一排標籤會讓設定更長，見「設定已經夠雜」）。 */
  .profile-fields{ display:flex; flex-direction:column; gap:8px; }
  .profile-fields input{
    border:1.5px solid var(--line);
    background:#fff;
    color:var(--ink);
    border-radius:11px;
    padding:11px 12px;
    font-size:1rem;
    font-family:inherit;
    width:100%;
    box-sizing:border-box;
  }
  .repeat-option:disabled.active{ background:var(--coral); border-color:var(--coral); color:#fff; }

  /* 釘在面板底部：內容（圖鑑30格、學過的字清單）長起來之後，
     原本要一路捲到底才按得到關閉按鈕，很不方便。sticky讓按鈕永遠停在
     畫面下緣，內容從後面捲過去。改共用樣式，所有面板一次全部受惠。 */
  /* 🔴 面板底下那顆按鈕一律**橘底白字**（2026-09-12 Steven 定，比照導覽的按鈕）。
     原本是米色的，跟面板底色太接近 —— 扭蛋抽完那顆「太好了」看起來不像能按，
     小孩會不知道要怎麼關掉。這是專案既有的規則（見 31e1a97「按鈕看得出能按」），
     這裡把它套到所有面板。
     ⚠️ 例外是成對出現時的次要那顆（目前只有小測驗的「先不要」）—— 見 .is-cancel。
        兩顆都橘就分不出主次，那比兩顆都米色更糟。 */
  .close-sheet{
    display:block;
    width:100%;
    text-align:center;
    margin-top:6px;
    padding:13px 13px 20px;
    border:none;
    background:var(--coral);
    color:#fff;
    border-radius:14px;
    font-size:0.9375rem;
    font-weight:600;
    cursor:pointer;
    position:sticky;
    bottom:0;
    z-index:2;
  }
  /* 🔴 小測驗面板有上下兩顆按鈕（開始複習／先不要），兩顆都是 position:sticky
     bottom:0，不把上面那顆墊高就會完全疊在一起。
     61px ＝ 下面那顆的高度 55 ＋ 它的 margin-top 6（實測，2026-09-09）。
     陰影補掉中間那 6px 縫，不然捲動時內容會從兩顆之間透出來。

     ⚠️ 這條規則曾經**整個消失**：它的選擇器被併進下面的 .star-burst，
     於是「開始複習」變成一顆 position:fixed、36×36、跑完 burst 動畫後
     opacity:0 的星星特效 —— **按鈕在畫面上完全看不見，小測驗開不了**。
     從 77878ff（改成 src/dist 結構那次）就是壞的，2026-09-09 Steven 回報才發現。
     📌 教訓：兩條規則之間刪東西時，**「}」和下一個選擇器一起看** ——
        少刪一個 }，兩個規則會靜靜合併成一個，不會有任何錯誤訊息。 */
  #start-quiz-btn{
    bottom:61px;
    box-shadow:0 6px 0 var(--paper);
  }

  .star-burst{
    position:fixed;
    pointer-events:none;
    width:36px;height:36px;
    z-index:200;
    animation:burst 0.85s ease-out forwards;
  }
  .star-burst svg{ width:100%; height:100%; }
  @keyframes burst{
    0%{ transform:translate(-50%,-50%) scale(0.3); opacity:0; }
    25%{ transform:translate(-50%,-50%) scale(1.15); opacity:1; }
    100%{ transform:translate(-50%,-160%) scale(0.9); opacity:0; }
  }

  /* =========================================================
     動物收藏：首頁橫向縮圖列 + 扭蛋機抽獎 + 圖鑑頁面
     ========================================================= */
  .collection-header{
    display:flex;
    align-items:center;
    justify-content:space-between;
    margin-bottom:12px;
  }
  .collection-header .library-title{ margin-bottom:0; }
  /* 🔴 **可展開的列**（第三級）。2026-09-09 補上視覺提示。

     原本整條規則只有 cursor:pointer —— 觸控螢幕上**沒有任何提示**說這裡能按。
     現在靠「整列有底色 + 右側 › 箭頭」兩件事，兩件都不依賴游標。
     ⚠️ 箭頭要**一律有**：有些列有、有些沒有，比全部都沒有更糟，
        因為使用者學不出規則。 */
  .tappable-row{
    cursor:pointer;
    background:var(--paper-deep);
    border:1.5px solid var(--line);
    border-radius:14px;
  }
  .collection-view-all{
    display:flex;
    align-items:center;
    gap:2px;
    border:none;
    background:none;
    padding:0;
    /* 🔴 跟左邊的「全部 ∨」同一個尺寸 —— 它們是同一列的兩個控制項，
       大小不一樣那一列就會看起來歪。兩邊要一起改。 */
    font-size:calc(var(--fs-body)*var(--s,1));
    color:var(--ink-soft);
    cursor:pointer;
  }
  .collection-view-all svg{ width:calc(14px*var(--s,1)); height:calc(14px*var(--s,1)); }
  .collection-strip{
    display:flex;
    gap:10px;
    overflow-x:auto;
    padding-bottom:4px;
    -webkit-overflow-scrolling:touch;
  }
  .collection-strip::-webkit-scrollbar{ display:none; }
  .collection-thumb{
    flex:0 0 auto;
    width:calc(48px*var(--s,1));
    height:calc(48px*var(--s,1));
    border-radius:50%;
    background:#fff;
    border:1px solid var(--line);
    overflow:hidden;
  }
  .collection-thumb img{ width:100%; height:100%; display:block; }

  /* ---- 圖鑑：30格方格牆，沒收集到的顯示灰色剪影 ---- */
  .badge-row{
    display:flex;
    flex-direction:column;
    gap:8px;
    margin-bottom:18px;
  }
  /* 🔴 單列橫向捲動（2026-09-09 從兩列改回一列）。

     兩列是為了「21 輪之後高度不要一直長」，但徽章現在是**分頁** ——
     兩列會讓前兩枚上下排，讀起來像清單不像分頁，而且點擊順序變成
     先下再右，跟所有人對分頁的直覺相反。

     ⚠️ **不能用「×21」這種收合**：每一枚徽章代表一輪不同的收藏，
        點進去看到的內容都不一樣，收合等於把那些輪次的內容藏到沒有入口。
        橫向捲動才是對的 —— 它一枚都不會少，高度也只有一列。 */
  .badge-wall{
    display:flex;
    gap:6px;
    overflow-x:auto;
    padding-bottom:2px;
    -webkit-overflow-scrolling:touch;
  }
  .badge-wall::-webkit-scrollbar{ height:4px; }
  .badge-wall::-webkit-scrollbar-thumb{ background:var(--line); border-radius:2px; }
  .badge-wall img{ width:38px; height:auto; display:block; }

  /* 一枚徽章＝一個分頁。選中用**底線**不用外框：
     外框會被 .badge-wall 的 overflow-x 切掉（實測橘圈缺一半），
     而且圓形徽章外面再套一個圈，兩個圓互相干擾很吵。
     底線在格子內部畫，不會溢出，也是分頁的通用語彙。 */
  .badge-tab{
    flex:0 0 auto;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:3px;
    background:none;
    border:none;
    padding:0;
    cursor:pointer;
  }
  .badge-tab::after{
    content:'';
    display:block;
    width:22px;
    height:3px;
    border-radius:2px;
    background:transparent;
  }
  .badge-tab.selected::after{ background:var(--coral); }
  .badge-row .badge-caption{
    font-size:0.8125rem;
    color:var(--ink-soft);
  }
  .gallery-grid{
    display:grid;
    grid-template-columns:repeat(5, 1fr);
    gap:10px;
    margin-bottom:18px;
  }
  .gallery-cell{
    aspect-ratio:1 / 1;
    border-radius:50%;
    background:#fff;
    border:1px solid var(--line);
    overflow:hidden;
  }
  .gallery-cell img{ width:100%; height:100%; display:block; }
  .gallery-cell.locked{
    background:var(--paper-deep);
    display:flex;
    align-items:center;
    justify-content:center;
    color:var(--ink-soft);
    font-size:1.125rem;
  }
  /* 還沒收集到的：換成剪影**檔案**，不是套濾鏡。

     🔴 這裡試過四種做法，最後全部退回：
       · opacity(.32) 淡彩 —— 兩邊都是彩色只差濃淡，看起來像圖檔品質不好
       · grayscale 清晰   —— 等於先把 30 隻都看完，轉出來沒有驚喜
       · 每格換成「?」    —— 30 個一樣的符號，整面是死的
       · blur 模糊        —— 可以，但終究不是剪影

     🔴 濾鏡做不出剪影，原因不是紙圓，是**淺色動物**：紙圓亮度 243，
     白兔子的身體 251、綿羊 246 —— 比紙圓還亮。任何把紙圓推成白的曲線
     也會把兔子推成白的，剪影會變成一個空心輪廓。用顏色分不開。

     所以改用形狀分：從圓的外緣灌水、碰到深色輪廓線就停，灌不到的塗成
     剪影色。那是離線做的（tools/make-silhouettes.cjs），產出的檔案
     一樣帶紙圓，所以這裡什麼都不用設。

     📌 剪影是另一個檔案（NN_name_s.webp），不是同一張圖套效果 ——
     所以這條規則刻意留空。以後想調鎖住的樣子，改的是那支工具。 */
  /* .gallery-cell.locked img —— 不需要任何 filter */
  /* 已收集的才點得開放大檢視；鎖住的維持預設游標、點了沒反應 */
  .gallery-cell:not(.locked){ cursor:pointer; }
  /* touch-action 不會繼承，body 設了不代表這裡有。這格是雙擊縮放的實際災區：
     鎖住的動物點了沒反應 → 小孩再點一次 → 整個畫面被放大。 */
  .gallery-cell{ touch-action:pan-y; }

  /* ---- 動物放大檢視（疊在圖鑑面板上面的第二層） ----
     z-index 要比 .sheet-overlay(100) 高，才不會被圖鑑蓋住；
     但要比 .star-burst(200) 低，星星特效仍該飛在最上層。 */
  .animal-view-overlay{ z-index:150; }
  .animal-view-sheet{ text-align:center; }
  .animal-view-figure{
    width:180px; height:180px;
    margin:8px auto 0;
    border-radius:50%;
    background:#fff;
    border:1px solid var(--line);
    display:flex; align-items:center; justify-content:center;
    overflow:visible;
  }
  .animal-view-figure img{
    width:150px; height:150px; display:block;
    /* 進場彈跳一次，之後持續呼吸。兩個動畫接在一起：第二個delay等於第一個的長度。
       動畫套在整張圖上（不拆解動物內部部件）——這些SVG是靜態圖形、沒有可獨立
       活動的部位，拆解30隻的工作量會爆炸，跟「圖案是現成素材」的定位也不符。
       手法跟扭蛋機的 prizePop 一致。 */
    animation:animalPopIn 0.6s cubic-bezier(.34,1.56,.64,1) both,
              animalBreathe 2.2s ease-in-out 0.6s infinite;
  }
  @keyframes animalPopIn{
    0%{ transform:scale(0.3); opacity:0; }
    60%{ transform:scale(1.12); opacity:1; }
    80%{ transform:scale(0.97); }
    100%{ transform:scale(1); opacity:1; }
  }
  @keyframes animalBreathe{
    0%,100%{ transform:scale(1); }
    50%{ transform:scale(1.06); }
  }
  .animal-view-sheet h3{ margin:16px 0 4px; }
  /* 會暈車/前庭敏感的使用者：關掉動畫，圖案照樣看得到 */
  /* ---- 畫面轉場 ----
     🔴 原本首頁↔練習是 display 直接互換，一幀之內整個畫面換掉，看起來像閃斷
        （Steven：「直接跳轉畫面很怪」）。

     只做**進場**、不做退場：兩個畫面都是一般區塊，要交叉淡入就得同時存在，
     那會讓頁面高度暫時變兩倍、捲軸跳動。進場動畫本身就足夠把「換頁了」講清楚。

     方向有意義：往前（進練習）從右邊滑進來，往回（回首頁）從左邊滑回去 ——
     跟手機的返回手勢同一個方向感，小孩不用學。 */
  @keyframes view-in-fwd{ from{ opacity:0; transform:translateX(14px); } to{ opacity:1; transform:none; } }
  @keyframes view-in-back{ from{ opacity:0; transform:translateX(-14px); } to{ opacity:1; transform:none; } }
  .view-enter-fwd { animation:view-in-fwd  .2s ease-out; }
  .view-enter-back{ animation:view-in-back .2s ease-out; }

  @media (prefers-reduced-motion: reduce){
    .animal-view-figure img{ animation:none; }
    /* 有些孩子對移動敏感；系統設定關掉動態時，這裡也要跟著關。 */
    .view-enter-fwd, .view-enter-back{ animation:none; }
  }

  /* ---- 扭蛋機：轉旋鈕 → 蛋掉下來 → 自動散開 → 圖案彈出 ---- */
  .gacha-sheet{ text-align:center; }
  /* 機器與小毫並排。wrap 是定位基準，小毫用絕對定位貼在機器右邊。 */
  .gacha-machine-wrap{
    position:relative;
    width:270px;
    margin:4px auto 0;
  }
  .gacha-machine-img{
    display:block;
    width:190px;
    height:auto;
    margin-left:0;
  }
  /* 🔴 第二格疊在第一格上面，平常透明。搖的時候才交替顯示。 */
  .gacha-machine-img.frame-b{
    position:absolute;
    inset:0 auto auto 0;
    opacity:0;
  }
  /* steps(1) 是關鍵：硬切才像動畫，漸變會變成兩個鬼影疊在一起。
     ⚠️ 不要 infinite —— 一直翻很吵也耗電，轉一次只播固定次數。 */
  .gacha-machine-wrap.shaking .frame-b{ animation:machineFlip .14s steps(1) 6 alternate; }
  @keyframes machineFlip{ from{ opacity:0; } to{ opacity:1; } }

  /* 小毫貼在機器右下，伸手去轉的位置。 */
  .gacha-mascot{
    position:absolute;
    right:-14px;
    bottom:-10px;
    width:126px;
    height:auto;
    pointer-events:none;
    /* 🔴 水平鏡像。生圖時他是「往**右**伸手轉」，但他站在機器**右邊** ——
       不翻的話他是背對機器往外伸手，看起來像在跟旁邊的空氣握手。
       上下不能鏡像（人會倒過來），左右可以，所以這一張兼顧兩個方向。 */
    transform:scaleX(-1);
  }
  /* 📌 這裡原本有 #gacha-knob 的旋轉樣式（2026-09-10 移除）。
     機器改成整張圖之後旋鈕不再是獨立元素 —— 要保留旋轉就得把旋鈕另外生一張、
     再對齊裝回去，而兩次生成的比例與光源不會一致，裝上去會像貼上去的。
     改成「玻璃罩裡的蛋在翻」，那個訊號對小孩更直接。 */
  /* 旋鈕的待機擺動。幅度放大過一次（2026-08-31）——
     原本 -11/7 度在 iPad 上幾乎看不出來，小孩不會知道那裡可以按。
     它的作用是「指出可以互動的地方」，太含蓄就沒有作用。 */
  @keyframes knobIdle{
    0%,84%,100%{ transform:rotate(0); }
    88%{ transform:rotate(-22deg); }
    93%{ transform:rotate(15deg); }
    97%{ transform:rotate(-6deg); }
  }
  @keyframes knobSpin{ to{ transform:rotate(360deg); } }
  /* 📌 原本的 .gacha-eggs-shake（SVG 蛋群位移）已由上面的兩格交替取代。 */

  .gacha-stage{
    position:relative;
    height:96px;
    display:flex;
    align-items:center;
    justify-content:center;
  }
  .gacha-egg{
    position:absolute;
    width:84px;
    height:84px;
  }
  /* 🔴 破殼：**同一張圖**疊兩層，clip-path 各裁一半。
     那顆蛋本來就有一條水平接縫，剛好切在中線。
     用同一個檔案就不可能有接縫誤差 —— 因為根本是同一張。 */
  .egg-half{
    position:absolute;
    inset:0;
    width:100%;
    height:100%;
    transition:transform .38s cubic-bezier(.3,1.3,.6,1), opacity .38s ease;
  }
  .egg-top   { clip-path:inset(0 0 50% 0); }
  .egg-bottom{ clip-path:inset(50% 0 0 0); }
  .gacha-egg.dropping{ animation:eggDrop 0.55s cubic-bezier(.3,1.4,.6,1); }
  /* 開殼：上半往上飛走、下半往下掉，中間讓出位置給圖案。 */
  .gacha-egg.opening .egg-top   { transform:translateY(-30px) rotate(-16deg); opacity:0; }
  .gacha-egg.opening .egg-bottom{ transform:translateY(22px)  rotate(10deg);  opacity:0; }
  @keyframes eggDrop{
    0%{ transform:translateY(-56px) scale(0.7); opacity:0; }
    60%{ transform:translateY(6px) scale(1.05); opacity:1; }
    100%{ transform:translateY(0) scale(1); opacity:1; }
  }
  .gacha-confetti{
    position:absolute;
    width:90px;
    height:90px;
    pointer-events:none;
  }
  .gacha-prize{
    position:absolute;
    width:92px;
    height:92px;
    animation:prizePop 0.4s cubic-bezier(.3,1.5,.6,1);
  }
  .gacha-prize img{ width:100%; height:100%; display:block; }
  @keyframes prizePop{
    0%{ transform:scale(0.3); opacity:0; }
    70%{ transform:scale(1.12); }
    100%{ transform:scale(1); opacity:1; }
  }
  .gacha-caption{
    margin:6px 0 14px;
    font-size:0.875rem;
    color:var(--ink-soft);
    min-height:20px;
  }
  /* 轉扭蛋的按鈕。原本唯一能點的是SVG裡的旋鈕，實際只有約29×29px——
     低於iOS(44)跟Android(48)的最小觸控建議，而使用者是3-8歲、手指準頭差。
     而且最大最顯眼的蛋艙(約105px)反而不能點，小孩的直覺會落空。
     .btn 預設是 flex:1（給並排的按鈕列用的），這裡是單獨一顆，要改成滿寬。 */
  .gacha-spin-btn{
    flex:none;
    width:100%;
    margin:10px 0 4px;
  }

  /* =========================================================
     星星獎勵：首頁展示區（小孩只能看）+ 設定裡的獎勵設定（大人操作）
     ========================================================= */
  .reward-stars{
    display:flex;
    align-items:center;
    gap:3px;
  }
  .reward-stars svg{ width:calc(18px*var(--s,1)); height:calc(18px*var(--s,1)); }
  .reward-stars .star-num{
    font-size:calc(0.875rem*var(--s,1));
    font-weight:600;
    color:var(--ink);
    margin-left:2px;
  }
  .reward-progress{
    /* 🔴 13px → --fs-body（2026-09-10 Steven 抓到）。它是「我的獎勵」底下的說明，
       跟「我的收藏」「我的字庫」底下那兩句是**同一種東西**，卻小了 3px ——
       漏網的，不是刻意的階層。說明類文字全 App 只有一個尺寸。
       📌 顏色跟其他說明一樣用 --ink-mid —— 說明就是說明，
          不因為講給誰聽而換色（換色只會讓版面看起來髒）。 */
    font-size:calc(var(--fs-body)*var(--s,1));
    color:var(--ink-mid);
    /* 🔴 強制一行（2026-09-10）。這句話裡有**家長自己打的獎勵名稱**，
       名字長一點就換行、首頁跟著變高 —— 那會破壞「首頁不捲」的保證，
       而且是在使用者輸入之後才發生，我們自己測不出來。
       ⚠️ 輸入端也有上限（app.js 的 REWARD_TEXT_MAX），兩道一起才擋得住：
          限制長度擋一般情況，nowrap 擋極端情況（例如超寬的全形字）。 */
    white-space:nowrap;
    overflow:hidden;
    text-overflow:ellipsis;
  }
  #reward-section.locked{ cursor:pointer; }

  .reward-setup input[type="text"],
  .reward-setup input[type="number"]{
    -webkit-appearance:none;
    appearance:none;
    border:1.5px solid var(--line);
    border-radius:12px;
    padding:11px 13px;
    font-size:0.9375rem;
    font-family:inherit;
    background:#fff;
    color:var(--ink);
    width:100%;
    margin-bottom:8px;
  }
  .reward-rule{
    background:#fff;
    border:1px solid var(--line);
    border-radius:14px;
    padding:12px 12px 4px;
    margin-bottom:10px;
  }
  .reward-rule-row{
    display:flex;
    align-items:center;
    gap:8px;
  }
  .reward-rule-row input[type="number"]{ width:78px; flex:0 0 auto; }
  .reward-rule-row span{
    font-size:0.875rem;
    color:var(--ink-soft);
    margin-bottom:8px;
    flex:1;
  }
  .reward-rule-remove{
    flex:0 0 auto;
    width:30px;
    height:30px;
    margin-bottom:8px;
    border:none;
    border-radius:50%;
    background:var(--paper-deep);
    color:var(--ink-soft);
    font-size:0.875rem;
    line-height:1;
    cursor:pointer;
  }
  .reward-rule .reward-redeem-btn{ margin-bottom:8px; }
  .reward-add-btn{
    display:block;
    width:100%;
    padding:11px;
    border:1.5px dashed var(--line);
    border-radius:14px;
    background:none;
    color:var(--ink-soft);
    font-size:0.875rem;
    font-family:inherit;
    cursor:pointer;
  }
  .reward-status{
    font-size:0.8125rem;
    color:var(--ink-soft);
    margin:2px 0 10px;
  }
  .reward-redeem-btn{
    display:block;
    width:100%;
    padding:12px;
    border:none;
    border-radius:14px;
    background:var(--coral);
    color:#fff;
    font-size:0.9375rem;
    font-weight:600;
    font-family:inherit;
    cursor:pointer;
  }
  .reward-redeem-btn:disabled{
    background:var(--paper-deep);
    color:var(--ink-soft);
    cursor:default;
  }
  .reward-locked{
    font-size:0.8125rem;
    color:var(--ink-soft);
    padding:12px 0 4px;
  }

  /* ---- 獎勵檢視面板（小孩看得到目標，但沒有兌換按鈕） ---- */
  .reward-view-stars{
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:3px;
    margin-bottom:18px;
  }
  .reward-view-stars svg{ width:22px; height:22px; }
  .reward-view-stars .star-num{
    font-size:0.875rem;
    color:var(--ink-soft);
    margin-left:6px;
  }
  .reward-view-item{
    background:#fff;
    border:1px solid var(--line);
    border-radius:14px;
    padding:14px 16px;
    margin-bottom:10px;
  }
  .reward-view-item.ready{ border-color:var(--coral); }
  .reward-view-name{
    font-size:1rem;
    font-weight:600;
    color:var(--ink);
    margin-bottom:8px;
  }
  .reward-view-meter{
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:3px;
  }
  .reward-view-meter svg{ width:18px; height:18px; }
  .reward-view-note{
    font-size:0.8125rem;
    color:var(--ink-soft);
    margin-left:6px;
  }
  .reward-view-item.ready .reward-view-note{ color:var(--coral); font-weight:600; }
  .reward-view-empty{
    font-size:0.875rem;
    color:var(--ink-soft);
    padding:8px 0 12px;
  }
  .reward-view-howto{ margin-top:14px; }
  /* 付費牆的說明文字裡有換行，預設會被摺成一行 */
  #paywall-desc-text{ white-space:pre-line; }

  /* 輸入提示有兩種性質完全不同的訊息，顏色要分開：
     warn＝字被擋下來了（紅），info＝只是告知（灰）。
     「已經學會了」是好消息，用紅色會讓家長以為出事了。 */
  #char-warning-hint .hint-warn{ color:var(--seal); }
  #char-warning-hint .hint-info{ color:var(--ink-soft); }
  #char-warning-hint .hint-warn + .hint-info{ margin-top:4px; }

  /* ---- 橫向（landscape）：欄寬與練習格子放寬，維持置中卡片、兩側留白，
     避免內容被拉得過寬、閱讀操作體驗變差。只在橫向生效，直向不受影響。 ---- */
  @media (min-width: 700px) and (orientation: landscape){
    header, #home-view, .stage-wrap{
      max-width:520px;
    }
    .canvas-stage{
      max-width:340px;
    }
    .stamp-char{
      font-size:3.5rem;
    }
  }

  /* ---- 直向（portrait）：不管螢幕多寬（手機還是平板直放），都滿版貼齊邊緣，
     不要置中卡片留白。手機本來就比420px窄，這條規則主要影響直放的平板。 ---- */
  /* 直放的平板（2026-08-31 改）：原本刻意滿版貼齊邊緣，但 iPad 直放時
     768px 寬配上手機比例的字級與間距，看起來又空又散。改成跟橫向一樣置中，
     只是寬一點（520px），閱讀行寬才合理。 */
  @media (orientation: portrait) and (min-width: 421px){
    header, #home-view, .stage-wrap, .sheet{
      max-width:520px;
      margin-left:auto;
      margin-right:auto;
    }
  }

  /* ---- 首頁：全裝置單欄（2026-09-08 Steven 定案，推翻先前 768px 切兩欄）----
     先前為了「iPad 直放別是一條窄帶＋下方全空（Apple HIG：別把 iPhone 版放大）」，
     在 768px 切兩欄、照用途分（左＝要做的事、右＝已完成的成果）。
     Steven 看過實機後決定回單欄，兩個理由都成立：
       1. App **鎖直式** → 當初另一半理由「橫放手機 390px 高、單欄會把內容擠出畫面」消失。
       2. 田字格那邊已定案「下方留白是刻意的、放大反而怪」→ 不再認同「又空又散」的舊顧慮。
     所以不再切兩欄：#home-view 沿用上面 portrait／landscape 的 520px 置中，
     .home-col 維持 display:contents（子項直接堆疊成單欄）。全裝置一致。 */

  /* ---- 平板（≥768）：單欄的「平板尺度」——加寬欄 + 放大首頁字級 ----
     單欄後，520px 的欄配手機字級擺在 iPad（768pt）上會顯得小又空。解法不是回兩欄，
     而是把首頁的欄放寬、字級放大，讓單欄填得滿、讀得舒服。
     🔴 **不要用 zoom**（踩過）：zoom 與 getBoundingClientRect 的座標互動看引擎 ——
        Chromium（桌機）對得上、**WebKit（iPad Safari，我們的真機）對不上**，會讓
        「量出來定位」的開場泡泡、喚醒泡泡等全部跑掉。所以改用不碰座標系的顯式放大。
     🔴 只放大首頁，**不碰練習畫面**（田字格維持 260px 是刻意的）。
     ⚠️ 面板（.sheet）刻意不放大 —— 閱讀用，維持原本好讀的字級。 */
  /* ---- 平板（≥768）：整個首頁用一個旋鈕 --s 放大 ----
     只動 ≥768、只動首頁選擇器；手機版的基礎 px **完全不碰**（零風險）。
     每項寫成 calc(手機基礎px × var(--s))，要調大小**只改 --s 一個數字**。
     🔴 不用 zoom：iPad Safari 會讓靠 JS 量座標定位的浮動元素（開場泡泡、喚醒）歪掉，
        桌機測不出來（見 PROPOSALS #17）。--s 只動字級/尺寸，不碰座標系，泡泡不會壞。
     🔴 不碰練習畫面（田字格 260px 刻意）與面板（.sheet 閱讀用、刻意窄）。
     📌 日後首頁加新元素，就在這裡補一行 calc(px×var(--s))，它就會跟著縮放。 */
  @media (min-width: 768px){
    /* 🔴 整個首頁的放大就靠這一行：--s 是唯一的旋鈕。
       每個尺寸在自己的基礎規則裡已寫成 calc(px*var(--s,1))，這裡只把 --s 從 1 改成 1.2。
       要調大小 → 只改這個數字。手機沒有 --s（fallback 1）→ 尺寸完全不變。 */
    #home-view, header{ --s:1.2; max-width:620px; }
  }

/* ---- 選組：一組一列 ----
   預覽用該組的前三個圖案疊在一起，不是只放文字：
   3~8 歲還不認得「交通工具」四個字，但看得懂圖。 */
.set-list{display:flex;flex-direction:column;gap:10px;margin:16px 0}
.set-row{display:flex;align-items:center;gap:12px;padding:10px 12px;width:100%;
  border:2px solid var(--line);border-radius:14px;background:var(--paper);
  cursor:pointer;text-align:left;font-family:var(--serif);color:var(--ink)}
.set-row.current{border-color:var(--coral);background:var(--seal-wash)}
.set-row-preview{display:flex;flex-shrink:0;padding-right:12px}
.set-row-preview img{width:40px;height:40px;margin-right:-14px}
.set-row-text{flex:1;min-width:0}
.set-row-name{font-weight:700;font-size:1rem}
.set-row-tag{font-size:0.75rem;color:var(--ink-soft);margin-top:2px}
/* 鎖住的組別照樣看得到輪廓 —— 跟圖鑑未收集的格子同一個做法：
   看得到有東西比一片問號更想解鎖。 */
.set-row.locked{opacity:.7}
/* 鎖住的組別：預覽圖要彩色、清楚 —— 這裡跟圖鑑的目的相反。
   圖鑑的淡是「還沒拿到」，這裡是「要付錢才有」；
   把商品蓋起來賣不掉東西。鎖的訊息交給右邊的文字講。 */
.set-row.locked .set-row-preview img{filter:none}

/* 付費說明裡的組別預覽：跟選組畫面同一套視覺語彙 */
.paywall-preview{display:flex;justify-content:center;padding:6px 0 12px}
.paywall-preview img{width:52px;height:52px;margin-right:-10px}

/* 家長門檻：題目要大、輸入要好按（大人單手拿手機） */
/* ---- 首頁導覽的泡泡 ----
   ⚠️ 版面上最容易出錯的一件事：泡泡跟著目標**水平**移動。
      目標靠右時泡泡就會超出螢幕，四種語言字長不同會更慘。
      所以泡泡永遠置中、寬度固定（跟喚醒提醒同一條規則），
      **只有尖角左右移動去指目標**。這樣不管指哪一張卡都不會破版。
   ⚠️ ::after 是 .speech-bubble 共用的尖角，這裡只改位置與邊框方向：
      泡泡在目標下面時尖角朝上，在上面時朝下。 */
#tour-catch{position:fixed;inset:0;z-index:60}
#tour-ring{
  position:fixed;
  z-index:61;
  pointer-events:none;   /* 點擊要穿過去給 tour-catch */
  border:2px solid var(--coral);
  border-radius:16px;
  /* 往外擴散一大圈陰影＝把其餘畫面壓暗，框內維持原樣。
     不用挖洞、不用動目標的 z-index。 */
  box-shadow:0 0 0 9999px rgba(43,38,34,0.55);
}
/* 🔴 導覽的小毫。跟泡泡同一層（62），浮在首頁上面 —— 首頁不會被推擠。
   他站在 #tour-ring 壓暗的區域上，而陰影在 61 層，所以他自己是亮的，會跳出來。
   ⚠️ pointer-events:none 必須留著：導覽是「點畫面任何地方＝下一步」，
      那個點擊由 60 層的 #tour-catch 接；小毫在 62 層，不設穿透就會擋掉。 */
#tour-mascot{
  position:fixed;
  z-index:62;
  /* 🔴 尺寸只寫在這裡一份。placeTour() 會讀 --mascot-size 來算泡泡的位置 ——
     以前它讀 offsetWidth，但第一次呼叫時小毫還是 display:none、offsetWidth=0，
     於是吃到 fallback 96，泡泡就往上疊到小毫身上 24px（實測）。
     📌 96 → 130px（2026-09-10 Steven：太小）。圖檔 300px 見方，放到 130 不會糊。 */
  width:var(--mascot-size);
  height:auto;
  pointer-events:none;
}

#tour-bubble{
  position:fixed;
  z-index:62;
  width:calc(100% - 28px);
  max-width:406px;
  padding:12px 14px 10px;
}
#tour-bubble p{margin:0 0 8px}
.tour-actions{display:flex;align-items:center;justify-content:space-between;gap:12px}
/* 略過做成小字：它是逃生門不是主要動作，跟「全部清除」同一種調性。 */
.tour-skip{
  background:none;border:none;padding:2px 0;
  font-family:inherit;font-size:0.78125rem;
  color:var(--ink-soft);text-decoration:underline;cursor:pointer;
}
.tour-next{background:var(--coral);color:#fff;border:none;border-radius:12px;
  font-family:inherit;font-size:var(--fs-body);font-weight:700;padding:8px 18px;
  box-shadow:0 3px 0 var(--seal)}
/* 尖角的水平位置由 JS 給（--arrow-x），指向目標的中心。
   預設泡泡在目標下面 → 尖角朝上；.tour-above 時翻到下緣、方向反過來。 */
#tour-bubble::after{left:var(--arrow-x,26px);margin-left:-5px;top:-6px;
  border:none;border-left:1.5px solid var(--coral-soft);border-top:1.5px solid var(--coral-soft);}
#tour-bubble.tour-above::after{top:auto;bottom:-6px;
  border:none;border-right:1.5px solid var(--coral-soft);border-bottom:1.5px solid var(--coral-soft);}

/* ---- 開場說明（給大人看的那一頁）----
   這是家長看到的第一段長文字，它決定他要不要每天陪著開這個 App，
   所以排版目標只有一個：**看起來像可以一分鐘讀完**。
   做法是三段各自有小標，用左邊一條珊瑚色的線切開 —— 家長掃過三個小標
   就知道大意，願意細看的才往下讀。整段等寬的文字會被直接跳過。
   ⚠️ 面板比別張高（78vh 幾乎滿），底部要留內距，
      不然按鈕會貼到手機底緣的手勢區。 */
.welcome-sheet{padding-bottom:20px}
/* 標題跟第一段之間要留一口氣。原本那行「這一頁是寫給大人看的」順便撐開了
   這段間距，2026-09-07 拿掉之後標題就直接貼在第一個小標上。 */
#welcome-points{margin-top:16px}
.welcome-point{
  border-left:3px solid var(--coral);
  padding:2px 0 2px 12px;
  margin-bottom:16px;
}
.welcome-point h4{
  margin:0 0 5px;
  font-size:0.9375rem;
  color:var(--ink);
}
.welcome-point p{
  margin:0;
  font-size:0.84375rem;
  line-height:1.65;
  color:var(--ink-soft);
}
/* 最後一段（這個 App 怎麼做）換個顏色：前三段講的是「為什麼」，
   這一段講的是「所以我們做了什麼」，不該看起來像第四個理由。 */
.welcome-point.welcome-how{border-left-color:var(--line);margin-bottom:18px}

/* ---- 設定面板：整體縮一號 ----
   這一張面板裡有四排選項＋獎勵設定＋備份＋重設，用練習畫面那種按鈕尺寸
   會長到兩個螢幕以上，家長根本捲不到下面的東西（重設就是被埋在最底下）。
   ⚠️ 只縮設定裡的，用 #settings-overlay 圈住 —— 同一組 class 也用在
      門檻、付費說明、備份、範圍篩選，那些地方只有一排，維持原本的大小。
   間距改由 CSS 給（原本四個標題各自掛 inline 的 margin-top），
   這樣才調得動，不用每個都加 !important。 */
#settings-overlay .sheet{padding:18px 18px 0}
#settings-overlay h3{font-size:1.0625rem}
#settings-overlay p.sub{margin:14px 0 7px;font-size:0.78125rem}
/* 每項設定的粗體前導小標。深墨+粗做出層次,說明維持灰;兩者同一行,不加高。 */
#settings-overlay p.sub b{color:var(--ink);font-weight:600;margin-right:5px}
#settings-overlay h3 + p.sub{margin-top:0}
#settings-overlay .repeat-options{gap:8px;margin-bottom:0}
#settings-overlay .repeat-option{padding:9px 6px;font-size:0.875rem;border-radius:11px}

/* 確認對話框。訊息有分段（四種語言都靠 
 分行），要保留換行。
   底部補內距 —— 這個面板沒有釘在底下的關閉列，不補的話按鈕會貼到螢幕邊。 */
.confirm-sheet{padding-bottom:22px}
.confirm-body{white-space:pre-line;margin-bottom:18px}
/* 這些對話框問的都是不可逆的動作，確定那一顆要看得出來是「會發生事情」的那顆。
   取消放左邊、確定放右邊：誤觸時先碰到的是安全的那一顆。 */
.confirm-danger{background:var(--coral);border-color:var(--coral);color:#fff}
.gate-question{font-size:2.125rem;font-weight:700;text-align:center;margin:14px 0 10px;letter-spacing:2px}
.gate-input{width:100%;box-sizing:border-box;font-size:1.625rem;text-align:center;padding:10px;
  border:2px solid var(--line);border-radius:12px;background:var(--paper);color:var(--ink);
  font-family:var(--serif)}
.gate-input:focus{outline:none;border-color:var(--coral)}
.gate-error{text-align:center;font-size:0.8125rem;color:var(--seal);margin:6px 0 2px}

/* 付費說明裡的警示列。圖示與文字同色、頂端對齊 ——
   文字換行時圖示要留在第一行，不要垂直置中飄到中間。 */
.paywall-warn{
  display:flex; align-items:flex-start; gap:7px;
  margin:2px 0 6px; font-size:0.8125rem; line-height:1.75; color:var(--seal);
}
.warn-icon{ width:17px; height:17px; flex-shrink:0; margin-top:4px; }
