  :root{
    /* 🔴 田字格的大小只有這一個數字。練習畫布跟查筆順的預覽都讀它。
       原本兩邊各寫死一個（練習 260、查筆順 200），結果查筆順的字
       莫名其妙比練習的小一圈 —— 那不是設計，是兩個地方沒對齊。
       它們是同一種東西（同一個田字格語彙），就該是同一個數字。 */
    --char-box:260px;
    --paper:#FAF3E7;
    --paper-deep:#F0E6D2;
    --ink:#211C18;
    --ink-soft:#7A6F61;
    --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;
    --serif:-apple-system,"PingFang TC","Microsoft JhengHei","Noto Sans TC",sans-serif;
  }
  *{box-sizing:border-box;}
  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{
    min-height:100vh;
    display:flex;
    flex-direction:column;
    align-items:center;
    padding:20px 16px 32px;
  }

  #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:15px;
    cursor:pointer;
    text-align:left;
    display:flex;
    align-items:center;
    gap:14px;
  }
  .lang-flag{ font-size:22px; }
  .lang-name{ font-weight:700; }
  .lang-app-name{ font-size:12px; 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 指出）。 */
  }
  .brand{
    font-family:var(--serif);
    font-size:18px;
    font-weight:700;
    letter-spacing:1px;
    color:var(--ink);
    display:flex;
    align-items:center;
    gap:8px;
  }
  /* 小紅點同時是語言選擇的隱藏入口（連點 5 下）。
     ⚠️ 7px 的東西手指點不到 —— 用透明外框把命中範圍撐到 24px，外觀不變。 */
  .brand .dot{
    width:7px;height:7px;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:36px;height:36px;
    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:18px;height:18px;}

  .revive-card{
    width:100%;
    max-width:420px;
    background:var(--sage-soft);
    border-radius:18px;
    padding:16px 18px;
    margin-bottom:14px;
  }
  .revive-card p{
    margin:0 0 12px;
    font-size:14px;
    color:var(--ink);
    line-height:1.5;
  }
  .revive-actions{
    display:flex;
    gap:10px;
  }
  .revive-actions .btn{
    padding:10px;
    font-size:14px;
  }

  #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; }

  /* ---- 今日字卡：簽名元素，像一張落了印章的紙條 ---- */
  /* 首頁的進度：放在「接下來練習」卡片的右下角。

     原本是卡片下面獨立的一條橫帶，但卡片右側一直空著一大塊（字在左邊、
     印章在右上）。搬進來同時解決兩件事：版面短一截，右側不再是空白。

     ⚠️ 絕對定位在卡片內 —— 卡片本身是一顆按鈕，文字放進去不影響點擊。 */
  /* ⚠️ 卡片只有 136px 高，右側要塞下印章、查筆順、進度三個東西。
     進度維持**一行** —— 兩行會跟上面的按鈕疊在一起（第一版就是這樣）。
     右側橫向空間其實很夠，一行不會擠。 */
  .stat-line{
    position:absolute;
    right:20px;
    bottom:14px;
    text-align:right;
    font-size:12px;
    line-height:1.7;
    color:var(--ink-soft);
  }
  .stat-line b{
    font-size:14px;
    color:var(--ink);
  }

  .today-stamp-card{
    position:relative;
    background:#fff;
    border:1px solid var(--line);
    border-radius:6px;
    /* 收緊過一次：原本 24/22/22 + margin 22，整張 178px 太重（見上方說明） */
    padding:16px 20px 16px;
    margin-bottom:16px;
    cursor:pointer;
    overflow:hidden;
  }
  .stamp-eyebrow{
    font-size:11px;
    letter-spacing:2px;
    color:var(--ink-soft);
    text-transform:uppercase;
    margin-bottom:8px;
  }
  .stamp-char{
    font-family:var(--serif);
    /* 78 → 56（下方 media query）、64 → 48：這個字是預覽不是主角，
       縮小之後卡片仍然是首頁最醒目的東西，只是不再誇張。 */
    font-size:48px;
    font-weight:600;
    color:var(--ink);
    line-height:1;
    margin-bottom:12px;
  }
  .stamp-cta{
    display:flex;
    align-items:center;
    gap:6px;
    font-size:15px;
    font-weight:600;
    color:var(--seal);
  }
  .stamp-cta svg{ width:15px; height:15px; }

  /* ---- 統計細線列 ---- */

  /* ---- 文字連結式入口（小測驗） ---- */
  .text-link-row{
    position:relative;
    width:100%;
    background:none;
    border:none;
    padding:13px 4px 13px 14px;
    display:flex;
    align-items:center;
    justify-content:space-between;
    font-size:14px;
    font-weight:600;
    color:var(--ink);
    cursor:pointer;
  }
  .text-link-row svg{ width:15px; height:15px; color:var(--ink-soft); }

  /* 🔴 所有右側的箭頭都要落在同一條垂直線上（距容器邊緣 4px）。
     「新增今天的字」有底色，內距必須是 14px 否則字會貼到色塊邊緣 ——
     所以用負邊距把它的箭頭拉回來對齊，而不是改它的內距。 */
  .row-primary > svg{ margin-right:-10px; }
  .row-chevron{
    width:15px;
    height:15px;
    color:var(--ink-soft);
    margin-left:auto;
    flex:none;
  }

  /* 連結列也帶同一條橘槓 —— 首頁每一塊都要有標記，少一個就會看起來漏掉。
     ⚠️ 必須絕對定位：這一列是 space-between 的 flex，
        用 inline 的 ::before 會變成第三個項目，把文字擠到中間。
     ⚠️「新增今天的字」不用，它已經有橘色圓形加號，兩個標記會打架。 */
  .text-link-row:not(.row-primary)::before{
    content:'';
    position:absolute;
    left:4px;
    top:50%;
    transform:translateY(-50%);
    width:3px;
    height:11px;
    border-radius:2px;
    background:var(--coral);
  }

  /* 家長每天的主要動作，重一階。跟「繼續練習」不衝突 ——
     那張卡片是小孩要按的，這一條是大人要按的，兩者的重量不該一樣。 */
  .row-primary{
    background:var(--seal-wash);
    border-radius:14px;
    padding:14px 14px 14px 12px;
    margin:2px 0 6px;
    font-size:15px;
  }
  .row-icon{
    display:flex;
    align-items:center;
    justify-content:center;
    width:26px;
    height:26px;
    border-radius:50%;
    background:var(--coral);
    flex:none;
  }
  .row-icon svg{ width:15px; height:15px; color:#fff; }
  .row-primary:active{ transform:scale(0.99); }

  /* ---- 我的字庫：填補首頁下方，也讓家長一眼看到學過的字 ---- */
  /* 動物收藏／我的獎勵／我的字庫共用。跟上面兩條連結列用同一套節奏：
     上分隔線 + 12px 上內距 + 4px 左右縮排，整頁才會是整齊的橫帶。 */
  .library-section{
    padding:16px 4px 14px;
  }
  /* 區塊標題（動物收藏／我的獎勵／我的字庫共用）。
     加粗、顏色加深：原本是淺灰細字，在一堆內容中間分不出哪裡是段落的開頭。 */
  /* 區塊標題左邊的短豎線：分區靠它，不靠橫線。 */
  .library-title::before{
    content:'';
    display:inline-block;
    width:3px;
    height:11px;
    margin-right:7px;
    vertical-align:-1px;
    border-radius:2px;
    background:var(--coral);
  }
  .library-title{
    font-size:12px;
    font-weight:700;
    letter-spacing:1.5px;
    color:var(--ink);
    text-transform:uppercase;
    margin-bottom:10px;
  }
  .library-grid{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
  }
  .library-chip{
    width:48px;
    height:48px;
    display:flex;
    align-items:center;
    justify-content:center;
    background:#fff;
    border:1px solid var(--line);
    border-radius:8px;
    font-size:22px;
    font-weight:600;
    color:var(--ink);
  }
  .library-empty{
    font-size:13px;
    color:var(--ink-soft);
    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:13px;
    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;
  }
  /* 🔴 練習畫布連捲動都不能有：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; }

  /* 按鈕裡的圖示與文字不吃事件 —— 手指碰到它們時，瀏覽器直接把命中目標算成
     按鈕本身。這是比 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%;
  }

  /* 漫畫式對話框（共用語言）。
     練習畫面的 .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);
    font-size:13px;
    line-height:1.6;
  }
  .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);
  }

  /* 喚醒提醒：浮在版面上，不佔位置（像 pop up）。
     **說話的是 App 本身**，不是畫面上的某個字，所以尖角朝上、從畫面上方冒出來。

     用 fixed 不是 absolute：捲動時它要跟著留在畫面上 —— 它是通知，不是內容。
     z-index 50 介於一般內容與面板（sheet 是 100）之間：面板打開時要蓋過它。 */
  .revive-bubble{
    position:fixed;
    top:62px;
    left:50%;
    transform:translateX(-50%);
    width:calc(100% - 28px);
    max-width:406px;
    z-index:50;
  }
  .revive-bubble::after{ top:-6px; left:26px; border:none;
    border-left:1.5px solid var(--coral-soft); border-top:1.5px solid var(--coral-soft); }
  .revive-bubble p{ margin:0 16px 6px 0; }
  .bubble-cta{
    color:var(--coral);
    font-weight:600;
    cursor:pointer;
  }
  .bubble-close{
    position:absolute;
    top:4px; right:6px;
    border:none; background:none;
    color:var(--ink-soft);
    font-size:13px;
    line-height:1;
    padding:4px;
    cursor:pointer;
  }

  .mistake-bubble{
    position:absolute;
    top:-14px;
    right:-8px;
    background:#fff;
    border:1.5px solid var(--coral-soft);
    color:var(--ink);
    font-size:13px;
    padding:8px 14px;
    border-radius:16px;
    box-shadow:0 6px 16px -8px rgba(43,38,34,0.3);
    pointer-events:none;
    opacity:0;
    transform:translateY(4px) scale(0.92);
    transition:opacity .2s, transform .2s;
    white-space:nowrap;
    z-index:10;
  }
  .mistake-bubble::after{
    content:'';
    position:absolute;
    bottom:-5px;
    right:22px;
    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);
  }
  .mistake-bubble.show{
    opacity:1;
    transform:translateY(0) scale(1);
  }
  .controls{
    width:100%;
    display:flex;
    gap:10px;
    margin-top:18px;
  }
  .btn{
    flex:1;
    border:none;
    border-radius:14px;
    padding:13px 10px;
    font-size:15px;
    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）。 */
  .hint-banner{
    position:absolute;
    top:-40px;
    right:-18px;
    z-index:5;
    /* 右上角整塊讓給泡泡了（「N 個字已學會」已移除，字庫就看得到那個數字）。
       但寬度仍然要收 —— 蓋住太多字，小孩就看不到自己要寫什麼。 */
    max-width:58%;
    text-align:left;
    font-size:12.5px;
    line-height:1.5;
    padding:8px 12px;
    color:var(--ink-soft);
    transition:opacity .2s;
  }
  .hint-banner:empty{ display:none; }
  .hint-banner::after{ bottom:-6px; left:20px; }

  .practice-record{
    margin-top:10px;
    width:100%;
    text-align:center;
    font-size:12px;
    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;
    max-height:78vh;
    overflow-y:auto;
  }
  /* 扭蛋機面板的按鈕一開始是隱藏的（要轉完才出現），
     底部沒內距的話文字會貼到螢幕邊緣，這裡補回來 */
  .gacha-sheet{ padding-bottom:24px; }
  .sheet h3{
    margin:0 0 4px;
    font-size:18px;
  }
  .sheet p.sub{
    margin:0 0 16px;
    font-size:13px;
    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;
    font-size:13px;
    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:13px;
    font-family:inherit;
  }
  .range-dash{ color:var(--ink-soft); }

  /* 卡片右側的「查筆順」入口：一個放大好幾倍的放大鏡圖示。

     不做成有邊框的小膠囊 —— 那樣它跟旁邊的印章、進度擠成一團小東西。
     圖示本身夠大就看得懂，也填得住卡片右邊那塊空白。
     ⚠️ 它疊在一張本身可點的卡片上，事件冒泡要擋（見 app.js）。 */
  .card-lookup-btn{
    position:absolute;
    right:22px;
    /* 垂直置中，跟左邊那個大字對齊 —— 靠上時它跟進度擠在同一側的上下，
       置中之後左右各一個主體（字、放大鏡），版面才平衡。
       ⚠️ 進度在卡片底部，所以置中時要避開它：位移一點點往上。 */
    top:50%;
    transform:translateY(-58%);
    display:flex;
    align-items:center;
    justify-content:center;
    padding:0;
    border:none;
    background:none;
    color:var(--coral);
    cursor:pointer;
  }
  .card-lookup-btn svg{ width:60px; height:60px; }
  .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:13px;
    color:var(--ink-soft);
  }
  .lookup-controls{
    display:flex;
    gap:8px;
    margin-bottom:4px;
  }
  /* 三顆控制鍵比練習畫面的主要按鈕小一階 —— 這是工具不是主要流程 */
  .lookup-controls .btn{ padding:10px 6px; font-size:14px; }

  .review-tag{
    display:inline-block;
    margin-left:6px;
    padding:2px 7px;
    border-radius:8px;
    background:var(--paper-deep);
    color:var(--ink-soft);
    font-size:11px;
    font-weight:600;
  }

  .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:14px;
    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:13px;
    line-height:1.75;
    color:var(--ink-soft);
    /* 文案裡的 \n 要真的換行。沒有這行的話「包含…」和「⚠️ 不含收藏組」
       會被摺成同一段，兩件相反的事讀起來像同一句。 */
    white-space:pre-line;
  }
  .info-section p{
    margin:0 0 16px;
    font-size:13px;
    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:16px;
    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:15px;
    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:15px;
  }
  .chip button{
    border:none;background:var(--paper-deep);
    width:20px;height:20px;border-radius:50%;
    font-size:13px;line-height:1;
    color:var(--ink-soft);
    cursor:pointer;
  }
  .chip.selectable{
    cursor:pointer;
    border-color:var(--line);
  }
  .chip.selectable.selected{
    background:var(--coral);
    border-color:var(--coral);
    color:#fff;
  }
  .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:15px;
  }
  .quiz-summary-item .status{
    font-size:13px;
    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:13px;
    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:13px;
    color:var(--ink-soft);
    text-decoration:underline;
    cursor:pointer;
    touch-action:pan-y;
  }

  .empty-note{
    font-size:13px;
    color:var(--ink-soft);
    background:#fff;
    border:1px dashed var(--line);
    border-radius:12px;
    padding:14px;
    text-align:center;
  }
  .onboarding-hint{
    font-size:13px;
    color:var(--seal);
    text-align:center;
    margin-top:-4px;
    margin-bottom:12px;
  }
  /* 獎勵的提示。灰的、小的、不加框 —— 它是一個建議，不是待辦事項，
     做得醒目會變成在推銷。 */
  .reward-tip{
    font-size:12.5px;
    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:16px;
    font-weight:600;
    cursor:pointer;
  }
  .repeat-option.active{
    background:var(--coral);
    border-color:var(--coral);
    color:#fff;
  }

  /* 釘在面板底部：內容（圖鑑30格、學過的字清單）長起來之後，
     原本要一路捲到底才按得到關閉按鈕，很不方便。sticky讓按鈕永遠停在
     畫面下緣，內容從後面捲過去。改共用樣式，所有面板一次全部受惠。 */
  .close-sheet{
    display:block;
    width:100%;
    text-align:center;
    margin-top:6px;
    padding:13px 13px 20px;
    border:none;
    background:var(--paper-deep);
    color:var(--ink);
    border-radius:14px;
    font-size:15px;
    font-weight:600;
    cursor:pointer;
    position:sticky;
    bottom:0;
    z-index:2;
  }
  /* 小測驗面板有上下兩顆按鈕（開始複習／先不要），
     上面那顆要往上墊一顆按鈕的高度，不然兩顆會疊在一起 */
  /* 小測驗面板有上下兩顆按鈕（開始複習／先不要）：上面那顆要往上墊
     一顆按鈕的高度才不會疊在一起，並用同色陰影補掉兩顆之間6px的margin縫 */
  #start-quiz-btn,
  .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; }
  .tappable-row{ cursor:pointer; }
  .collection-view-all{
    display:flex;
    align-items:center;
    gap:2px;
    border:none;
    background:none;
    padding:0;
    font-size:13px;
    color:var(--ink-soft);
    cursor:pointer;
  }
  .collection-view-all svg{ width:14px; height:14px; }
  .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:48px;
    height:48px;
    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;
  }
  /* 徽章牆固定兩列高、自己橫向捲動：21輪之後全是同色的硃紅徽章，
     再多也不會有新資訊，讓它無限往下長會把動物格子整個推出畫面外。
     grid-auto-flow:column＋兩列，徽章會一欄兩個往右排，高度永遠固定。 */
  .badge-wall{
    display:grid;
    grid-template-rows:repeat(2, auto);
    grid-auto-flow:column;
    grid-auto-columns:38px;
    gap:6px;
    overflow-x:auto;
    padding-bottom:4px;
    -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-row .badge-caption{
    font-size:13px;
    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:18px;
  }
  /* 還沒收集到的：換成剪影**檔案**，不是套濾鏡。

     🔴 這裡試過四種做法，最後全部退回：
       · 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; }
  /* 會暈車/前庭敏感的使用者：關掉動畫，圖案照樣看得到 */
  @media (prefers-reduced-motion: reduce){
    .animal-view-figure img{ animation:none; }
  }

  /* ---- 扭蛋機：轉旋鈕 → 蛋掉下來 → 自動散開 → 圖案彈出 ---- */
  .gacha-sheet{ text-align:center; }
  .gacha-machine-wrap{ padding-top:4px; }
  .gacha-machine{ width:170px; height:auto; overflow:visible; }
  #gacha-knob{ transform-origin:150px 186px; cursor:pointer; }
  .gacha-knob-idle{ animation:knobIdle 1.6s ease-in-out infinite; }
  .gacha-knob-spin{ animation:knobSpin 0.7s cubic-bezier(.35,0,.25,1); }
  /* 旋鈕的待機擺動。幅度放大過一次（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{ animation:eggsShake 0.16s 4; }
  @keyframes eggsShake{
    0%,100%{ transform:translateY(0); }
    25%{ transform:translateY(-3px) rotate(-2deg); }
    75%{ transform:translateY(3px) rotate(2deg); }
  }

  .gacha-stage{
    position:relative;
    height:96px;
    display:flex;
    align-items:center;
    justify-content:center;
  }
  .gacha-egg{
    position:absolute;
    width:76px;
    height:76px;
    transition:transform .3s ease, opacity .3s ease;
  }
  .gacha-egg.dropping{ animation:eggDrop 0.55s cubic-bezier(.3,1.4,.6,1); }
  .gacha-egg.opening{ transform:scale(1.15); 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:14px;
    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:18px; height:18px; }
  .reward-stars .star-num{
    font-size:14px;
    font-weight:600;
    color:var(--ink);
    margin-left:2px;
  }
  .reward-progress{
    font-size:13px;
    color:var(--ink-soft);
  }
  #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:15px;
    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:14px;
    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:14px;
    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:14px;
    font-family:inherit;
    cursor:pointer;
  }
  .reward-status{
    font-size:13px;
    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:15px;
    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:13px;
    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:14px;
    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:16px;
    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:13px;
    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:14px;
    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, .revive-card, #home-view, .stage-wrap{
      max-width:520px;
    }
    .canvas-stage{
      max-width:340px;
    }
    .stamp-char{
      font-size:56px;
    }
  }

  /* ---- 直向（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;
    }
    /* 浮動的喚醒泡泡也要跟著收 —— 它是 fixed，不吃上面那條 */
    .revive-bubble{ max-width:496px; }
  }

  /* ---- 首頁：全裝置單欄（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）上會顯得小又空。
     解法不是回兩欄，而是把**整個首頁**等比放大：字、卡片、間距一起大，填得滿。
     🔴 只放大首頁與標題列，**不碰練習畫面** —— 田字格維持 260px 是刻意定案的
        （放大寫起來反而怪），跟著 zoom 會破壞它。
     📌 為什麼用 zoom 不改十幾處 px 字級：這版 CSS 全用 px，zoom 能一次等比放大
        整塊（字/卡片/間距/圖示），Safari/iPad 支援良好，且**正常重排**（不像
        transform:scale 會留原尺寸的框、破壞置中與捲動）。
     ⚠️ 面板（.sheet）刻意不放大 —— 那是閱讀用的，維持原本好讀的字級。 */
  @media (min-width: 768px){
    header, #home-view{ zoom: 1.2; }
  }

/* ---- 選組：一組一列 ----
   預覽用該組的前三個圖案疊在一起，不是只放文字：
   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:16px}
.set-row-tag{font-size:12px;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);
}
#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:12.5px;
  color:var(--ink-soft);text-decoration:underline;cursor:pointer;
}
.tour-next{background:none;border:none;font-family:inherit;font-size:14px;padding:2px 0}
/* 尖角的水平位置由 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:15px;
  color:var(--ink);
}
.welcome-point p{
  margin:0;
  font-size:13.5px;
  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:17px}
#settings-overlay p.sub{margin:14px 0 7px;font-size:12.5px}
/* 每項設定的粗體前導小標。深墨+粗做出層次,說明維持灰;兩者同一行,不加高。 */
#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:14px;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:34px;font-weight:700;text-align:center;margin:14px 0 10px;letter-spacing:2px}
.gate-input{width:100%;box-sizing:border-box;font-size:26px;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:13px;color:var(--seal);margin:6px 0 2px}

/* 付費說明裡的警示列。圖示與文字同色、頂端對齊 ——
   文字換行時圖示要留在第一行，不要垂直置中飄到中間。 */
.paywall-warn{
  display:flex; align-items:flex-start; gap:7px;
  margin:2px 0 6px; font-size:13px; line-height:1.75; color:var(--seal);
}
.warn-icon{ width:17px; height:17px; flex-shrink:0; margin-top:4px; }
