  :root{
    --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{
    width:100%;
    max-width:420px;
  }

  /* ---- 今日字卡：簽名元素，像一張落了印章的紙條 ---- */
  /* 首頁的進度：放在「接下來練習」卡片的右下角。

     原本是卡片下面獨立的一條橫帶，但卡片右側一直空著一大塊（字在左邊、
     印章在右上）。搬進來同時解決兩件事：版面短一截，右側不再是空白。

     ⚠️ 絕對定位在卡片內 —— 卡片本身是一顆按鈕，文字放進去不影響點擊。 */
  /* ⚠️ 卡片只有 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;}
  .canvas-stage{
    position:relative;
    width:100%;
    max-width:260px;
    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-canvas-holder{
    width:100%;
    height:100%;
    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;
  }

  /* 漫畫式對話框（共用語言）。
     練習畫面的 .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); }
  /* 徽章說明現在有兩行（已完成 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;
  }
  /* 字放在紙色的方框裡，跟練習畫面的畫布同一個語彙，一眼看得出「這是那個字」 */
  #lookup-canvas{
    width:200px;
    height:200px;
    margin:0 auto;
    background:#FFFDF8;
    border:1px solid var(--line);
    border-radius:12px;
  }
  .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);
  }
  .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;
  }
  .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,
  .onboarding-mode #close-sheet-btn{
    bottom:60px;
    z-index:3;
    padding-bottom:13px;
    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; }
  .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;
  }
  .gallery-cell.locked img{
    filter:grayscale(1) opacity(0.28);
  }
  /* 已收集的才點得開放大檢視；鎖住的維持預設游標、點了沒反應 */
  .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; }
  }

/* ---- 選組：一組一列 ----
   預覽用該組的前三個圖案疊在一起，不是只放文字：
   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:grayscale(1) opacity(.5)}
